Uber Jobs API
Query Uber's postings through one API with ?source=uber — plus the pagination footgun that returns the same 1,000 jobs every call.
Uber is not a platform. It's one employer running a bespoke careers site, and this adapter reaches Uber's postings and nobody else's. Everywhere else, an adapter is leverage: write Greenhouse once and you reach every company that bought Greenhouse. Write this one and you reach Uber.
It exists because Uber is big enough for its jobs to matter on their own, and because a bespoke careers site is invisible to platform discovery — there's no vendor fingerprint to detect. The cost is an adapter per company, maintained forever, breaking on that company's schedule.
How Uber's careers API actually works#
A POST, no real authentication, full job objects with markdown descriptions inline:
curl -X POST "https://www.uber.com/api/loadSearchJobsResults?localeCode=en" \
-H "x-csrf-token: x" \
-d '{"params":{},"page":0,"limit":100}'The x-csrf-token header is required and its value is not checked. A literal x
is accepted. It's a gate that only stops requests that don't know the gate exists.
Descriptions come inline as markdown — not HTML, which is unusual in this pool and means no detail call and no tag soup.
Nest `limit` and `page` under `params` and you get the same jobs forever
limit and page must be top-level keys in the body, sitting beside
params, not inside it.
Put them inside params — which is exactly where they look like they belong —
and the API doesn't reject them. It ignores them, falls back to its default
page of around a thousand results, and returns that same page for every call you
make. Your loop runs, every request succeeds, every response is full of real
jobs, and you collect the same rows over and over into a duplicate storm.
Nothing errors. The only symptom is a job count that climbs while the number of distinct ids doesn't.
totalResults is a 64-bit envelope#
The total isn't a number. It's an object, and the value lives at
totalResults.low:
"totalResults": { "low": 4123, "high": 0, "unsigned": false }That's a 64-bit integer serialisation leaking through the JSON layer. Read
totalResults as a number and you get an object where you expected an integer;
compare it against your offset and the loop's termination condition is nonsense.
The adapter reads .low.
What the adapter reads#
| Field | What it becomes |
|---|---|
id, title | Identity. Rows missing either are dropped |
description | The body, inline, as markdown |
department, team | Two levels of grouping, both structured |
timeType | "Full-Time", "Intern" — employment type and commitment |
creationDate | The posted timestamp, already ISO |
location, allLocations[] | City / region / country objects, joined |
allLocations[] is preferred over location when present, so a posting open in
several cities keeps all of them instead of collapsing to the first.
creationDate arriving as a real ISO string is worth appreciating quietly — it's
the field that goes wrong on Amazon and is absent entirely on
IBM, whose search endpoint publishes no posting date at all.
What bites you#
The duplicate storm is silent. Worth repeating, because it's the failure mode that produces a confident, wrong dataset instead of an error. Every request returns 200 and real data.
There's no salary. Nothing structured in the payload. Whatever comp exists is prose inside the markdown.
A placeholder CSRF token is an invitation to change. x-csrf-token: x working
is a quirk of today's gate, not a contract — precisely the sort of thing that gets
tightened without notice, and when it does, this adapter is what breaks.
One adapter, one company, forever. Same deal as Apple, Amazon, TikTok, Google, Microsoft and IBM: a dedicated adapter, its own schedule of breakage, one employer's jobs at the end of it.
Or use ours#
Top-level pagination keys, .low read out of the envelope, allLocations
preferred over location, markdown kept intact — with Uber's rows in the same
Job shape as every other source,
dedup against the same role on LinkedIn, expiry by board diffing
since the payload declares none, and a single-company adapter's upkeep being ours
rather than yours.
Query it with ?source=uber — the same string that keys
/v1/meta → sources. That endpoint is the live
vocabulary and carries the counts as they move; it's worth reading rather than
trusting a list on a page.
Every source above normalises onto the same schema, with the hiring company already joined onto each posting.
Get an API key