Endpoints / Get the feed

Get the feed

A time-windowed feed of newly indexed postings, built for keeping a local copy in sync.

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

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

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

Relative 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_gte
ISO 8601 date

Absolute 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.

limit
integerdefault: 200

Results per page, 1 to 1,000. Out-of-range values — including negative numbers — are a 400.

cursor
string

Opaque keyset cursor from the previous page's next_cursor. Page until it comes back null.

description_format
"text" | "html"default: omitted

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

since
string

The window that was applied. since is validated (a bad value is a 400), so the echo is trustworthy.

count
integer

The number of rows in data — this page, not a total.

data
Job[]

The postings, oldest first — ascending by created_at, the moment we first indexed each job. Same shape as everywhere else — see the Job object.

next_cursor
string | null

Pass it back as ?cursor= for the next page. null means you've drained the window.

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

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.

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#

Was this page helpful?