# Errors

> The error format, when to retry, and every code the API can return.

An error always has the same shape, whatever the call:

```json
{
  "error": {
    "type": "invalid_request",
    "code": "parameter_invalid",
    "message": "Invalid parameter: limit.",
    "param": "limit",
    "doc_url": "https://docs.rustine.io/api/errors#parameter_invalid"
  }
}
```

- `type`: the category, to know who must act (you, your plan, or Rustine).
- `code`: the exact case. This is what your code should test.
- `message`: a sentence for a human, in the language of the key's owner. Log it, do not test it: its wording may change.
- `param`: with `invalid_request`, the parameter at fault.
- `doc_url`: the line of this page that explains the code.
- `upgrade_url`: with `plan_limit`, where to change plans or buy a pack.

Every response also carries a `Request-Id` header: give it to support if an error keeps coming back.

## When to retry

- `429`: yes, after the number of seconds in the `Retry-After` header.
- `500`, `502` and `503`: yes, a little later. For a write, send the same [`Idempotency-Key`](https://docs.rustine.io/en/idempotency.md) again: the action will not run twice.
- `409 idempotency_in_progress`: yes, in a moment.
- Other `4xx`: no. The same request will get the same answer; fix it first, or change plans for a `402`.

## Every code

| error.code | HTTP | error.type | What it means |
| --- | --- | --- | --- |
| `body_invalid` | 400 | `invalid_request` | The request body is not valid JSON. Check the `Content-Type: application/json` header. |
| `cursor_invalid` | 400 | `invalid_request` | `starting_after` is not the id of an item of this list. |
| `idempotency_key_invalid` | 400 | `invalid_request` | The `Idempotency-Key` header must be 1 to 255 printable characters. |
| `parameter_invalid` | 400 | `invalid_request` | A parameter is missing or invalid; `param` names it. |
| `reason_required` | 400 | `invalid_request` | Marking a false positive or an accepted risk needs a reason, 10 to 500 characters. |
| `target_kind_invalid` | 400 | `invalid_request` | The scan kind does not fit the target: `repository` for a GitHub repository, `quick` or `audit` for a site. |
| `url_invalid` | 400 | `invalid_request` | The address is not the valid URL of a public site. |
| `url_private` | 400 | `invalid_request` | Rustine only scans public sites, never a local or private address. |
| `authentication_required` | 401 | `authentication` | No key in the request. Pass it in the `Authorization: Bearer rst_…` header, never in the address. |
| `invalid_api_key` | 401 | `authentication` | The key or OAuth token is wrong, revoked or expired. Check it is copied in full, or create a new key. |
| `address_limit_reached` | 402 | `plan_limit` | The project already follows as many addresses as your plan allows. |
| `api_plan_required` | 402 | `plan_limit` | The workspace is on the Free plan. The API comes with Pro, or on a 14-day trial with Solo. |
| `api_trial_ended` | 402 | `plan_limit` | Solo's 14-day trial is over. Move to Pro: your keys work again at once. |
| `audit_not_in_plan` | 402 | `plan_limit` | The Free plan runs quick scans only; the full audit comes with Solo and Pro. |
| `audit_quota_exceeded` | 402 | `plan_limit` | This period's full audits are used up, and no pack credit is left. |
| `domain_taken` | 402 | `plan_limit` | On the Free plan, a domain another free account already follows cannot be added or scanned. |
| `fix_quota_exceeded` | 402 | `plan_limit` | This period's fixes are used up, and no pack credit is left. A fix already unlocked stays readable. |
| `plan_limit` | 402 | `plan_limit` | Your plan does not include this action. `upgrade_url` leads to the plan that allows it. |
| `project_limit_reached` | 402 | `plan_limit` | All your plan's projects are in use. Add an extra project or change plans. |
| `project_paused` | 402 | `plan_limit` | The project is paused: your plan covers fewer projects than your workspace has. Its issues stay readable, but nothing new runs. |
| `repositories_not_in_plan` | 402 | `plan_limit` | GitHub repository analyses come with Solo and Pro. |
| `scan_quota_exceeded` | 402 | `plan_limit` | This period's quick scans are used up, and no pack credit is left. |
| `scope_missing` | 403 | `authentication` | The OAuth token lacks the permission this route needs (shown on its page). Reconnect the agent to grant it. An API key has every permission. |
| `fix_locked` | 404 | `not_found` | The fix of this issue is not unlocked yet: `POST /issues/{issue_id}/fix` unlocks it. |
| `resource_not_found` | 404 | `not_found` | Nothing with this id in the key's workspace. An object of another workspace answers the same: its existence is never revealed. |
| `route_not_found` | 404 | `not_found` | This route does not exist. Check the method and the path in the reference. |
| `conflict` | 409 | `conflict` | The request does not fit the object's current state. Read it again, then retry. |
| `github_unavailable` | 409 | `conflict` | GitHub repository analyses are not available right now. |
| `idempotency_conflict` | 409 | `conflict` | This `Idempotency-Key` was already used for another request (method, path or body). A new action takes a new key. |
| `idempotency_in_progress` | 409 | `conflict` | A request with this `Idempotency-Key` is still running. Retry in a moment. |
| `issue_already_fixed` | 409 | `conflict` | The issue is fixed: there is nothing to mark. |
| `proof_required` | 409 | `conflict` | The site must first be proved yours (full audit, fix, marking). Prove it in the app: DNS, file or meta tag. |
| `site_excluded` | 409 | `conflict` | The owner of this site asked not to be scanned. Rustine will not scan it. |
| `target_exists` | 409 | `conflict` | The project already follows this address. |
| `rate_limited` | 429 | `rate_limited` | Too many requests for this key (120 per minute). Wait `Retry-After` seconds. |
| `recheck_limit_reached` | 429 | `rate_limited` | Too many rechecks on this project today (60 per day). Try again tomorrow. |
| `repository_analyses_limit_reached` | 429 | `rate_limited` | The workspace started many repository analyses in the last 24 hours. Try again later. |
| `internal_error` | 500 | `api_error` | An error on our side, reported automatically. Retry later; give the `Request-Id` to support if it comes back. |
| `recheck_failed` | 502 | `api_error` | The recheck could not complete (site unreachable?). Check the site answers, then retry. |
| `platform_busy` | 503 | `rate_limited` | Rustine is not starting new scans for a few minutes. Retry later; nothing was charged. |
| `service_unavailable` | 503 | `api_error` | Rustine is unavailable for a moment. Retry with the same `Idempotency-Key`. |

## Types

Test `type` to decide what to do, `code` to know exactly what happened.

| error.type | What it means |
| --- | --- |
| `invalid_request` | The request itself needs fixing. |
| `authentication` | The key or token is not accepted, or lacks the permission. |
| `plan_limit` | The plan or the quota does not allow it; `upgrade_url` says what to do. |
| `not_found` | The object or the route does not exist for this key. |
| `rate_limited` | Too many requests, or a ceiling reached: wait a little. |
| `conflict` | The action does not fit the current state (unproved site, key already used…). |
| `api_error` | A problem on Rustine's side: retry. |

## Examples

### Node

```js
const res = await fetch(url, { headers })
if (!res.ok) {
  const { error } = await res.json()
  if (error.type === "plan_limit") {
    console.log(`Plan or quota: ${error.upgrade_url}`)
  } else if (res.status === 429 || res.status >= 500) {
    const wait = Number(res.headers.get("Retry-After") ?? 30)
    await new Promise((r) => setTimeout(r, wait * 1000))
    // ...then retry, with the same Idempotency-Key
  } else {
    throw new Error(`${error.code}: ${error.message}`)
  }
}
```

### Response: 402 Payment Required

```json
{
  "error": {
    "type": "plan_limit",
    "code": "audit_quota_exceeded",
    "message": "This period's full audits are used up.",
    "doc_url": "https://docs.rustine.io/api/errors#audit_quota_exceeded",
    "upgrade_url": "https://app.rustine.io/settings?section=offer"
  }
}
```
