Docs · Reference · Errors

Errors.

Every non-2xx response uses one envelope. The code is a stable machine-readable string — branch on it, not on the message text, which can change.

The envelope#

Error response body
{
  "error": {
    "code": "invalid_request",
    "message": "title: String must contain at least 2 character(s)"
  }
}

Validation failures concatenate every issue into the message as field: problem pairs joined with "; ", so one response tells you everything wrong with the payload.

Error codes#

FieldTypeDescription
invalid_request400The body failed validation (bad enum value, missing field, oversized file…). The message lists each violation.
missing_rubric400Screening was requested on a job with no rubric. Generate the JD (which writes one) or PATCH a rubric first. Only from POST /v1/jobs/{id}/screen.
no_eligible_candidates400No applications with a CV are eligible for screening (all withdrawn/rejected, or none have CVs). Only from POST /v1/jobs/{id}/screen.
no_cv400An interview kit was requested for an application with no CV. Only from POST /v1/applications/{id}/interview-kit.
invalid_api_key401Missing Authorization header, malformed key, unknown key, or revoked key — deliberately indistinguishable.
insufficient_credits402The org balance can't cover the operation. The message states exactly how many credits were needed and available. Nothing was charged.
job_limit403Publishing would exceed the plan's active-job limit. Close or pause a published job, or upgrade. Only from POST /v1/jobs/{id}/publish.
not_found404The resource doesn't exist or belongs to another organization — the API doesn't distinguish. Applies to jobs, applications, screenings, kits, and runs.
rate_limit_exceeded429Per-key request budget exhausted for the current 60-second window. Comes with a retry-after header in seconds.
internal_error500Something broke on our side. Safe to retry with backoff; the failure is already logged and alerting.

Status mapping#

  • 2xx — success. 200 reads and idempotent replays, 201 created resources (CV intake, webhook endpoints), 202 accepted async work (job creation, screening, interview kits).
  • 4xx — your request needs to change before retrying (except 429, which just needs time, and 402/403, which need credits or plan room).
  • 5xx — our problem; retry with backoff.

402 insufficient_credits#

402 example
HTTP/1.1 402 Payment Required
Content-Type: application/json

{
  "error": {
    "code": "insufficient_credits",
    "message": "Insufficient credits: need 87, have 12. Top up or upgrade your plan."
  }
}

The failure happens before anything is created — no half-created screening to clean up. Failed runs that were successfully created refund automatically, so a 402 always reflects your true balance, not stuck holds.

403 job_limit#

403 example
HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": {
    "code": "job_limit",
    "message": "Your free plan allows 1 active job post. Close or pause a job, or upgrade your plan."
  }
}

Counted against currently publishedjobs only — drafts, paused, and closed jobs don't consume slots.

429 rate_limit_exceeded#

429 example
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
x-ratelimit-limit: 60
x-ratelimit-remaining: 0
x-ratelimit-reset: 1783677160
retry-after: 21

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Back off and retry."
  }
}

Wait retry-after seconds (or until x-ratelimit-reset, a unix timestamp) and retry. Details on the window mechanics are in Rate limits.

Handling pattern#

With the SDK
try {
  const screen = await hire.jobs.screen(jobId, { k: 5 });
} catch (err) {
  if (err instanceof HireError) {
    switch (err.code) {
      case "insufficient_credits":    // 402 — top up, then retry
      case "job_limit":               // 403 — close a job or upgrade
      case "rate_limit_exceeded":     // 429 — the SDK already retried twice
      case "invalid_request":         // 400 — fix the payload, don't retry as-is
      case "missing_rubric":          // 400 — set a rubric first
      case "no_eligible_candidates":  // 400 — nothing to screen yet
      case "not_found":               // 404 — check the ID and org
      default:                        // internal_error etc.
    }
    console.error(err.status, err.code, err.message);
  } else {
    throw err; // network failure, abort, bug
  }
}