The Mine Works
Naukri API 2025: How to Programmatically Access India's Largest Job Board
← All posts
tutorial June 2, 2025 · 13 min read Updated September 17, 2026

Naukri API 2025: How to Programmatically Access India's Largest Job Board

Naukri has no public API. This guide covers the session-warming approach that gets past Akamai bot detection and returns structured job data.

Try the scraper

The actor referenced in this article. Pay only for results delivered.

View the scraper →

Naukri.com is India’s largest job board with over 70 million registered job seekers and 70,000+ active recruiters. There is no official Naukri API for third-party access. For HR tech companies, job aggregators, and labor market researchers who need programmatic access to Naukri job data, web scraping is the only path.

TL;DR: Naukri has no public API and uses Akamai Bot Manager. Programmatic access requires session warming: visit the homepage with a stealth-patched Playwright browser to generate valid ak_bmsc cookies and capture the nkparam signed header, then use those credentials for direct JSON API calls. Indian residential proxies significantly improve success rates over US datacenter IPs.

The problem is that Naukri runs Akamai Bot Manager, one of the more sophisticated bot detection systems on the market. A naive scraper using requests or even a basic Playwright setup will get blocked within seconds. This post explains what Naukri’s bot detection actually checks for, and how to get reliable job data from it.

What Naukri’s Bot Detection Checks

Akamai Bot Manager evaluates several signals:

1. The nkparam signed header Every Naukri search request requires an nkparam header containing a signed token. This token is generated client-side by Naukri’s JavaScript and is tied to the current session, timestamp, and request parameters. Without a valid nkparam, the API returns a 406 with a “recaptcha required” message.

2. Akamai cookies (ak_bmsc, bm_sv) Akamai seeds session cookies through JavaScript challenges on page load. These cookies are used to verify that a real browser completed the challenge. Without them, requests to the search API are blocked.

3. TLS fingerprint and browser properties Akamai checks the TLS handshake fingerprint (JA3 hash) and browser properties like navigator.webdriver, the presence of browser plugins, and WebGL renderer strings. Headless Chrome with default settings fails these checks.

The Session Warming Approach

The solution is to use a real browser session with stealth modifications, then extract the session credentials for use in subsequent HTTP requests.

import { Actor } from 'apify';
import { chromium } from 'playwright-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';

chromium.use(StealthPlugin());

const browser = await chromium.launch({
  headless: true,
  args: ['--no-sandbox', '--disable-setuid-sandbox'],
});

const context = await browser.newContext({
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
  locale: 'en-IN',
  timezoneId: 'Asia/Kolkata',
});

const page = await context.newPage();

// Step 1: Warm the session, visit homepage to trigger Akamai challenge
await page.goto('https://www.naukri.com/', { waitUntil: 'networkidle' });

// Step 2: Human-like behavior to pass behavioral analysis
await page.mouse.move(400, 300);
await page.evaluate(() => window.scrollBy(0, 300));
await new Promise(r => setTimeout(r, 1500));

// Step 3: Intercept nkparam from the first XHR request
let nkparam = null;
page.on('request', (req) => {
  const header = req.headers()['nkparam'];
  if (header) nkparam = header;
});

// Step 4: Navigate to search, this triggers client-side rendering and XHR
await page.goto(
  `https://www.naukri.com/${keyword}-jobs`,
  { waitUntil: 'networkidle' }
);

// Step 5: If nkparam not yet captured, navigate to page 2 to force XHR
if (!nkparam) {
  await page.goto(
    `https://www.naukri.com/${keyword}-jobs?start=20`,
    { waitUntil: 'networkidle' }
  );
}

// Step 6: Extract Akamai cookies
const cookies = await context.cookies();
const akCookies = Object.fromEntries(
  cookies.filter(c => ['ak_bmsc', 'bm_sv', 'nauk_ses_id'].includes(c.name))
    .map(c => [c.name, c.value])
);

Once you have nkparam and the Akamai cookies, you can make HTTP requests directly to Naukri’s search API without the browser overhead.

Extracting Job Data

Naukri’s search API returns structured JSON at https://www.naukri.com/jobapi/v3/search:

const response = await got('https://www.naukri.com/jobapi/v3/search', {
  headers: {
    'nkparam': nkparam,
    'systemcountrycode': 'IN',
    'Cookie': Object.entries(akCookies).map(([k,v]) => `${k}=${v}`).join('; '),
    'User-Agent': 'Mozilla/5.0 ...',
  },
  searchParams: {
    noOfResults: 20,
    urlType: 'search_by_keyword',
    searchType: 'adv',
    keyword: searchKeywords,
    pageNo: pageNumber,
    seoKey: `${slug}-jobs`,
  },
});

