---
title: "Ids of roles that have come off the ledger"
method: GET
path: "/v1/jobs/expired"
---

# Ids of roles that have come off the ledger

`GET /v1/jobs/expired`

## Request

- Base URL: `https://api.jobopportunitiesapi.org`
- URL: `https://api.jobopportunitiesapi.org/v1/jobs/expired`
- Auth: HTTP bearer

The cheap half of /v1/jobs/closed. Returns id, closed_at and closed_reason only — enough to mark rows stale in your own database without transferring the jobs themselves. About 89% smaller than the same page from /v1/jobs/closed.

Closure dates: JOA records as the closure date the day its daily verification confirmed that an advert had gone. Before late September 2026 that verification ran in large catch-up batches, so closure dates recorded before then show when JOA confirmed a closure, not when the employer closed the role; daily verification applies from late September 2026.
Keyset paged exactly like /v1/changes: pass `since` on the first call and the returned `next_since` from then on. An empty page echoes your cursor back, so the loop keeps working once you are current.

Needs the delta feed (Growth and up; 403 `plan_upgrade_required` otherwise). Not metered: it returns ids, not job rows, charges no records and sends no `X-RateLimit-Records-*` headers.

## Query parameters

- `since` string, required
- `limit` integer

## Response `200`

A page of closures.

- object
  - `count` integer
  - `data` object[]
    - `closed_at` string, date-time: JOA records as the closure date the day its daily verification confirmed that an advert had gone. Before late September 2026 that verification ran in large catch-up batches, so closure dates recorded before then show when JOA confirmed a closure, not when the employer closed the role; daily verification applies from late September 2026.
    - `closed_reason` 'expired_upstream' | 'not_seen' | 'employer_closed' | 'deadline_passed'
    - `id` string, uuid
  - `next_since` string

## Other responses

- `400` (unresolved $ref)
- `401`: Missing, unknown, revoked or expired key. All four are reported identically.
- `403`: This endpoint is not included in your plan. `X-JOA-Required-Feature` names the missing entitlement — `delta_feed` or `bulk_export`. See https://jobopportunitiesapi.org/api for the tiers that include it.
- `422`: A parameter we will not guess at: an unknown source_type or provider, an unparseable timestamp, a malformed cursor. Note the deliberate exceptions, which are clamps rather than errors: `limit` is capped at the plan maximum (a non-numeric value falls back to the default), and comma-separated lists are truncated to the per-parameter maximum shown on each. Unknown query parameters are ignored.
- `429`: Over your plan's per-minute or per-day limit. `Retry-After` says how long (seconds to the next minute boundary, or to UTC midnight on a daily breach). The body says exactly when the limit resets (`resets_at`, `resets_at_text`) and, when a higher plan exists, links a same-key upgrade (`upgrade_url`).

---

[API](https://skmtc.dev/jobopportunitiesapi/apis/job-opportunities-api.md) · [All operations](https://skmtc.dev/jobopportunitiesapi/apis/job-opportunities-api/llms.txt) · [OpenAPI document](https://skmtc.dev/jobopportunitiesapi/apis/job-opportunities-api/revisions/b5ef9709a0c9?raw)
