> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vort-sourcing.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors, rate limits and retries

> How the API reports failures, how much you may call it, and which calls are safe to repeat.

RFC 2119 keywords apply.

## Error envelope

Every non-2xx response has this envelope:

```json theme={null}
{
  "error": "requirements_rejected",
  "message": "One or more requirements cannot be used as written.",
  "rejected": [ ... ],
  "warnings": [ ... ]
}
```

<ResponseField name="error" type="string" required>
  Stable machine code. Branch on this, never on `message`.
</ResponseField>

<ResponseField name="message" type="string" required>
  A human sentence in the request language. You MAY show it to the recruiter where this documentation says so; otherwise log it.
</ResponseField>

<ResponseField name="(extra fields)" type="varies">
  Depend on the code (listed below).
</ResponseField>

<Info>
  A malformed JSON body or a request that does not match the schema returns a `4xx` whose `error` code is not fixed in v1; handle it by HTTP status and log `message`. Any `error` code not listed on this page MUST be handled by its HTTP status.
</Info>

## Error codes

<AccordionGroup>
  <Accordion title="Cross-cutting errors (any endpoint)" icon="globe" defaultOpen>
    | HTTP | `error` | Meaning | What to do |
    | - | - | - | - |
    | 401 | `invalid_partner_key` | `x-partner-key` missing or not recognized. | Check your key configuration. Do not retry. |
    | 401 | `invalid_org_token` | `x-vort-org` missing or not recognized. | The customer must reconnect: show your "Connect Vort" screen again. |
    | 403 | `partner_disabled` | Your partner account is disabled. | Contact Vort. Do not retry. |
    | 403 | `organization_disabled` | This customer's connection is disabled. | Tell the customer admin their Vort connection is paused; contact Vort. |
    | 429 | `rate_limited` | Too many requests. Carries `Retry-After`. | Wait `Retry-After` seconds, then retry. |
    | 503 | `rate_limit_unavailable` | Vort could not verify your request quota, so the request was not run. | Retry with backoff (nothing was executed). |
    | 5xx | (any) | Vort-side failure. | Retry with backoff where the endpoint is safe to retry (see [Idempotency](#idempotency-and-retries)). |
  </Accordion>

  <Accordion title="Error codes by endpoint" icon="list">
    | Endpoint | HTTP | `error` | Extra fields |
    | - | - | - | - |
    | `POST /activations/redeem` | 404 | `code_not_found` | — |
    | | 409 | `code_already_redeemed` | `redeemed_at` |
    | | 410 | `code_expired` | — |
    | | 409 | `external_id_taken` | — |
    | `POST /users` | 422 | `invalid_user` | — |
    | `POST /jobs` | 422 | `job_rejected` | `rejected[]` (JobIssue) |
    | `POST /runs` | 422 | `requirements_rejected` | `rejected[]`, `warnings[]` (RequirementIssue) |
    | | 404 | `job_not_found` | — |
    | | 404 | `user_not_found` | — |
    | `GET /runs/{run_id}` | 404 | `run_not_found` | — |
    | `POST .../reveal` | 402 | `insufficient_credits` | `balance`, `required` |
    | | 404 | `candidate_not_in_run` | — |
    | | 404 | `run_not_found` | — |
    | `POST .../decisions` | 422 | `unknown_reject_reason` | `allowed[]` |
    | | 422 | `invalid_decision` | — |
    | | 404 | `run_not_found` | — |

    JobIssue and RequirementIssue are described under [Objects](/integration-flow#objects).

    `acting_user_external_id` on reveal and decisions must name a user you registered with `POST /users`; an unknown user is rejected with `404 user_not_found`, as on `POST /runs`.

    A run that exists but belongs to another customer organization returns `404 run_not_found`, never `403`. Runs are only visible to the organization whose token started them.
  </Accordion>
</AccordionGroup>

## Rate limits

* Limits are **per partner** (all your customers together), not per customer.
* Defaults: **120 requests per minute** and **20,000 requests per day**. Vort may adjust them per partner by agreement.
* Over the limit you get `429 rate_limited` with `Retry-After: <seconds>`. You MUST wait at least that long before retrying. Do not retry in a tight loop.
* Every request counts, including failed ones and requests still in progress (a burst of concurrent requests is counted as it arrives).
* If Vort cannot verify your quota at that moment, the request is refused with `503 rate_limit_unavailable` and nothing is executed; retry with backoff.

<Tip>
  Budgeting guidance: polling one run every 5 seconds costs 12 requests per minute. Poll from your server once per run (not once per open browser tab), fan the result out to your clients, stop polling when the run reaches a terminal status, and use the `run_completed` / `run_failed` [webhooks](/webhooks) instead of polling runs no recruiter is looking at.
</Tip>

## Idempotency and retries

v1 has no `Idempotency-Key` header. Each endpoint's behavior on a repeated call:

| Endpoint | Repeat with the same input | Safe to retry after a timeout? |
| - | - | - |
| `POST /activations/redeem` | Single-use. A second redeem of the same code returns `409 code_already_redeemed` with `redeemed_at`; the org token is **not** returned again. | **No.** If the first response was lost, ask Vort for a new activation code. |
| `POST /users` | Upsert on `external_id` within the organization. Returns the same `user_id`, `created: false`. | Yes. |
| `POST /jobs` | Upsert on `external_id` within the organization. A repeat call **updates** `title`, `description_text` and `location_text` and returns the same `job_id` with `created: false, updated: true`. Send it whenever the job changes in your ATS, so the next run uses the current description. Runs already started keep the description they started with. | Yes. |
| `POST /runs` | **Not idempotent.** Every accepted call starts a new run. | **No.** Do not retry automatically on a timeout or 5xx. Show the recruiter an error with a manual "Try again" action. A `422`/`404` created nothing and may be retried after fixing the input. |
| `GET /runs/{run_id}` | Read-only. | Yes. |
| `POST .../reveal` | A candidate already revealed for this organization MUST NOT be charged again; the repeat returns the contact details with `credits_charged: 0`. | Yes. |
| `POST .../decisions` | Each `(run_id, candidate_id, decision, occurred_at)` is stored once. A retry resends the same `occurred_at` and is counted in `duplicates`. A later action on the same candidate, including a note added afterwards, carries a new `occurred_at` and is kept as a new history entry. `notices_shown` codes are stored once per run. | Yes, when `occurred_at` is reused on retry. |

<Warning>
  Retry policy for retry-safe calls: exponential backoff starting at 1 s, max 3 attempts, honoring `Retry-After` on `429`. Never retry `401`, `403`, `404`, `409`, `410`, `422`.
</Warning>

## Testing error handling in the sandbox

Send `x-vort-sandbox-simulate: rate_limited`, `rate_limit_unavailable`, `internal_error` (or `insufficient_credits`) on a sandbox request to force the matching error response (`429` with `Retry-After: 30`, `503`, `500`, `402`). See [Simulating errors](/sandbox#simulating-errors). The sandbox also has a few error codes of its own, such as `422 sandbox_limit` and `429 sandbox_run_quota`; they are listed under [Sandbox-only codes and headers](/sandbox#sandbox-only-codes-and-headers).