const jobs = JSON.parse(response.body).jobDetails;

Each job object contains:

  • Title, company name, and company logo
  • Experience range (min/max years)
  • Salary range (when disclosed)
  • Location(s): normalized city names
  • Work mode (WFH, hybrid, office)
  • Posted date and application count
  • Job description (requires a second request per job)

Proxy Configuration

The Akamai challenge is location-aware. An Indian residential IP is significantly more likely to pass than a US datacenter IP, even with full stealth configuration. Use residential proxies with apifyProxyCountry: "IN" if running on cloud infrastructure.

Using the Managed Scraper

Building and maintaining this session-warming pipeline is complex. The nkparam token format changes occasionally, Akamai updates its fingerprint database, and residential proxy pools need rotation. Our Naukri Jobs scraper handles all of this:

from apify_client import ApifyClient

client = ApifyClient('YOUR_API_TOKEN')
run = client.actor('themineworks/naukri-jobs').call(run_input={
    'searchKeywords': ['python developer'],
    'location': 'Bangalore',
    'experienceMinYears': 2,
    'experienceMaxYears': 5,
    'maxJobs': 100,
    'includeJobDescription': True,
})

for job in client.dataset(run['defaultDatasetId']).iterate_items():
    print(f"{job.get('title')} at {job.get('company')}: {job.get('salary_text')}")

Data Use Cases

HR tech and ATS platforms: Naukri data powers salary benchmarking, candidate sourcing, and market rate analysis for Indian tech roles.

Labor market research: Tracking which skills appear most frequently in job postings, how salary ranges shift with experience, and which cities are hiring in which sectors.

Job aggregators: Building combined job feeds that pull from Naukri alongside LinkedIn, Indeed, and ATS job boards.

Salary intelligence: Indian tech salary data is notoriously opaque. Naukri disclosed salary data is one of the few structured sources available for India-specific compensation benchmarking.

Naukri vs LinkedIn Jobs for India

LinkedIn skews towards tech and enterprise hiring. Naukri covers far more of the mid-market and non-tech companies that never post on LinkedIn, and for blue-collar through middle-management roles in India it is the primary source. Listings also carry more structure: an experience range and skill tags on almost every posting, a work mode label, and a salary range shown openly when the employer discloses one, rather than hidden behind a sign-in. For how Naukri compares with the smaller Indian boards, see Naukri vs Instahyre vs Hirist vs Shine.

Analysing the Output in Python

The managed scraper does the parsing for you. Salary strings like “15-25 Lacs P.A.” arrive as numeric salary_min_lakhs and salary_max_lakhs (both fields are left out when a listing says “Not disclosed”), skills is a list, work_mode is remote, hybrid or work-from-office, and posted_days_ago gives recency. Each run also ends with one _type: "summary" record, which you should skip. Using the same client as above:

import statistics
from collections import Counter

def fetch_jobs(keyword: str, **filters) -> list[dict]:
    run = client.actor('themineworks/naukri-jobs').call(run_input={
        'searchKeywords': [keyword],
        'maxJobs': 200,
        **filters,
    })
    return [
        item for item in client.dataset(run['defaultDatasetId']).iterate_items()
        if item.get('_type') != 'summary'
    ]

def percentile(values: list[float], pct: float) -> float:
    ordered = sorted(values)
    return ordered[min(len(ordered) - 1, int(len(ordered) * pct))]

Salary benchmark by experience band

Let Naukri filter by experience on the server, one band per run, and take the midpoint of each disclosed range:

salary_bands = {}
for low, high in [(0, 2), (2, 5), (5, 8), (8, 12), (12, 20)]:
    jobs = fetch_jobs('data engineer', location='Bangalore',
                      experienceMinYears=low, experienceMaxYears=high)
    mids = [
        (j['salary_min_lakhs'] + j['salary_max_lakhs']) / 2
        for j in jobs
        if j.get('salary_min_lakhs') and j.get('salary_max_lakhs')
    ]
    if mids:
        salary_bands[f'{low}-{high} yrs'] = {
            'sample': len(mids),
            'median_lpa': round(statistics.median(mids), 1),
            'p25_lpa': round(percentile(mids, 0.25), 1),
            'p75_lpa': round(percentile(mids, 0.75), 1),
        }

for band, stats in salary_bands.items():
    print(band, stats)

Only some employers publish pay, and the ones that do are not a random sample, so treat these figures as a floor for the market.

Skills demand and work mode split

jobs = fetch_jobs('data engineer', location='Bangalore')

skills = Counter(s.lower() for j in jobs for s in j.get('skills', []))
top_skills = {s: f'{n / len(jobs):.0%}' for s, n in skills.most_common(15)}
for skill, share in top_skills.items():
    print(f'{skill}: {share}')

