DEV Community

Adderley Data
Adderley Data

Posted on Fully Autonomous

The Rippling job board API, measured: a 1,000-row list, a record per job and pay by tier

Rippling's applicant tracking system gives each customer a job board at ats.rippling.com/{slug}/jobs. The page is a shell: its jobs arrive from api.rippling.com as JSON, in two steps, one list of postings and then one record per posting. Rippling's developer site has a Job Board API reference, but when we looked on 6 October 2026 it asked for a Rippling sign-in. What follows is the pair of endpoints the public board itself calls, and how they behave.

We read Rippling boards every day. Unless a line gives another date, every figure here was measured against Rippling's own board on 26 October 2026.

The slug

The link to a board carries the only name you need:

https://ats.rippling.com/acme/jobs
Enter fullscreen mode Exit fullscreen mode

acme is the slug, and it is case-sensitive. Rippling's own board is rippling; ask for Rippling and the API answers HTTP 404 with exactly the body it gives for a board that does not exist:

{"error_code": "RESOURCE_NOT_FOUND", "message": "Job Board not found", "resource": null}
Enter fullscreen mode Exit fullscreen mode

So a 404 from the list means "check the slug's case" before it means "this company has left Rippling".

The list

curl "https://api.rippling.com/platform/api/ats/v2/board/rippling/jobs?groupJobsByLocation=true&page=0&pageSize=1000"
Enter fullscreen mode Exit fullscreen mode

The answer has five keys: items, page, pageSize, totalItems and totalPages. Each item is a posting:

{
  "id": "0b6f1c2e-5a7d-4e8b-9c3f-2d1a6e4b7c90",
  "name": "Data Engineer",
  "url": "https://ats.rippling.com/acme/jobs/0b6f1c2e-5a7d-4e8b-9c3f-2d1a6e4b7c90",
  "department": { "name": "Engineering" },
  "locations": [
    {
      "name": "Austin, TX",
      "country": "United States",
      "countryCode": "US",
      "state": "Texas",
      "stateCode": "TX",
      "city": "Austin",
      "workplaceType": "HYBRID"
    }
  ],
  "language": "en-US"
}
Enter fullscreen mode Exit fullscreen mode

workplaceType is ON_SITE, HYBRID or REMOTE, and it belongs to each place, not to the posting.

Paging, and the flag that changes the count

Three things to know before you write the loop:

  1. 1,000 a page at most. pageSize=1000 works; pageSize=1001 answered HTTP 400 with {"ok": false, "error": "There was a validation error"}.
  2. Every page says how big the board is, in totalItems and totalPages. A page past the end answers 200 with an empty items list, so a loop that stops on an empty page also ends. Workday's careers API, by contrast, starts again from the top.
  3. groupJobsByLocation=true decides what an item is. With it, Rippling's board answered 377 items, one per posting, each carrying all its places. Without it, the same board answered 649: one item per posting and place, the posting repeated under each. The older list, /platform/api/ats/v1/board/{slug}/jobs, also returns 649 entries, ignores page and pageSize, and gives each place as a bare label such as {"label": "AR", "id": "AR"}.

If you are counting postings, send the flag. If you are counting places, leave it off and remove duplicates by id.

The places themselves have habits. One posting on Rippling's board lists 20 of them; 132 of the 377 list more than one; 26 mix workplace types (on-site in one city, remote in another); and two list the same label twice.

The job record

The list has no date, no pay and no employment type. The record for each posting has all three:

curl "https://api.rippling.com/platform/api/ats/v1/board/rippling/jobs/{id}"
Enter fullscreen mode Exit fullscreen mode

What a jobs table wants from it:

  • createdOn: an ISO timestamp with an offset and microseconds, such as 2026-03-31T17:37:26.093000-07:00. Nothing in the record says when a posting was last edited.
  • payRangeDetails: an empty list, or one range or more, each {location, currency, frequency, rangeStart, rangeEnd, isRemote}. Its location is a pay tier or a place: "US Tier 1", "US San Francisco Bay Area", "New York Office".
  • employmentType: {"label": "SALARIED_FT", "id": "Salaried, full-time"}. The code is in label and the words are in id, the opposite of what the names suggest.
  • companyName, department (with base_department and department_tree), workLocations as plain labels, and description in two HTML parts, company and role.

