Skip to main content

Check the status code, then the envelope

The API returns three different error shapes depending on which layer rejected the request. Write your error handling against the status code first, and only then read the body.
A 200 is not always a success. GET /api/locations/geocode returns 200 with succeeded: false for input it cannot resolve, and a search filter with an unrecognised value returns 200 with total: 0 rather than an error. See silent failures.

Status codes

Credits are never charged for a failed request. Per-delivered-unit endpoints (search, feed, company jobs) run a balance precheck and only debit after a successful response is built. Flat-rate endpoints such as geocode debit up front and are automatically refunded on any 4xx or 5xx. See Billing.

Gateway errors

These come from middleware, before your request reaches an endpoint. They are plain JSON, not problem+json.

401 Unauthorized

Also sets WWW-Authenticate: ApiKey realm="api.jobo.world".

402 Payment Required

One condition produces this status on the API: your remaining included jobs plus your wallet balance cannot cover the request. The response also sets X-Credits-Required and X-Credits-Balance:
Credits are the wallet’s wire unit — 1,000 credits = $1.00 — and quota_remaining is your included jobs remaining; see Billing. The same condition surfaces in two other places with the same status but a different envelope. Dashboard Export and Outbound (portal APIs, not this host) return 402 with the error value "insufficient_credits" (lowercase) and a sentence-form message. The MCP server relays a problem+json document whose title is "Insufficient credits" and whose detail includes a top-up link. In every case, match on the status and the error / title string — those values are stable — and treat the prose detail / message as rewordable at any time.

403 Forbidden

Only two situations produce a 403:
  • POST /api/jobs/feed/managed called with anything other than a customer API key — master, marketplace, and sandbox keys are rejected.
  • A sandbox token used against a product it was not minted for.
API keys have no per-endpoint product scopes. Search and Feed can run from included jobs or at the pay-as-you-go rate; the Unlimited plan is a billing option , not a requirement to call Feed.

429 Too Many Requests

Also sets Retry-After and the X-RateLimit-* headers. group names the rate-limit group you exhausted — Default, JobSearch, JobFeed, Geocode, AutoApply, or AutoApplyManagement.

Endpoint errors

Validation and conflict errors follow RFC 7807 with Content-Type: application/problem+json.

Stable error codes

code is a stable, lowercase, machine-readable string. Branch on it rather than on title or detail, which are prose and may be reworded at any time.
code is present on 400, 409, 413, 415, 422, and 429 only, and only for requests authenticated with a customer API key. 401, 402, 403, and 404 carry no code — match on the status and the error string for those. Auto Apply is the exception: it returns code on its domain errors throughout.
Responses carrying a code also carry docs_url, pointing at the relevant page here.

Unhandled errors

An unexpected server-side failure returns a minimal envelope. A detail field is only populated outside production, so never depend on it.

Auto Apply errors

Auto Apply is an invite-only beta. Its errors use problem+json with a stable code, and every Auto Apply response — errors included — carries api_version. POST /api/auto-apply/applications refuses before queueing anything when: The answers endpoint adds 400 validation_failed (per-field errors, nothing consumed), 409 invalid_state, and 409 stale_correction_round — see The application loop. Applications that fail after being queued report a failure.code instead, including answers_timeout when a step’s answer window expired.

Silent failures

Three cases return a successful status with no data and no error. They account for most “the API returned nothing” reports.
Enum filters are matched literally against the index. There is no validation and no 400 — an unknown value matches nothing, and you get 200 with total: 0.
Send the canonical values listed under Enums. Jobs where the field is null are excluded from any filter on that field.On POST /api/jobs/search, array values are not comma-split: ["full-time,contract"] is one literal value and matches nothing. Use ["full-time", "contract"].
Unrecognised query parameters are silently dropped, so a typo returns unfiltered results that look plausible. ?is_remote=true is not a parameter — the filter is work_model=remote — so that request succeeds while filtering nothing.The same applies to include_facets: unknown names are dropped, so ?include_facets=work_models (plural typo) yields no facets rather than the default set.
Placeholder and unresolvable input is not an error. Check the succeeded field, not the status code.
Only a missing or whitespace-only location parameter is a real 400. See Resolve locations.

Retry strategy

Retry only 429, 500, and 503. Everything else is a request you must change.
1

Honour Retry-After

On 429 and 503, wait exactly the number of seconds in the Retry-After header. Never hardcode a delay.
2

Back off exponentially with jitter

For 500, start at 1s and double, capping around 30s. Add ±25% jitter so concurrent clients do not retry in lockstep.
3

Cap attempts

Stop after 3–5 tries. Persistent 429 means your sustained rate is too high — slow the pipeline down rather than retrying harder.
4

Treat a feed 409 as a restart, not a retry

On feed_cursor_restart_required, discard the cursor and begin pagination again with {"stable_scan": true, "batch_size": 1000}. Retrying the same cursor fails forever.
5

Send an idempotency key on writes

So a retry cannot double-charge or duplicate work. See Idempotency.
The official clients do this for you as of 4.0.0: 429 and 503 are retried with bounded backoff honouring Retry-After, and everything else raises a typed exception — JoboAuthenticationError, JoboPermissionError, JoboNotFoundError, JoboRateLimitError, JoboValidationError, JoboCursorRestartRequiredError, and JoboServerError, all deriving from JoboError and carrying the problem code. See Client libraries.

Getting support

Every response carries an x-correlation-id header. Quote it when contacting support@jobo.world — it identifies the exact request in our logs.