Get started / Errors

Errors

Every status code the API returns, what causes it, and what to do about it.

Errors are JSON with an error key. Most carry extra fields with context — validation errors name the offending parameter and tell you what would have been accepted.

JSON
{
  "error": "invalid request",
  "param": "seniority",
  "hint": "allowed: Internship, Entry level, Associate, Mid-Senior level, Director, Management"
}

Status codes#

StatusMeaningRetry?
200Success.
400Unknown parameter, bad value, or malformed search syntax.No — fix the request
401Missing, unknown, or revoked API key.No — fix the key
404Unknown job id, company slug, or endpoint path.No
405Non-GET method. The API is read-only.No
429Per-minute rate limit or monthly quota exceeded.Per-minute: yes, after a pause. Quota: no
500Unhandled server error.Yes, with backoff

That's the complete list.

There is a 400 now#

Strict validation is the feature

In a pre-release draft of this API, malformed parameters were silently coerced or dropped, and a typo'd filter came back as a plausible-looking 200 — the single biggest source of "the API returned the wrong data". That's gone. Every parameter is validated: an unknown parameter, a bad value, an empty value, or a malformed cursor is a 400 that names the parameter and says what's allowed.

?limit=abc is a 400, not a silent limit=50. ?seniority=senior is a 400 listing the enum, not an empty result set. ?time_frame=lastweek is a 400, not results from all time. Bad input can no longer masquerade as an empty result set — if you got a 200, every filter you sent was applied.

400 Bad request#

Two flavors, same shape. For an unknown parameter, a bad or empty value, or a malformed cursor:

JSON
{ "error": "invalid request", "param": "limit", "hint": "minimum is 1" }

For malformed search syntax in title, description, or their _advanced variants — an unbalanced quote, a dangling operator:

JSON
{ "error": "invalid search syntax", "param": "title", "hint": "…" }

param is always the query parameter to fix; hint says why it was rejected — the allowed enum values, the numeric bounds, the expected format. When the hint isn't enough, /openapi.json is the generated spec with every parameter's exact constraints, no key needed.

A 400 will never succeed on retry. Fix the request.

401 Unauthorized#

JSON
{ "error": "invalid or missing api key", "hint": "Authorization: Bearer <key>" }

Returned when the key is absent, unknown, or revoked. The response deliberately doesn't distinguish between those cases.

401 responses carry no X-RateLimit-* headers — those only appear once a request has authenticated. See rate limits.

404 Not found#

Two different shapes. For an unknown resource:

JSON
{ "error": "not found" }

For an unknown endpoint path:

JSON
{
  "error": "unknown endpoint",
  "see": "/v1/jobs · /v1/jobs/count · /v1/jobs/feed · /v1/jobs/expired · /v1/jobs/modified · /v1/jobs/:id · /v1/companies · /v1/companies/:slug · /v1/meta · /v1/health · /openapi.json"
}

A 404 on /v1/jobs/{id} means the posting isn't in the dataset — most often because it expired and was reaped, or because it was folded into another posting as a duplicate. See duplicates.

405 Method not allowed#

JSON
{ "error": "method not allowed — the API is read-only (GET)" }

Any POST, PUT, PATCH, or DELETE. There are no write routes; every endpoint is a GET. (OPTIONS is the one exception — it's answered with a 204 for CORS preflight.)

429 Rate limited#

Two distinct limits produce a 429, with different bodies and different correct responses.

Per-minute#

JSON
{ "error": "rate limit exceeded", "rate_per_min": 300, "retry_after_s": 12 }

rate_per_min is the ceiling you hit; retry_after_s — also sent as a standard Retry-After header — is how many seconds until the sliding 60-second window frees up. Wait that long and retry; if you're hitting this regularly, pace the client with the X-RateLimit-* headers instead — see rate limits.

Monthly quota#

JSON
{
  "error": "monthly quota exceeded",
  "monthly_quota": 50000,
  "resets": "first of next month (UTC)"
}

No Retry-After header — retrying won't help until the quota resets on the first of the next month (UTC) or you upgrade. Distinguish the two flavors by the error string or by the presence of Retry-After.

500 Server error#

JSON
{ "error": "internal", "message": "…" }

Ours, not yours. Retry with exponential backoff and jitter. If it persists, send the message to support — it's the server's own exception text and usually identifies the problem immediately.

Handling errors well#

A client that's correct against this API needs to do three things:

  1. Retry per-minute 429 and 5xx, nothing else

    400, 401, and 404 will never succeed on retry, and neither will a quota 429 — retrying them just burns quota. Tell the two 429s apart by the Retry-After header: present means wait and retry, absent means stop.

  2. Honor Retry-After, back off 5xx with jitter

    JS
    async function request(url, key, attempt = 0) {
      const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
      if (res.ok) return res.json();
     
      const retryAfter = res.headers.get("Retry-After");
      // Retryable: 5xx, or a per-minute 429 (quota 429s send no Retry-After).
      const retryable = res.status >= 500 || (res.status === 429 && retryAfter);
      if (!retryable || attempt >= 5) {
        throw new Error(`${res.status}: ${JSON.stringify(await res.json())}`);
      }
     
      // The server tells you exactly how long the sliding window needs. Jitter
      // keeps a fleet of workers from retrying in lockstep and re-triggering it.
      const base = retryAfter ? Number(retryAfter) * 1_000 : 500 * 2 ** attempt;
      await new Promise((r) => setTimeout(r, base + Math.random() * 500));
      return request(url, key, attempt + 1);
    }
  3. Treat 400 as a bug report, not a transient

    A 400 names the parameter and says what was expected — surface param and hint in your logs verbatim rather than swallowing them into a generic failure. Enum-valued filters like seniority and work_arrangement are exact and case-sensitive; the hint lists the vocabulary, and the enums reference has it all in one place.

Was this page helpful?