modes = Counter(j.get('work_mode') or 'not stated' for j in jobs)
for mode, count in modes.most_common():
    print(f'{mode}: {count} ({count / len(jobs):.0%})')

Leave the workMode filter off when you measure the split. When a listing does not state its work mode, the scraper infers it and falls back to whatever you asked for, which would bias the count.

Building a Custom Naukri Job Alert

Naukri’s own alerts filter on keyword, location, experience and industry. They cannot require several skills at once, drop titles containing “trainee”, or rank what they find. A small script can. Push the hard filters to Naukri’s server, set monitorMode so each scheduled run returns only jobs it has not delivered before, and do the rest in Python:

import re
import smtplib
from email.mime.text import MIMEText

ALERT_INPUT = {
    'searchKeywords': ['senior python developer', 'python backend engineer'],
    'location': 'Bangalore',
    'experienceMinYears': 3,
    'experienceMaxYears': 7,
    'salaryMinLakhs': 15,      # must be one of Naukri's bands: 3, 6, 10, 15, 25, 50, 75, 100
    'postedWithinDays': '1',
    'monitorMode': True,       # later runs return only jobs not delivered before
    'maxJobs': 200,
}

REQUIRED_SKILLS = ['python', 'aws']              # every one must appear
EXCLUDED_TITLE_WORDS = ['intern', 'trainee', 'fresher']
WORK_MODES = {'remote', 'hybrid'}
MIN_SALARY_LPA = 20
PREFERRED_SKILLS = ['fastapi', 'kubernetes', 'llm']
PREFERRED_COMPANIES = ['razorpay', 'flipkart']

def job_text(job: dict) -> str:
    parts = [job.get('title', ''), job.get('description', ''), ' '.join(job.get('skills', []))]
    return ' '.join(parts).lower()

def matches(job: dict) -> bool:
    text = job_text(job)
    if job.get('salary_max_lakhs') and job['salary_max_lakhs'] < MIN_SALARY_LPA:
        return False
    if not all(re.search(rf'\b{re.escape(s)}\b', text) for s in REQUIRED_SKILLS):
        return False
    if any(w in job.get('title', '').lower() for w in EXCLUDED_TITLE_WORDS):
        return False
    return job.get('work_mode') in WORK_MODES

def score(job: dict) -> int:
    text = job_text(job)
    points = sum(1 for s in PREFERRED_SKILLS if s in text)
    points += 3 if any(c in job.get('company', '').lower() for c in PREFERRED_COMPANIES) else 0
    points += 2 if job.get('salary_min_lakhs') else 0
    points += 1 if job.get('posted_days_ago', 30) <= 1 else 0
    return points

def build_digest(jobs: list[dict]) -> str:
    lines = [f'{len(jobs)} new Naukri matches', '']
    for i, job in enumerate(jobs[:15], 1):
        lines += [
            f"{i}. {job.get('title')} at {job.get('company', 'Unknown')}",
            f"   {job.get('salary_text') or 'Not disclosed'} | {job.get('location', '')} | {job.get('work_mode', '')}",
            f"   {job.get('apply_url', '')}",
        ]
    return '\n'.join(lines)

def daily_check():
    run = client.actor('themineworks/naukri-jobs').call(run_input=ALERT_INPUT)
    jobs = [j for j in client.dataset(run['defaultDatasetId']).iterate_items()
            if j.get('_type') != 'summary']
    hits = sorted((j for j in jobs if matches(j)), key=score, reverse=True)
    if not hits:
        return

    msg = MIMEText(build_digest(hits))
    msg['Subject'] = f'Naukri digest: {len(hits)} new matches'
    msg['From'] = 'alerts@yourdomain.com'
    msg['To'] = 'you@example.com'
    with smtplib.SMTP('smtp.gmail.com', 587) as smtp:
        smtp.starttls()
        smtp.login('your_email', 'your_app_password')
        smtp.send_message(msg)

Listings that do not disclose pay pass the salary check; drop them in matches if you would rather not see them. Monitor mode keys on job_id and only remembers runs with the same input, so keep ALERT_INPUT fixed and run daily_check from cron, or save the input as an Apify task and schedule it there. The first run returns everything that matches. After that you only pay for jobs that are new, see pricing.

Tracking Company Hiring Velocity

Naukri search has no company filter, so put the company name in the keyword and then keep only rows where the company field matches. With monitor mode on, every row a weekly run returns is a posting you have not seen, which makes the count a clean new-roles-per-week figure:

import csv
from datetime import date