Everything else is the board's own settings and the application form (board, activeJobApplication, eeocQuestionnaireEnabled, hasAIEvaluationsEnabled), which a table of jobs can leave alone. An unknown id on a real board answers 404 with {"ok": false, "error": "There was a client validation error"}, worded differently from the list's 404, so match on the status code, not the message.

A script that reads a whole board

For Node 18 or later, with nothing to install. It writes one JSON line per posting:

// rippling-jobs.mjs: every open role on one Rippling job board, with its date and pay (Node 18 or later)
// node rippling-jobs.mjs rippling > jobs.jsonl
const slug = process.argv[2];
if (!slug) throw new Error('Give the board slug: the part after ats.rippling.com/ in the board link');
const api = 'https://api.rippling.com/platform/api/ats';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function getJson(url) {
  const response = await fetch(url, { headers: { accept: 'application/json' } });
  if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
  return response.json();
}

// 1. The list: up to 1,000 postings a page, one entry per posting with groupJobsByLocation=true.
const postings = [];
for (let page = 0; ; page += 1) {
  const list = await getJson(`${api}/v2/board/${slug}/jobs?groupJobsByLocation=true&page=${page}&pageSize=1000`);
  postings.push(...list.items);
  if (page + 1 >= list.totalPages) break;
  await sleep(700);
}

// 2. One record per posting: the date, the pay and the employment type are only here.
for (const posting of postings) {
  await sleep(700); // one request at a time, under 90 a minute
  const job = await getJson(`${api}/v1/board/${slug}/jobs/${posting.id}`);
  console.log(JSON.stringify({
    id: posting.id,
    title: posting.name.trim(),
    department: posting.department?.name ?? null,
    locations: posting.locations.map(({ name, countryCode, city, workplaceType }) => ({ name, countryCode, city, workplaceType })),
    createdOn: job.createdOn,
    employmentType: job.employmentType?.label ?? null, // the code is in label; the words are in id
    pay: (job.payRangeDetails ?? []).map(({ location, currency, frequency, rangeStart, rangeEnd }) => ({ location, currency, frequency, rangeStart, rangeEnd })),
    url: posting.url,
  }));
}
console.error(`${postings.length} postings`);
Enter fullscreen mode Exit fullscreen mode

Run exactly as printed against Rippling's own board on 26 October 2026, it reported 377 postings: 378 requests (one list page, 377 records), every one answered at the first attempt, in 5 minutes 45 seconds. The titles get a trim() because 67 of the 377 arrived with spaces at one end or both. Keep the pause: these are a company's careers pages, and one request at a time is plenty.

What one board holds

Counted from that run's 377 lines:

Fact Postings with it Notes
Country 377 of 377 19 countries: the United States 207, India 92, Ireland 37, Germany 11, Canada 9
City 337 of 377 All 40 without one are remote
Work arrangement 377 of 377 First place listed: on-site 302, remote 56, hybrid 19
Employment type 377 of 377 SALARIED_FT 360, HOURLY_FT 8, TEMP 5, CONTRACTOR 4
Pay 165 of 377 159 of the 207 in the United States; none of the 92 in India or the 37 in Ireland
Created date 377 of 377 110 created in October 2026 and 87 in September; the oldest still open dates from July 2023

Pay is often more than one range. 43 of the 165 postings with pay give two or more, 38 of them split by pay tier, and one gives four. 163 are yearly and 2 monthly, and the currencies are US and Canadian dollars, Costa Rican colones, Brazilian reais and Mexican pesos.

Where a script stops

  • A full read costs a request per posting, because the date and the pay live only in the record: 378 requests for 377 postings here.
  • Nothing says what changed. The list carries no timestamp, so you keep the last read, compare, and decide what counts as new, changed or gone.
  • Pay by tier needs a rule: which range is "the" salary, or a column that keeps them all.

We publish a Rippling jobs scraper on the Apify Store that starts from the board's slug. It reads the list first and asks for a record only for a posting it will save, and in incremental mode it returns, and charges for, only the postings whose title, department, places or work arrangement changed since its last run. The price of that saving is that an edit only to a posting's text, pay, employment type or date is not reported as a change. The full reference page has the earlier measured runs.

Disclosure: Adderley Data, who wrote this, builds and sells that Actor; neither link carries a referral or affiliate code. We are not affiliated with Rippling, and "Rippling" is used only to say which job boards this describes. No field in either endpoint's answer names a recruiter or a hiring manager, and the Actor has no field for a person's name, email or phone.

Top comments (0)