# FirstStage Jobs API

> Query jobs from FirstStage boards through one API with ?source=firststage — plus how FirstStage's board pages really behave.

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

FirstStage is an applicant tracking system with per-tenant boards at
`<tenant>.firststage.co`. That sentence is close to the limit of what we can tell
you about the company, and this page is going to be short because of it.

What we have is a board, read end to end: one tenant, 107 postings, every field
populated on all 107. What we don't have is the thing the rest of these pages open
with — who buys it, how many employers run on it, how the platform sits in its
market. We could pad this out with plausible sentences about the vendor, and you'd
have no way to tell which ones we'd checked. So: the board behaves as described
below, the rows are queryable with `?source=firststage`, and everything else here
is a gap we're naming rather than filling.

## How FirstStage boards actually work

Two calls per posting, not one. The board lists, the posting carries the body:

```bash
curl "https://<tenant>.firststage.co/jobs"             # the cards
curl "https://<tenant>.firststage.co/jobs/<id>/view"   # JSON-LD, full body
```

The board is a Next.js app, and the useful surprise is that it **server-renders
every card** — so there's no flight-payload parsing to do, no build-id-coupled
format to chase between Next releases. You read the anchors. The detail page
carries a schema.org `JobPosting` block with the description (5,088 characters on
the posting we measured), `datePosted`, `validThrough` and `baseSalary`.

It's an N+1: a posting per request, so a board costs as many calls as it has jobs.
That's the ordinary case in this pool, not a complaint.

### It tells you who the employer is

Worth calling out, because most boards don't. FirstStage's JSON-LD fills
`hiringOrganization.sameAs` — the employer's actual homepage — and the logo, on
every posting we've seen. A board slug is a string; a homepage is a
[company](/docs/objects/company). Most platforms in this pool hand you a token
like `northwind` and leave you to work out who that is, which is a whole pipeline.
Here the board just says.

## What bites you

**The card links are absolute, and the obvious regex finds nothing.** Every href
on the board is a fully-qualified `https://<tenant>.firststage.co/jobs/<id>/view`,
not a relative `/jobs/...`. Reach for `href="/…"` — which is the pattern that works
on nearly every other board — and you match zero cards, on a page that returned a
perfectly healthy `200` with all its jobs rendered in it. The board reads as empty
and nothing anywhere says otherwise.

**One location field can hold several locations.** `jobLocation.address` is a
plain string, and a multi-location posting comma-joins them:
`"London, United Kingdom, Sunnyvale, California USA"` is **two** places. Parse it
as one and you get a city called London and a country called USA.

**US addresses drop the comma before the country.** `"Sunnyvale, California USA"` —
so the trailing segment reads as `California USA`, which is not a country and
doesn't resolve against anything. Between them, those two quirks accounted for
roughly 43% of this source's country fill. Both are fixable once seen: walk the
comma-separated parts and close a location whenever a part *is* a country or *ends
with* one.

## Or use ours

Two calls per posting, hrefs in the one form the obvious regex won't match, and a
location field that comma-joins two places and then drops the comma before the
country. None of it announces itself: the board returns a healthy `200` with every
job rendered in it whether you read it correctly or not.

You get the anchors matched in the shape the board actually emits, the N+1 walked,
the description, `datePosted`, `validThrough` and `baseSalary` read off the
JSON-LD, multi-location strings split back into separate places, country segments
closed on the part that *is* or *ends with* a country, and
`hiringOrganization.sameAs` carried through to a real
[company](/docs/objects/company) rather than a slug you'd have to resolve
yourself.

Query it with `?source=firststage`. How many rows that returns is a number that
moves, so read it — and the rest of the live vocabulary — from the `sources` keys
of [`/v1/meta`](/docs/api-reference/meta), which is the list that's true at the
moment you read it. [Quickstart](/docs/quickstart), then [pricing](/pricing).
