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

Two distinct causes share this status. Tell them apart by the error string. Wallet balance too low — also sets X-Credits-Required and X-Credits-Balance:
The endpoint requires a subscription:

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.
There is no 403 for “your plan does not include this endpoint”, and API keys have no scopes or per-endpoint permissions. Plan-gated access returns 402.

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 a contract preview and is not accepting traffic. POST /api/auto-apply/applications is deployment-gated and returns:
This signals availability, not transient capacity — do not retry it. Every Auto Apply response, errors included, carries api_version. At launch, domain errors will use problem+json with a stable code. Planned conflict codes include webhook_secret_not_configured, idempotency_key_reuse, idempotency_key_in_flight, ambiguous_ats, and not_cancelable; 422 adds job_apply_url_missing and unsupported_ats.

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 Python client retries these statuses for you and raises typed exceptions — JoboAuthenticationError, JoboRateLimitError, JoboValidationError, and JoboServerError, all deriving from JoboError. 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.