# Apple Jobs API

> Query Apple's postings through one API with ?source=apple — plus the CSRF handshake and the format block that silently returns zero without it.

Source: https://hyperjobs.io/ats/apple

Apple is not a platform. It's one employer with a bespoke careers site at
`jobs.apple.com`, and this adapter reaches Apple and nothing else. One
[Workday](/ats/workday) adapter covers every Workday tenant. This one covers a
single company.

That's the trade this page describes. Apple's jobs are worth having, and the only
way to have them is a dedicated adapter for one employer, maintained forever,
breaking whenever Apple changes something.

That adapter exists and its rows are served: query it with `?source=apple`.
[`/v1/meta`](/docs/api-reference/meta) is the authoritative live vocabulary, with
the current count per source.

## How jobs.apple.com actually works

Two steps, and the first one is not optional:

```bash
# 1. Handshake — returns an x-apple-csrf-token header AND session cookies
curl -i "https://jobs.apple.com/api/v1/CSRFToken"

# 2. Search — POST, with the token and the cookies from step 1
curl -X POST "https://jobs.apple.com/api/v1/search" \
  -H "X-Apple-CSRF-Token: <token>" -H "Cookie: <cookies>" \
  -d '{"query":"","filters":{},"page":1,"locale":"en-us","sort":"","format":{…}}'
```

Both halves of step 1 matter. The token comes back as a **response header**, the
cookies come back as `Set-Cookie`, and the search POST needs **both**. Carry the
token without the cookies and the session isn't real.

<Warning title="Omit the `format` block and the API returns zero">
  The search payload must include a `format` object — the date format strings the
  web app happens to send:

  ```json
  "format": { "longDate": "MMMM D, YYYY", "mediumDate": "MMM D, YYYY" }
  ```

  Leave it out and the API does not error. It does not 400. It returns **zero
  results**, with a perfectly well-formed response, and a zero-result search from
  Apple is entirely plausible if your query looks narrow.

  A presentation detail that silently governs whether you get data at all is the
  worst kind of quirk: there's nothing to debug, because nothing went wrong.
</Warning>

### The listing is thin, and pages twenty at a time

`PAGE_SIZE` is **20**. Not 100, not 500 — twenty results per POST, which makes a
full pass a lot of round trips for one company's board.

Per job you get `positionId`, `postingTitle`, `jobSummary`, `locations[]`,
`standardWeeklyHours`, `homeOffice`, `team.teamName`, `reqId` and `postDateInGMT`.

`jobSummary` is a **summary**, and this page should be plain about that: it runs
about a thousand characters, the full body lives in the detail page's React loader,
and the adapter doesn't reach it yet. Descriptions on this source are Apple's own
abstract, not the complete text.

`standardWeeklyHours` is a real number, so employment type comes from hours (30 or
more is full-time) rather than a guess at the title.

### Pay is behind an opt-in N+1

Apple ships compensation in exactly one place: a `postingFooters` "Pay &
Benefits" block on the per-job detail, never in the description. It's a
template-stable sentence:

> The base pay range for this role is between $184,700 and $324,800…

One fixed template means a targeted extraction rather than mining prose. It's on
roughly **77% of US roles** — pay-transparency law doing the work — and
essentially none outside the US. Retail comes through hourly rather than annual.

Being a per-job call makes it an N+1, so it's **off by default** behind
`SCRAPE_APPLE_PAY`: comp for one company at the price of one extra request per
posting.

## What bites you

**The city is in `name`, and appending a country naively gives `India, India`.**
Apple's location objects put the place in `name` and leave `city` blank more often
than not, so reaching for the obvious field makes locations vanish. And some rows
are country-only, where `name` already equals `countryName` — concatenating those
duplicates the place name. The adapter appends a country only when the place
differs from it.

**One adapter, one company, forever.** A CSRF handshake, a cookie session, a magic
`format` block and a twenty-row page size, all maintained to reach one employer.
When Apple changes any of it, nothing else in your pipeline notices and no other
adapter covers the gap. Same deal for [Amazon](/ats/amazon), [Uber](/ats/uber),
[TikTok](/ats/tiktok), [Google](/ats/google), [Microsoft](/ats/microsoft) and
[IBM](/ats/ibm): seven adapters, seven companies, seven independent schedules of
breakage.

## Or use ours

The handshake, the cookie session, the `format` block that silently zeroes the
response, twenty-at-a-time pagination, locations reassembled without inventing
`India, India`, and comp pulled from the one footer that carries it — with Apple's
rows in the same [`Job`](/docs/objects/job) shape as every other
[source](/docs/reference/sources), dedup against the same role on
[LinkedIn](/ats/linkedin), expiry by board diffing, and a single-company adapter's
upkeep being ours rather than yours.

[Sources](/docs/reference/sources) · [Quickstart](/docs/quickstart)
