# SmartRecruiters API: Get Jobs as JSON (No Key)

> SmartRecruiters' public Posting API returns any company's jobs as JSON with no API key. Endpoints, pagination, curl and Node examples, and gotchas.

Source: https://cleanjobdata.com/articles/how-to-scrape-smartrecruiters-job-listings
Published: 2026-09-19 · Updated: 2026-11-01

TL;DR: SmartRecruiters has a public postings API keyed by a company identifier. The list call returns summaries, 100 at a time, and a second call per posting returns the job ad, whose text is split across several sections. There's no salary field, and remote or hybrid comes from location flags plus title text.

To get a company's jobs from SmartRecruiters, call `https://api.smartrecruiters.com/v1/companies/{Identifier}/postings` for the list, then one detail request per posting for the job ad. SmartRecruiters is one of the applicant tracking systems that exposes a public JSON API behind its hosted careers pages. If you want to pull a company's listings without parsing HTML, this article covers the endpoints, the fields that matter, and the awkward parts of the data.

That's the DIY route, and it works for a handful of companies you already know. If you'd rather not build and maintain a SmartRecruiters scraper, CleanJobData already ingests ATS platforms and company career sites and serves the jobs normalized through one REST API — see the [Jobs API docs](https://cleanjobdata.com/docs/api/jobs).

## At a Glance

| | |
|---|---|
| List endpoint | `GET https://api.smartrecruiters.com/v1/companies/{identifier}/postings` |
| Detail endpoint | `GET https://api.smartrecruiters.com/v1/companies/{identifier}/postings/{id}` |
| API key | None |
| Pagination | `offset`, 100 postings per page (the maximum) |
| Full description | Detail call per posting, split into sections |
| Salary field | No |
| Remote | Location flags plus title text |

## Public Posting API vs the Official SmartRecruiters API

SmartRecruiters also has authenticated APIs for its customers. They need an API key from the employer's own SmartRecruiters account and cover that company's hiring data, such as candidates and internal job records. You don't need any of that to read published job ads: the Posting API below is public and returns exactly what's on the company's careers page, for any company that uses SmartRecruiters.

## Finding the Company Identifier

Hosted SmartRecruiters careers pages live at `https://jobs.smartrecruiters.com/<Identifier>`, and the last part of that URL is the identifier you use in the API. Copy it exactly as it appears. Some companies embed the board on their own site, so if you don't see the address, look for requests to `api.smartrecruiters.com` or `jobs.smartrecruiters.com` in the network tab. As with every applicant tracking system, there's no public directory of identifiers.

## The List Endpoint

```bash
curl "https://api.smartrecruiters.com/v1/companies/HireVue/postings"
```

No API key is needed. The response is a page of summaries:

```json
{
  "offset": 0,
  "limit": 100,
  "totalFound": 2,
  "content": [
    {
      "id": "744000149874461",
      "name": "FP&A Manager / Senior FP&A Analyst",
      "refNumber": "REF314P",
      "releasedDate": "2026-09-16T13:44:50.650Z",
      "company": { "identifier": "HireVue", "name": "HireVue Inc" },
      "location": {
        "city": "Sandy", "region": "UT", "country": "us",
        "remote": true, "hybrid": false,
        "fullLocation": "Sandy, UT, United States"
      },
      "department": { "label": "Finance" },
      "typeOfEmployment": { "id": "permanent", "label": "Full-time" },
      "experienceLevel": { "id": "associate", "label": "Associate" },
      "ref": "https://api.smartrecruiters.com/v1/companies/HireVue/postings/744000149874461"
    }
  ]
}
```

The list has no job description, only summary fields. An identifier that doesn't exist doesn't return an error: you get `200` with `"totalFound": 0` and an empty `content` array, so check `totalFound` rather than the status code.

## Pagination

The API returns 100 postings per page by default and won't return more. Passing `limit=200` still gave 100 items, so treat 100 as the maximum. Page with `offset`:

```bash
curl "https://api.smartrecruiters.com/v1/companies/ServiceNow/postings?limit=100&offset=100"
```

Compare `offset + content.length` against `totalFound` to know when to stop. A board with 646 postings took seven requests, and the last page had 46 items. Skipping this step is easy to do by accident, because small boards fit in a single page and look complete.

## The Detail Endpoint

The description lives in a second call per posting:

```bash
curl "https://api.smartrecruiters.com/v1/companies/ServiceNow/postings/744000150513709"
```

The detail response contains everything in the summary plus the job ad and some URLs:

- **`jobAd.sections`** is an object with named sections, each with a `title` and an HTML `text`. The ones we saw were `companyDescription`, `jobDescription`, `qualifications` and `additionalInformation`. Some postings leave sections out, and a job needs at least `jobDescription` to read sensibly. Build the full description by joining whichever sections exist, in that order.
- **`postingUrl`** and **`applyUrl`** are the public job page and the apply link; on the postings we saw they were identical.
- **`location`** adds `address` and `postalCode` to the summary's fields.
- **`active`** tells you whether the posting is live.

## Fetching Jobs (Node/TypeScript)

```ts
interface Summary {
  id: string;
  name: string;
  releasedDate: string;
  location?: {
    city?: string; region?: string; country?: string;
    remote?: boolean; hybrid?: boolean; fullLocation?: string;
  };
  typeOfEmployment?: { label?: string };
  experienceLevel?: { label?: string };
  department?: { label?: string };
}

const API = "https://api.smartrecruiters.com/v1/companies";
const SECTION_ORDER = ["companyDescription", "jobDescription", "qualifications", "additionalInformation"];

async function getJson(url: string) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`SmartRecruiters ${res.status} for ${url}`);
  return res.json();
}

export async function listPostings(company: string): Promise<Summary[]> {
  const all: Summary[] = [];
  let offset = 0;
  while (true) {
    const page = await getJson(`${API}/${company}/postings?limit=100&offset=${offset}`);
    all.push(...page.content);
    offset += page.content.length;
    if (page.content.length === 0 || offset >= page.totalFound) break;
  }
  return all;
}

export async function fetchSmartRecruitersJobs(company: string) {
  const summaries = await listPostings(company);
  const jobs = [];
  const BATCH = 10;

  for (let i = 0; i < summaries.length; i += BATCH) {
    const batch = summaries.slice(i, i + BATCH);
    const settled = await Promise.allSettled(
      batch.map((s) => getJson(`${API}/${company}/postings/${s.id}`)),
    );
    settled.forEach((r, idx) => {
      const s = batch[idx];
      const d = r.status === "fulfilled" ? r.value : null; // still keep the job if detail fails
      const sections = d?.jobAd?.sections ?? {};
      const html = SECTION_ORDER
        .filter((k) => sections[k]?.text)
        .map((k) => `<h3>${sections[k].title ?? k}</h3>\n${sections[k].text}`)
        .join("\n");
      jobs.push({
        id: s.id,
        title: s.name,
        published: s.releasedDate,
        location: s.location?.fullLocation ?? [s.location?.city, s.location?.region, s.location?.country].filter(Boolean).join(", "),
        remote: Boolean(s.location?.remote),
        hybrid: Boolean(s.location?.hybrid),
        employmentType: s.typeOfEmployment?.label,
        experienceLevel: s.experienceLevel?.label,
        department: s.department?.label,
        url: d?.postingUrl ?? `https://jobs.smartrecruiters.com/${company}/${s.id}`,
        html,
      });
    });
  }
  return jobs;
}
```

Fetching details in small batches keeps the request rate polite. On a board with hundreds of postings the detail calls are the slow part.

## Fields Worth Knowing

- **`releasedDate`** is an ISO timestamp, the best "published" date.
- **`typeOfEmployment`** and **`experienceLevel`** are objects with an `id` and a readable `label`, for example `permanent` / "Full-time" and `associate` / "Associate".
- **`location.country`** is a lowercase ISO code such as `us`. **`fullLocation`** is a ready-made string like "San Diego, California, United States", so prefer it when it's present.
- **`department`**, **`function`** and **`industry`** are labeled objects that you can use for filtering.
- **`customField`** is an array of company-specific labels (for example "Exempt / Non Exempt"). It varies by employer.
- **`creator`** holds the recruiter's name, so decide whether you want to store it at all.

## Gotchas

**Remote and hybrid are two booleans.** `location.remote` and `location.hybrid` are real fields, but employers don't always set them. Also check the title for "remote" or "hybrid".

**There's no salary field.** The detail responses we looked at had no compensation field. If a company discloses pay, it's inside the ad text.

**Sections vary.** Some postings lack a qualifications or additional-information section, and `companyDescription` is boilerplate that repeats on every job. Decide whether you want it in your description.

**Edits are easy to miss.** The list doesn't tell you when a posting was last changed. If you skip detail calls for IDs you already stored, you won't see edits to those jobs.

**Old postings can stay open.** Compare `releasedDate` with today's date before you treat a job as fresh.

**No webhooks and no published limits we can point to.** It's pull-only. Keep detail concurrency modest, retry on 429 and 5xx, and compare posting IDs between runs to detect closures.

## When This Is Enough

For one company's jobs, the list-plus-detail pattern is simple and free. Add pagination and you can cover even very large boards.

## When It Isn't

Across many companies and platforms you end up writing and maintaining a different collector for each. See [Greenhouse](https://cleanjobdata.com/articles/how-to-scrape-greenhouse-job-listings) and [Lever](https://cleanjobdata.com/articles/how-to-scrape-lever-job-listings) for how different the shapes are, and [how we normalize jobs from many ATS platforms](https://cleanjobdata.com/articles/how-we-normalize-ats-job-data) for the general approach.

CleanJobData reads SmartRecruiters boards alongside other platforms and returns one schema, with locations resolved to cities, states and countries, a remote type, an employment type and an experience level. There's no salary to extract from SmartRecruiters itself, so that field can be empty. Try it in the [Playground](https://cleanjobdata.com/playground) or see the [API Documentation](https://cleanjobdata.com/docs).

## Skip the scraper: get this data from the CleanJobData API

If you'd rather not maintain a SmartRecruiters collector, you can query normalized jobs from ATS platforms and company career sites with a single request. The response has structured city/state/country locations, parsed salary when the employer publishes one, a remote type, and company metadata, and you page through results with the cursor in `pagination.next_page`.

```bash
curl "https://api.cleanjobdata.com/jobs?title=engineer&domain=jobs.smartrecruiters.com&limit=20" \
  -H "Authorization: Bearer $CLEANJOBDATA_API_KEY"
```

The `domain` filter is an exact match on the host of each job's `application_url`, so it only returns jobs whose apply link resolves to `jobs.smartrecruiters.com`; use `company_website_url` or `employer_id` to scope to one employer instead. Coverage depends on which employers are in the dataset, so check that the companies you care about appear before you rely on it.

- [Jobs API reference](https://cleanjobdata.com/docs/api/jobs) for every filter and response field
- [Playground](https://cleanjobdata.com/playground) to run queries without writing code
- [Pricing](https://cleanjobdata.com/pricing) — the Trial is free with no card required
