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": {
"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#
| Field | Type | Description |
|---|---|---|
| invalid_request | 400 | The body failed validation (bad enum value, missing field, oversized file…). The message lists each violation. |
| missing_rubric | 400 | Screening 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_candidates | 400 | No applications with a CV are eligible for screening (all withdrawn/rejected, or none have CVs). Only from POST /v1/jobs/{id}/screen. |
| no_cv | 400 | An interview kit was requested for an application with no CV. Only from POST /v1/applications/{id}/interview-kit. |
| invalid_api_key | 401 | Missing Authorization header, malformed key, unknown key, or revoked key — deliberately indistinguishable. |
| insufficient_credits | 402 | The org balance can't cover the operation. The message states exactly how many credits were needed and available. Nothing was charged. |
| job_limit | 403 | Publishing would exceed the plan's active-job limit. Close or pause a published job, or upgrade. Only from POST /v1/jobs/{id}/publish. |
| not_found | 404 | The resource doesn't exist or belongs to another organization — the API doesn't distinguish. Applies to jobs, applications, screenings, kits, and runs. |
| rate_limit_exceeded | 429 | Per-key request budget exhausted for the current 60-second window. Comes with a retry-after header in seconds. |
| internal_error | 500 | Something broke on our side. Safe to retry with backoff; the failure is already logged and alerting. |
Status mapping#
- 2xx — success.
200reads and idempotent replays,201created resources (CV intake, webhook endpoints),202accepted async work (job creation, screening, interview kits). - 4xx — your request needs to change before retrying (except
429, which just needs time, and402/403, which need credits or plan room). - 5xx — our problem; retry with backoff.
402 insufficient_credits#
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#
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#
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#
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
}
}