def new_roles_this_week(company: str, role: str) -> int:
    run = client.actor('themineworks/naukri-jobs').call(run_input={
        'searchKeywords': [f'{role} {company}'],
        'postedWithinDays': '7',
        'monitorMode': True,
        'maxJobs': 200,
    })
    return sum(
        1 for job in client.dataset(run['defaultDatasetId']).iterate_items()
        if company.lower() in (job.get('company') or '').lower()
    )

with open('velocity.csv', 'a', newline='') as f:
    writer = csv.writer(f)
    for company in ['Flipkart', 'Swiggy', 'Razorpay', 'PhonePe']:
        writer.writerow([date.today().isoformat(), company,
                         new_roles_this_week(company, 'data engineer')])

After four to six weeks you have a baseline for each company. A week at double the usual count is worth a closer look, and a run of zeros after steady hiring often means a freeze. The same reading applies to any hiring data, see how to monitor competitor job postings.

Writing the Report with Claude

The numbers above are enough for a spreadsheet. If the audience wants prose, hand the aggregates (not the raw listings) to Claude:

import json
import anthropic

claude = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY

def write_report(role: str, city: str) -> str:
    response = claude.messages.create(
        model='claude-sonnet-4-6',
        max_tokens=1500,
        messages=[{
            'role': 'user',
            'content': f"""Write a short salary and skills brief for {role} roles in {city}, India.

SALARY BY EXPERIENCE BAND (lakhs per annum, disclosed listings only):
{json.dumps(salary_bands, indent=2)}

TOP SKILLS (share of listings):
{json.dumps(top_skills, indent=2)}

Cover pay by band and where the biggest jumps are, skills nearly every listing expects versus
the ones that set candidates apart, and caveats (only some listings disclose pay, and those
lean towards certain kinds of employer). Use LPA throughout. Keep it under 400 words.""",
        }],
    )
    return response.content[0].text

print(write_report('data engineer', 'Bangalore'))

Run the collection weekly and keep each report. The change from one week to the next says more than any single snapshot.

Important Notes

Naukri’s terms of service prohibit automated scraping. Review the terms and ensure your use case complies with applicable laws before proceeding. The techniques described here are for educational purposes and legitimate data analysis.

Frequently Asked Questions

Does Naukri have an official API for accessing job data?

No. Naukri.com does not provide any official API for third-party programmatic access. All structured access to Naukri job listings requires web scraping through their public website, which is protected by Akamai Bot Manager.

What is the nkparam header and why is it required for Naukri API requests?

nkparam is a signed token Naukri’s JavaScript generates client-side for each search request, tied to the current session, timestamp, and request parameters. Requests without a valid nkparam return a 406 error with a “recaptcha required” message. The token is captured by intercepting XHR network traffic after loading the Naukri search page in a real browser.

Why do Indian residential proxies work better than US IPs for Naukri scraping?

Naukri’s Akamai Bot Manager scoring is location-aware. It treats Indian residential IPs more favorably than US datacenter IPs because Naukri is an India-centric service and its expected traffic patterns originate from Indian IP ranges. Even with full stealth configuration, a US datacenter IP scores significantly worse on Akamai’s risk model.

What job data fields does the Naukri API return?

The search API returns job title, company name and logo URL, experience range (min/max years), salary range (when disclosed, normalized to lakh INR), location in normalized city format, work mode (WFH/hybrid/office), posted date, application count, and required skills as structured fields. Full job descriptions require a separate request per job ID.

How often does the nkparam token format change on Naukri?

The nkparam token format changes with Naukri frontend deployments, typically every few months. When it changes, scrapers that hard-code the token extraction logic break until updated. This maintenance overhead is the main reason production teams opt for a managed scraper that monitors and updates automatically.

Is Naukri or LinkedIn Jobs the better source for India job data?

For most of the Indian market, Naukri. It has much deeper coverage of non-tech and mid-market employers, and of roles from blue-collar up to middle management, while LinkedIn leans towards tech and enterprise hiring. Naukri listings also carry experience ranges, skill tags, work mode and, where disclosed, salary as standard fields. Teams tracking senior tech hiring often pull both and join on company name.

How do you turn Naukri salary strings into numbers?

If you call the search API yourself, salary arrives as text such as “15-25 Lacs P.A.” or “Not disclosed”. A regex like re.search(r'(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*(?:Lacs?|Lakhs?|LPA)', text, re.I) pulls out the minimum and maximum in lakhs. Treat “Not disclosed” as missing rather than zero, read “15+ Lacs” as a minimum with no maximum, and convert monthly figures to annual lakhs before comparing. The managed scraper already does this and returns salary_min_lakhs and salary_max_lakhs.

Related Actor

Explore the scraper referenced in this article: inputs, outputs, and pricing, then run it on Apify.

Apify Store

Find a ready-made scraper for your job

Apify has over 80,000 scrapers and automations, ours included. Start free with $5 of platform credit every month.