---
title: "Indeed Jobs API: Get Job Postings as JSON | Perceptlayer"
description: "One GET to /v1/indeed/jobs returns job postings with company, location and apply URL as JSON for 1 credit. Parameters, a real response and monthly cost."
url: https://perceptlayer.com/blog/indeed-jobs-api-json
---

[Blog](https://perceptlayer.com/blog) / [API guides](https://perceptlayer.com/blog/category/api-guides)

# Indeed Jobs API: Get Job Postings as JSON

3 Aug 2026 Updated 26 Oct 2026 Perceptlayer 8 min read

You can get Indeed job postings as JSON with a single `GET /v1/indeed/jobs` call. Pass a `query`, optionally narrow it with `location`, `country` and `job_type`, and send your key in the `X-API-Key` header. Each successful call costs 1 credit, and failed calls cost nothing. There's no partner application: a free key is all you need.

## Key takeaways

-   `GET /v1/indeed/jobs` is the only Indeed endpoint. Only `query` is required.
-   Each job has an `id`, `title`, `company`, a `location` object with coordinates and `is_remote`, `description_html`, `attributes`, `date_published` and `apply_url`.
-   A successful call costs 1 credit, so 1,000 calls cost 1,000 credits. Failed calls return `X-Credits-Used: 0`.
-   The free tier gives 1,000 credits a month after you verify an email address, with no credit card.
-   Tracking one role with a daily call for a month uses 30 credits.

## Is there still an official Indeed jobs API?

Indeed's partner documentation describes its Job Sync API as a way to "create and manage job postings on Indeed and Indeed PLUS", and says Indeed creates an app for your integration once you become a partner ( [Indeed partner docs](https://docs.indeed.com/job-sync-api/integrate-with-job-sync-api), 2026). That is access for posting jobs, and it goes through a partner agreement.

If you're an employer or ATS vendor, that is the route to check first. If you want to read postings for a role in a city, for market research, alerts or an agent's tool, the Perceptlayer [Indeed API](https://perceptlayer.com/apis/indeed) returns postings as JSON from one endpoint, with a free key and no partner application.

## How do you make your first Indeed jobs request?

Get a free key at [/signup](https://perceptlayer.com/signup), then send one GET request. The base URL is `https://api.perceptlayer.com`, the path is `/v1/indeed/jobs`, and the key goes in the `X-API-Key` header. Keys start with `sk_live_`. The call below searches for full-time software engineer roles in Berlin and asks for 20 results.

```bash
curl "https://api.perceptlayer.com/v1/indeed/jobs?query=software+engineer&location=Berlin&country=DE&job_type=full_time&limit=20" \
  -H "X-API-Key: $API_KEY"
```

Values in the query string need URL encoding, so a space becomes `%20` ( [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986)), or `+` in form-style query strings like the one above. If you can't set headers, the key also works as an `api_key` query parameter, but a header keeps it out of logs and URLs. The [authentication docs](https://docs.perceptlayer.com/authentication) cover both.

You can also run the call without writing code. Open the [Indeed API: Jobs page](https://perceptlayer.com/apis/indeed/jobs), fill in the parameters and run it in the playground with your key.

## Which parameters does the Indeed jobs endpoint take?

The endpoint takes one required parameter, `query`, and ten optional ones that narrow by place, job type, order and page. Everything below is documented. Nothing else about defaults or page sizes is, so set `limit` explicitly if you care about how many results you get per call.

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `query` | string | yes | Search text, up to 500 characters |
| `location` | string | no | City or region text, up to 200 characters |
| `country` | string | no | Up to 2 characters, for example `DE` |
| `radius` | integer | no | Search distance around `location` |
| `radius_unit` | string | no | `MILES` or `KILOMETERS` |
| `job_type` | string | no | `full_time`, `part_time`, `contract`, `internship` or `remote` |
| `sort` | string | no | `relevance` or `date` |
| `limit` | integer | no | Number of results to ask for |
| `cursor` | string | no | Pass `data.pagination.next_cursor` from the previous response |
| `hl` | string | no | Up to 2 characters; not documented further |
| `gl` | string | no | Up to 2 characters; not documented further |

Two combinations cover most tracking work. Use `sort=date` to order postings by date rather than by `relevance`. Use `radius` with `radius_unit` to widen a city search to the surrounding area. The full list of calls is on the [Indeed API page](https://perceptlayer.com/apis/indeed), and the [requests docs](https://docs.perceptlayer.com/requests) explain parameter handling across the API.

## What does the response look like?

A successful call returns `success: true`, a `data.jobs` array, a `data.pagination` object with `next_cursor` and `has_more`, and `data.metadata` with `total_results` and a `timestamp`. Each job is a flat object with nested `company` and `location` objects, so you can read the fields without parsing HTML around them. The excerpt below is trimmed from a recorded response to the Berlin request above, with `…` marking where we cut.

```json
{
  "success": true,
  "data": {
    "jobs": [
      {
        "id": "61ac648a11df5cc6",
        "title": "Product Designer",
        "company": {
          "name": "Telli",
          "profile_url": "/cmp/Telli"
        },
        "location": {
          "city": "Berlin",
          "state": "BE",
          "country": "DE",
          "country_name": "Deutschland",
          "formatted": "Berlin",
          "latitude": 52.52437,
          "longitude": 13.41053,
          "is_remote": false
        },
        "description_html": "<div><h1 class=\"jobSectionHeader\"><b>about us</b></h1><p><br>\nEvery 10 minutes, 18 million conversations happen between businesses and consumers around the worl…",
        "attributes": [
          "Full-time",
          "UI design"
        ],
        "date_published": "2025-07-21 05:00:00",
        "apply_url": "https://jobs.ashbyhq.com/telli/156032e9-0e1d-4f7f-9ac7-c828ba49964d?utm_source=l6jqqRwnQ5"
      },
      …
    ],
    "pagination": {
      "next_cursor": "ABQAAYEAFAAAAAAAAAAAAAAAAmYRNdcAMgEBAQcpfSGrk7wi9QBuLZ6BEihQwmDA0xA2nDAGrqtuJIdLnPZKyLE0IcnT2+IETfyRAAA=",
      "has_more": true
    },
    "metadata": {
      "total_results": 20,
      "timestamp": "2026-10-21T13:25:37.233649Z"
    }
  }
}
```

Notice that the query asked for software engineers and the first posting is a product designer. Search relevance is not a filter, so check `title` on your side if you need an exact match.

### What do the fields mean for your pipeline?

| Field | Use it for |
| --- | --- |
| `id` | Identifying each posting, for example as a key in your own store |
| `title` | Matching roles and grouping by seniority |
| `company.name` | Counting who is hiring |
| `location.latitude`, `location.longitude` | Mapping postings |
| `location.is_remote` | Splitting remote from on-site roles |
| `attributes` | Skills and employment type tags, such as `Full-time` |
| `date_published` | Telling new postings from old ones |
| `apply_url` | Linking to the application page |
| `data.pagination.has_more` | Deciding whether another page exists |
| `data.pagination.next_cursor` | Passing as `cursor` to request the next page |
| `data.metadata.total_results` | Seeing how many postings this response holds (20 in the recording) |

`description_html` is HTML, so strip tags or render it safely before you store it as text. In the recorded sample, `date_published` for one posting is from 2025 and others are from 2026, so an old posting can still appear in results. Filter on that field if you only want recent ones. Data is returned as found at the time of the call.

## How do you track a role in Python?

To track a role, call the endpoint on a schedule, sort by date and store the `id` values you've seen. This script is a sketch built from the documented parameters and the recorded response shape. It reads your key from `API_KEY`, requests the newest 20 full-time postings and prints the ones whose `id` isn't in your local file. If `data.pagination.has_more` is true, it can optionally fetch a second page by passing `next_cursor` as `cursor`, which costs one more credit. The script uses `id` as the posting's key, the same field shown in the recorded response.

```python
import json
import os
import requests

URL = "https://api.perceptlayer.com/v1/indeed/jobs"
SEEN_FILE = "seen_ids.json"

params = {
    "query": "software engineer",
    "location": "Berlin",
    "country": "DE",
    "job_type": "full_time",
    "sort": "date",
    "limit": 20,
}

FETCH_SECOND_PAGE = False  # each extra page costs 1 more credit
headers = {"X-API-Key": os.environ["API_KEY"]}

resp = requests.get(URL, params=params, headers=headers, timeout=30)
resp.raise_for_status()

seen = set(json.load(open(SEEN_FILE))) if os.path.exists(SEEN_FILE) else set()
data = resp.json()["data"]
jobs = data["jobs"]

if FETCH_SECOND_PAGE and data["pagination"]["has_more"]:
    page2 = requests.get(
        URL,
        params={**params, "cursor": data["pagination"]["next_cursor"]},
        headers=headers,
        timeout=30,
    )
    page2.raise_for_status()
    jobs += page2.json()["data"]["jobs"]

for job in jobs:
    if job["id"] not in seen:
        print(job["date_published"], job["title"], "-", job["company"]["name"], job["apply_url"])

json.dump(sorted(seen | {j["id"] for j in jobs}), open(SEEN_FILE, "w"))
print("credits used:", resp.headers.get("X-Credits-Used"), "remaining:", resp.headers.get("X-Credits-Remaining"))
```

Every successful response carries `X-Credits-Used` and `X-Credits-Remaining`, so you can stop when the balance gets low. For paging, the response carries `data.pagination.has_more` and `data.pagination.next_cursor`. While `has_more` is true, send `next_cursor` back as the `cursor` parameter to get the next page. Each extra page is another call and another credit. For safe page loops, see [Paginate a Web Data API in Python](https://perceptlayer.com/blog/paginate-web-data-api-python).

## What does it cost to track a role for a month?

Each successful call costs 1 credit, so cost is a call count. One daily call for a month is 30 calls × 1 credit = 30 credits, well inside the free tier's 1,000 monthly credits. Ten roles checked daily is 300 credits. Checking those same ten roles every hour is 10 roles × 24 calls × 30 days = 7,200 credits.

| Plan | Calls | Credits | Cost |
| --- | --- | --- | --- |
| One role, daily | 30 | 30 | Free tier |
| Ten roles, daily | 300 | 300 | Free tier |
| Ten roles, hourly | 7,200 | 7,200 | Within the Starter pack's 10,000 credits |

Credits come in prepaid packs of 10,000, 50,000, 200,000, 500,000, 1,000,000 and 5,000,000 credits. Purchased credits last 12 months, with no subscription and no seats. See [pricing](https://perceptlayer.com/pricing) for what credits cost and the [credits docs](https://docs.perceptlayer.com/credits) for how they're spent.

The service is prepaid. When the balance runs out, calls return `insufficient_credits` (402) and nothing more is debited, and there is no automatic top-up. That makes a runaway loop cost a bounded amount. For agents that call tools on their own, [AI Agent API Cost Control](https://perceptlayer.com/blog/ai-agent-web-data-costs) covers caps in more detail.

## How do you handle errors and rate limits?

Errors come back as an HTTP status with a JSON body of the form `{"error": {"code", "message", "request_id"}}`. A failed call returns `X-Credits-Used: 0`, so retrying an error response costs nothing for the failure itself. One caveat: requests have no idempotency key, so a request whose reply was lost (a client timeout) may have succeeded and been billed. Retrying that one can cost a second credit; [Retry Web API Calls Without Paying Twice](https://perceptlayer.com/blog/retry-web-api-calls-without-paying-twice) covers how to handle it. Branch on `error.code` rather than the message text, and log `request_id` so you can quote it if you need help.

| Code | Status | What to do |
| --- | --- | --- |
| `invalid_api_key` | 401 | Check the key and the `X-API-Key` header |
| `insufficient_credits` | 402 | Add credits, then retry |
| `invalid_parameters` | 422 | Check lengths and allowed values, such as `job_type` |
| `rate_limited` | 429 | Wait the seconds in the `Retry-After` header, then retry |
| `source_error` | 502 | Retry after a short delay |
| `temporarily_unavailable` | 503 | Wait the seconds in the `Retry-After` header, then retry |

The [errors docs](https://docs.perceptlayer.com/errors) list every code, and the [rate limits docs](https://docs.perceptlayer.com/rate-limits) describe the limits. Keep retries to a small, bounded number with a growing delay.

## Can an AI agent search jobs through MCP?

Yes. The MCP server at `https://api.perceptlayer.com/mcp` exposes `search_endpoints`, `get_endpoint` and `call_endpoint`. The first two only look up the catalogue and cost nothing. `call_endpoint` spends the endpoint's credits on success only, and its result reports the credits used and remaining.

In Claude Code, add the server with one command, then ask for jobs in plain language.

```bash
claude mcp add --transport http perceptlayer https://api.perceptlayer.com/mcp --header "Authorization: Bearer $API_KEY"
```

The agent finds `/v1/indeed/jobs` through `search_endpoints`, reads its parameters with `get_endpoint` and runs it with `call_endpoint`. For a full walkthrough, see [Claude MCP Web Search](https://perceptlayer.com/blog/claude-agent-live-search-mcp) and the [MCP docs](https://docs.perceptlayer.com/ai-agents).

## Where does this fit, and what's next?

Use the endpoint when you want postings for a role and place as JSON, with a cost you can calculate in advance. It returns one source's listings, so it won't replace a hiring pipeline or a candidate database. Browse other job sources in the [Jobs APIs](https://perceptlayer.com/verticals/jobs), or see how a similar single-endpoint guide works in [Zillow Listings API](https://perceptlayer.com/blog/zillow-listings-api-json) and the rest of the [API guides](https://perceptlayer.com/blog/category/api-guides).

Your next step: [get a free API key](https://perceptlayer.com/signup) and run the cURL call above with your own `query` and `location`.

## APIs in this post

-   [Indeed **1 credit per call** · 1 endpoint](https://perceptlayer.com/apis/indeed)

## Frequently asked questions

### Does Indeed still have a public jobs API?

Indeed's partner documentation describes an API for creating and managing job postings, available to partners who sign a developer agreement ( [Indeed partner docs](https://docs.indeed.com/job-sync-api/integrate-with-job-sync-api), 2026). That page covers posting jobs, not reading search results. If you just need to read job postings for a role and location, the Perceptlayer Indeed endpoint returns them as JSON with a free key and no partner application.

### How much does each Indeed jobs call cost?

A successful call to `/v1/indeed/jobs` costs 1 credit. Failed calls cost nothing and return `X-Credits-Used: 0`. The free tier gives 1,000 credits every month after email verification, with no credit card. Paid packs start at 10,000 credits, which is 10,000 calls at 1 credit each; see [pricing](https://perceptlayer.com/pricing) for what credits cost.

### Which fields does each job posting include?

Each job has `id`, `title`, `company` (with `name` and `profile_url`), a `location` object with `city`, `country`, coordinates and `is_remote`, `description_html`, `attributes`, `date_published` and `apply_url`. The recorded response in this article shows the exact shape.

### Can I filter for remote or contract roles?

Yes. Set `job_type` to `remote`, `contract`, `full_time`, `part_time` or `internship`. Each job's `location.is_remote` field also tells you whether a posting is remote, so you can filter again on your side. An invalid value returns `invalid_parameters` (422) and costs nothing.

### Is the data complete and up to date?

Data is collected from publicly available sources and returned as found at the time of the call. That means a result reflects what was available when you made the request. Postings can be old, as the `date_published` values in the sample show, so filter on that field if recency matters.

### Should I build an Indeed scraper instead?

An Indeed scraper means maintaining parsers for search and job pages, handling blocks and following Indeed's terms. The jobs endpoint returns company, location and apply URL as named JSON fields for 1 credit per successful call. A scraper only makes sense for data the endpoint doesn't return, so compare in the playground on the Indeed API page first.

jobs indeed job postings api

## More from the blog

1.  [Web data by industry](https://perceptlayer.com/blog/category/web-data) 21 Oct 2026 Perceptlayer 8 min read
    
    ## [Job Posting Data for Labour-Market Research](https://perceptlayer.com/blog/job-posting-data-labour-market-research)
    
    Which Indeed posting fields support trend analysis, where postings mislead versus hires, and a weekly collection plan that costs 3,000 credits a month.
    
2.  [API guides](https://perceptlayer.com/blog/category/api-guides) 15 Sept 2026 Perceptlayer 7 min read
    
    ## [Google Maps Directions API Alternative: Routes as JSON](https://perceptlayer.com/blog/google-maps-directions-api-alternative)
    
    One GET to /v1/google-maps/directions returns a route as JSON for 1 credit. Parameters, a 15,000-route cost example and Google's per-request rates.
    
3.  [API guides](https://perceptlayer.com/blog/category/api-guides) 10 Sept 2026 Perceptlayer 7 min read
    
    ## [Idealista API: Spanish Property Listings as JSON](https://perceptlayer.com/blog/idealista-api-property-listings)
    
    Search Idealista listings in Spain as JSON with one GET call: 1 credit per call, location IDs, price and size filters, and a real recorded response.
