---
title: "Bulk export of the full corpus, as a stream of NDJSON."
method: GET
path: "/v1/export"
tags: ["jobs"]
---

# Bulk export of the full corpus, as a stream of NDJSON.

`GET /v1/export`

## Request

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

One JSON object per line, not a JSON array: the stream can be processed as it arrives, and a dropped connection costs one line rather than the whole document. Requires a plan with `bulk_export`.
Rows are ordered by `id`, so an interrupted transfer resumes exactly where it stopped — pass the last id you received as `after`. On a multi-gigabyte export that is not a rare need.
Records are charged in batches as they are written, not at the end, so an export that is abandoned partway is still billed for what it delivered. If the monthly allowance runs out mid-stream, the last line of the body is an object carrying `error: record_quota_exhausted` and the `after` cursor to resume from. A consumer must therefore check the final line rather than assume a clean end of stream.
Every filter from `GET /v1/jobs` applies.
Personal contact details removed: when `include_description=true`, recruiter email addresses and phone numbers found inside the advert body are replaced with `[contact removed — see apply_url]` before the row is written. `apply_url` and every other field are unchanged. `GET /v1/jobs` and `GET /v1/jobs/{id}` apply a narrower rule instead: they remove only email addresses that identify a person (see the `description` field), keep company mailboxes such as `jobs@`, and leave phone numbers unchanged.

## Query parameters

- `after` string, uuid
- `status` 'live' | 'closed' | 'any'
- `include_description` boolean
- `category` string
- `family` string
- `country` string
- `remote` string
- `seniority` string
- `provider` string
- `source_type` string
- `company` string
- `posted_after` string
- `verified_after` string
- `has_salary` 'true' | 'structured' | 'any'
- `has_description` boolean

## Response `200`

A stream of listings, one JSON object per line.

## Other responses

- `401`: Missing, unknown, revoked or expired key. All four are reported identically.
- `402`: The plan's monthly RECORD allowance is spent. Distinct from 429: a 429 means slow down and retry, a 402 means the licence is used up until the allowance resets. The monthly window is the CALENDAR month in UTC: it resets on the 1st of the next month at 00:00 UTC, and the body gives the exact instant. Do not retry before then. Every metered response carries `X-RateLimit-Records-Remaining` and `X-RateLimit-Records-Reset` (the reset instant, Unix seconds), so this is visible long before it happens.
- `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)
