Endpoints / Expired jobs

Get expired jobs

The purge feed — ids of jobs that expired within a window, for deleting from your mirror.

GET/v1/jobs/expired
GEThttps://api.hyperjobs.io/v1/jobs/expired

The 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#

since
<number><m|h|d>default: 24h

Relative window: jobs that expired within it. 15m, 2h, 24h, 7dm is minutes. A bad value is a 400.

expired_gte
ISO 8601 date

Absolute alternative to since: expiries on/after this instant. Use it to resume from the exact expired_at of the last row you processed.

limit
integerdefault: 1000

Results per page, 1 to 5,000 — the cap is higher than the other feeds because the payload is just ids and timestamps.

cursor
string

Opaque 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#

count
integer

Rows in data — this page, not a total.

data
{ id, expired_at }[]

Expiries, oldest first — ascending by expired_at. Each row is just the job id and when it expired.

next_cursor
string | null

Pass it back as ?cursor=; null means the window is drained.

Response
{
  "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:

JS
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#

StatusCause
400Invalid parameter or value — the body names the param and carries a hint.
401Missing, unknown, or revoked key.
429Rate limit or monthly quota exceeded.
500Server error. Retry with backoff.

Next#

Was this page helpful?