Docs / Shared / Pagination & errors
Pagination & errors
Cursor pagination and the same error codes on every dataset.
Paying nothing for a page that has not changed
Every 200 carries an ETag. Send it back as If-None-Match and an unchanged page answers 304 Not Modified with no body.
curl -sS -D- -o/dev/null https://api.softon.dev/v1/jobs?limit=100 \ -H "Authorization: Bearer $SOFTON_KEY" ETag: "9f2c1ab4e0d73851c6ba204fd1e88a37" # ...later, asking whether it moved curl -sS -D- -o/dev/null https://api.softon.dev/v1/jobs?limit=100 \ -H "Authorization: Bearer $SOFTON_KEY" \ -H 'If-None-Match: "9f2c1ab4e0d73851c6ba204fd1e88a37"' HTTP/2 304
A 304 costs no credit. That is not a special case — a credit is one successful request, and a 304 is not a 2xx. The per-second rate limit still applies, so this is not a way to poll without limit; it is a way to stop paying for bytes you already hold.
The tag is computed over the data, not the envelope, so the request_id that differs on every response does not change it. It is strong and exact: same rows, same tag.
Cache-Control is private, max-age=0, must-revalidate. Private matters — a response is scoped to your key, so a shared cache holding one account's page and serving it to another would leak data. Do not put these behind a shared proxy.
One honest steer on when to use it: if you are tailing new and changed rows, updated_after is cheaper than any amount of revalidation and is what you should reach for. Conditional requests are for the other job — periodically verifying a full set, which is what reconciliation and re-checking removed records need.
Pagination
Collections are cursor-paginated. Follow meta.next until it is null; cursors are opaque, so do not construct them yourself, and keep every other filter identical between pages — a cursor is only valid for the query and sort order that produced it. Offset pagination is deliberately not offered: it skips and repeats rows when the underlying data moves between pages, and every dataset here is rewritten by a scraper on its own schedule — see Data freshness.
# page 1 GET https://api.softon.dev/v1/jobs?limit=25 # page 2 — pass the cursor back, keep the other filters identical GET https://api.softon.dev/v1/jobs?limit=25&cursor=eyJvIjoyfQ
limit defaults to 25 and is capped at 100. Asking for more is a 400, not a silent truncation, so a client that expected 500 rows finds out rather than concluding the dataset is small.
Errors
The same codes on every dataset. The HTTP status is authoritative; error.code is the stable string to branch on. An unrecognised query parameter is a 400 naming the parameter and listing the accepted ones — a mistyped filter must not look like an empty result set.
| Status | Code | Meaning | Retry? |
|---|---|---|---|
| 400 | invalid_request | A parameter is missing, malformed or out of range. The message names it. | No — fix the request. The message names the parameter. |
| 401 | unauthorized | The key is missing, malformed or revoked. | No — check the Authorization header. |
| 403 | forbidden | The key is valid but your plan does not include this dataset. | No — upgrade the plan or drop the dataset. |
| 404 | not_found | No resource with that id on this dataset. | No. |
| 410 | gone | This record was held and has since been removed at the request of the employer or the individual concerned. It will not come back. If you hold a copy, stop serving it. Returned only by the single-resource paths, and only for an id this platform actually had -- an id that never existed here is still a 404. | Never — and delete any copy you hold. |
| 429 | rate_limited | Plan rate exceeded. Retry-After gives the wait in seconds. | Yes — after Retry-After. |
| 429 | quota_exceeded | The account's monthly request allowance is spent. It resets on the 1st; X-Quota-Limit and X-Quota-Used say where you are. Retrying sooner cannot help, which is why this is a different code from rate_limited. | Not this month — the allowance resets on the 1st. Upgrade, or wait. |
A 5xx is always safe to retry with backoff. A 4xx never is: the request itself needs changing, and replaying it just spends quota. A 403 means the key is real but does not include the dataset it asked for — a key can be scoped to a subset of its plan's datasets when it is issued, and the error body names both what was asked and what the key can read.