- API reference
- Endpoints
Get expired jobs
The purge feed — ids of jobs that expired within a window, for deleting from your mirror.
https://api.hyperjobs.io/v1/jobs/expiredThe delete side of a sync loop. While /v1/jobs/feed
tells you what to add, this endpoint tells you what to remove: the ids and
expiry timestamps of every job that expired within the window, oldest expiry
first. The payload is deliberately minimal — no titles, no companies, just
enough to run DELETE WHERE id IN (…) against your copy. See
syncing the feed for where this fits.
curl -G https://api.hyperjobs.io/v1/jobs/expired \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "since=24h"Query parameters#
sinceRelative window: jobs that expired within it. 15m, 2h, 24h, 7d
— m is minutes. A bad value is a 400.
expired_gteAbsolute alternative to since: expiries on/after this instant. Use it to
resume from the exact expired_at of the last row you processed.
limitResults per page, 1 to 5,000 — the cap is higher than the other feeds because the payload is just ids and timestamps.
cursorOpaque keyset cursor from the previous page's next_cursor. Page until it
comes back null.
Any other parameter — including description_format, which has nothing to
attach to here — is a 400.
Response#
countRows in data — this page, not a total.
dataExpiries, oldest first — ascending by expired_at. Each row is just
the job id and when it expired.
next_cursorPass it back as ?cursor=; null means the window is drained.
{
"count": 2,
"data": [
{ "id": "greenhouse:4118377", "expired_at": "2026-07-15 18:00:41" },
{ "id": "workable:J7F3K2", "expired_at": "2026-07-15 21:12:09" }
],
"next_cursor": null
}`expired_at` is not ISO 8601
It comes back as YYYY-MM-DD HH:MM:SS — a space, no T, no zone suffix.
The instant is UTC. new Date("2026-07-15 18:00:41") parses it in the
local zone in most runtimes, silently shifting every timestamp — append
Z (and swap the space for T where the parser is strict) before parsing:
const when = new Date(row.expired_at.replace(" ", "T") + "Z");The sync loop role#
A mirror built on the feed alone only ever grows. Postings close, and nothing
in the add-side feed revisits them — so on each poll cycle, after ingesting
/v1/jobs/feed and upserting
/v1/jobs/modified, pull this endpoint
with the same window and delete (or mark dead) every returned id locally.
Ids are stable across all three endpoints, so the delete is a plain key match.
Use a window comfortably wider than your poll interval (or resume with
expired_gte from the last stored expired_at) — the operation is
idempotent, so overlap costs nothing, but a gap leaves zombie postings your
users can still click. The full pattern, including the initial snapshot, is in
syncing the feed.
An expired job may remain retrievable at
/v1/jobs/{id} while it's still in the
pool — with expires_at in the past. That's deliberate: this endpoint is the
signal to purge, not the last moment the record exists.
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. |
Next#
Snapshot, poll, expire — the whole mirroring pattern.
Get the feedThe add side: newly indexed jobs, ascending.
Modified jobsThe update side: jobs whose tracked fields changed.
DeduplicationWhy ids are stable, and what a duplicate fold means for your keys.