Skip to main content
Branch on the status code first, then on the stable code in the body when there is one. Retry only the statuses under Retry strategy.

Check the status code, then the envelope

Three layers can reject a request, and each returns a different body shape:
A 200 is not always a success. GET /api/locations/geocode returns succeeded: false for input it cannot resolve, and an unrecognized search filter value returns total: 0. See silent failures.

Status codes

Your wallet is never charged for a failed request. Per-job endpoints (search, feed, company jobs) charge only for delivered jobs; if that charge can’t be covered, the jobs are withheld and you get 402. Flat-rate endpoints such as geocode refund their up-front debit on any 4xx or 5xx. See Billing.

Gateway errors

These are returned before the request reaches an endpoint, as plain JSON, not problem+json.

401 Unauthorized

The response also sets WWW-Authenticate: ApiKey realm="api.jobo.world". On /api/auto-apply/* paths the body also carries api_version.

402 Payment Required

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). quota_remaining is the number of included jobs you have left. See Billing. Three other surfaces return the same 402 with a different envelope: Match on the status and the error or title string, which are stable. The prose in detail and message can change at any time.

403 Forbidden

This key or account may not use the endpoint. A 403 is never a plan check. Keys have no per-endpoint scopes, and Feed works on any plan: without Unlimited, it uses included jobs, then the pay-as-you-go rate. Four situations produce a 403: Playground tokens are short-lived tokens from the public jobo.world playground. Their refusal:
Two related refusals are 404, not 403: a saved feed id owned by another account, and a sandbox key calling an endpoint the sandbox does not serve (code: not_available_in_sandbox).

429 Too Many Requests

The response also sets Retry-After and the X-RateLimit-* headers. group names the rate-limit group you exhausted, and detail names the window, the group, and the limit.
The Auto Apply groups add api_version. The 429 for repeated authentication failures has no code.

Endpoint errors

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

Stable error codes

code is a stable, lowercase string. Branch on it, not on title or detail, which are prose and may be reworded. Not every error carries a code. For requests made with a customer API key:
  • 400, 409, 413, 415, 422, and 429 carry one, except the authentication-failure 429.
  • 403 and 404 carry one only for access gates (company_enrichment_not_enabled, auto_apply_not_enabled), the sandbox (not_available_in_sandbox), and other Auto Apply domain errors.
  • 401 and 402 never do.
Without a code, match on the status and the error or title string. For customer API keys, endpoint errors with status 400, 409, 413, 415, or 422 also carry docs_url, a link to the relevant docs page. The gateway 429 has code but no docs_url.

Unhandled errors

An unexpected server failure returns a minimal envelope. Its detail field is filled only outside production, so never depend on it.
A transient database conflict returns 503 with the same { "message" } envelope and Retry-After: 2. Retry it.

Auto Apply errors

Auto Apply is coming soon; you can build against it today with a sandbox key, no approval needed. Its errors are 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: With a live key, create runs the three 403 checks in the order under Access: agreement, enablement, business review. A sandbox key skips all three. The answers endpoint adds 400 validation_failed (per-field errors, nothing consumed), 409 invalid_state, and 409 stale_correction_round. See The application loop. An application that fails after being queued reports a failure.code instead, such as answers_timeout when a step’s answer window expired.

Silent failures

These three cases return a success status, so they look like empty or unfiltered data rather than errors.
Enum filters are matched literally against the index. An unknown value matches nothing and returns 200 with total: 0, never 400.
Send the canonical values listed under Enums. Jobs where the field is null never match a 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. Send ["full-time", "contract"].Occupation filters behave the same way. Take their values from GET /api/occupations:
  • job_categories and job_subcategories match the taxonomy name, ignoring case (Healthcare or healthcare, not Health care).
  • occupation_ids matches the occupation id. An occupation name such as Registered Nurse returns 200 with total: 0.
  • A subcategory whose name contains a comma (Film, TV & Stage Production) is split by the comma-separated GET parameter and matches nothing. Send it on POST /api/jobs/search.
Unrecognized parameters are ignored, so a typo returns plausible but unfiltered results. ?is_remote=true filters nothing, because the remote filter is work_model=remote.Search reports unknown query parameters and body fields as an unknown_request_fields warning, in the body and the X-Jobo-Warnings header:
Feed requests ignore unknown fields without a warning. Unknown include_facets names are also dropped silently: ?include_facets=work_models (plural typo) returns no facets, not the default set.
Placeholder and unresolvable input is not an error. Check succeeded, not the status code.
Only a missing or whitespace-only location parameter returns 400. See Resolve locations.

Retry strategy

Retry only 429, 500, and 503, plus Auto Apply’s 409 idempotency_key_in_flight. Every other status means the request must change.
1

Honor Retry-After

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

Back off exponentially with jitter

On 500, or a 503 without Retry-After, wait 1 second and double the wait each time, up to 30 seconds. Add ±25% jitter so concurrent clients don’t retry in lockstep.
3

Cap attempts

Stop after 3–5 attempts. A persistent 429 means your sustained rate is too high: slow the pipeline down instead of retrying harder.
4

Restart a cursor instead of retrying it

A cursor error fails on every retry. On 400 invalid_cursor, restart the scan. On 409 feed_cursor_restart_required, start again with {"stable_scan": true, "batch_size": 1000}. On 409 feed_changed, restart the saved-feed scan. See Pagination.
5

Give feed requests enough time

Set a response timeout of at least 120 seconds on feed calls. The feed can work for up to 90 seconds before it returns a retryable 503.
6

Send an idempotency key on writes

A retried write that reuses the key cannot double-charge or duplicate work. See Idempotency.
The official 4.0.0 clients retry 429 and 503 for you, but not 500: catch JoboServerError and apply the backoff above. Other errors raise a typed exception that carries the problem code; see Client libraries.

Getting support

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