- API reference
- Endpoints
Get the feed
A time-windowed feed of newly indexed postings, built for keeping a local copy in sync.
https://api.hyperjobs.io/v1/jobs/feedEvery job we indexed within a time window, oldest first. This is the endpoint
to poll when you're mirroring the dataset — it's windowed by time rather than
by offset, so unlike /v1/jobs it can't drift
under you as new rows land. See syncing the feed for
the full pattern.
curl -G https://api.hyperjobs.io/v1/jobs/feed \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "since=1h" \
--data-urlencode "limit=1000"Query parameters#
sinceRelative window: 15m, 2h, 24h, 7d. Filters on when we indexed
the job — not its posting date.
m means minutes. There's no week or month unit — write 7d or 30d.
An unparseable value is a 400 naming the parameter, not a silent fallback.
created_gteAbsolute alternative to since: jobs indexed on/after this instant. When
both are sent, created_gte wins. Use it to resume from the exact
created_at of the last row you stored.
limitResults per page, 1 to 1,000. Out-of-range values — including
negative numbers — are a 400.
cursorOpaque keyset cursor from the previous page's next_cursor. Page until it
comes back null.
description_formatInclude the description. Omitted by default — the key is absent, not null. Think hard before turning this on for a 1,000-row feed.
That's the whole vocabulary. The feed takes no filters — no skill, no
country, no source — and any other parameter is a 400. Filter
client-side after you've pulled the window, or use
/v1/jobs with created_gte when you need
a filtered slice rather than a sync.
The feed excludes expired postings and duplicates, exactly like
/v1/jobs — feed rows and index rows are the same population. (A pre-release
draft served expired rows here; that asymmetry never shipped.) Your mirror
doesn't accumulate dead jobs from the feed; to
remove the ones that die after you ingested them, poll
/v1/jobs/expired.
Response#
sinceThe window that was applied. since is validated (a bad value is a
400), so the echo is trustworthy.
countThe number of rows in data — this page, not a total.
dataThe postings, oldest first — ascending by created_at, the moment we
first indexed each job. Same shape as everywhere else — see
the Job object.
next_cursorPass it back as ?cursor= for the next page. null means you've drained
the window.
{
"since": "2h",
"count": 143,
"data": [
{
"id": "greenhouse:4820193",
"title": "Senior Data Engineer",
"company": { "slug": "figma", "name": "Figma", "domain": "figma.com" },
"location": { "remote": false, "countries": ["United States"], "cities": ["San Francisco"] },
"employment_type": ["FULL_TIME"],
"source": "greenhouse",
"posted_at": "2026-07-16T09:41:02.000Z",
"created_at": "2026-07-16T09:48:15.000Z",
"expires_at": null,
"updated_at": "2026-07-16T09:48:15.000Z"
}
// …142 more rows
],
"next_cursor": "eyJzIjoiMjAyNi0wNy0xNiAwOTo0ODoxNSIsImlkIjoiZ3JlZW5ob3VzZTo0ODIwMTkzIn0"
}The window is on index time, not posting date
since=1h means "indexed in the last hour". A job posted three days ago that
we discovered ten minutes ago is in the window — posted_at can be much
older than created_at. That's the property a sync loop wants (you never
miss a late-discovered job), but don't treat the feed as "posted in the last
hour".
Paging#
Ascending order plus a keyset cursor means the window can't overflow: request,
follow next_cursor, repeat until it's null. The draft-era failure mode —
count === limit meaning the oldest rows were silently cut with no way to
reach them — doesn't exist here. Rows land in the order we indexed them, so each
page picks up exactly where the last one ended, and new arrivals during paging
appear at the end rather than shifting anything under you.
Once you've drained a window, store the last row's created_at and use
created_gte on the next poll instead of a fixed since — no overlap, no gap.
There's no Cache-Control header on the feed, despite it being the most
cacheable thing here. Don't expect a CDN or a fetch cache to help — if you
poll on an interval, keep your own copy.
Errors#
| Status | Cause |
|---|---|
400 | Invalid parameter or value — the body names the param and carries a hint. |
401 | Missing, unknown, or revoked key. |
429 | Rate limit or monthly quota exceeded. |
500 | Server error. Retry with backoff. |
Validation is strict: ?since=banana is a 400, not a silent
24-hour default — a typo can no longer make your sync loop quietly re-ingest a
day of jobs every poll.
Next#
Snapshot, poll, expire — the whole mirroring pattern.
Expired jobsThe purge feed: ids to delete from your mirror.
Modified jobsJobs whose tracked fields changed — the upsert side of a sync.
PaginationCursors vs. offsets, and why the index drifts.