# Errors

Every error is `{ "error": { "code", "message", "requestId" } }` with one of **23** stable codes; branch on `code`, show `message` to people, and quote `requestId` to support.

## Shape

```json
{
  "error": {
    "code": "not_found",
    "message": "No app `klavyio` in the catalogue. GET /v1/apps lists them.",
    "requestId": "6f1c2a9e-4b7d-4e2a-9c51-0d3b8e7f2a14"
  }
}
```

- `code` is the contract: codes are added, never renamed. `message` is for people and may change.
- `requestId` is also in the `X-Request-Id` header of every response.
- Some errors add fields: a `402` carries `offers` (see [Credits](https://installgraph.com/docs/credits.md#running-out)), `purchase_not_allowed` adds `settings`, a `405` sends an `Allow` header.
- **Errors never cost credits**, except store lookups past the free-miss allowance (`404` on [`GET /v1/companies/{domain}`](https://installgraph.com/docs/reference/get-company.md), 1 credit, shown in `X-Credits-Charged`).

## Codes

| Status | Code | Meaning | What to do | Retry |
| --- | --- | --- | --- | --- |
| 400 | `invalid_request` | A parameter or body field is missing or malformed. | Fix the request; `message` names the field. Do not retry unchanged. | No |
| 400 | `invalid_cursor` | The `cursor` is not one this endpoint issued (edited, truncated, or from another list). | Pass `pagination.nextCursor` back unchanged, or start again without a cursor. | No |
| 400 | `invalid_email` | Sign-up refused the address (malformed or disposable). | Use a real work email. | No |
| 401 | `missing_api_key` | No key was sent. | Send `Authorization: Bearer sfs_…` or `X-API-Key: sfs_…`. | No |
| 401 | `invalid_api_key` | The key is not one we issued. | Check for truncation or whitespace; keys are `sfs_` plus 32 letters and digits. | No |
| 401 | `api_key_revoked` | The key was revoked. | Make a new key at installgraph.com/account. | No |
| 402 | `insufficient_credits` | The balance cannot cover one row or lookup. Nothing was charged. | Buy credits: `error.offers.pack.buyUrl`, or `POST /v1/billing/purchase` if agent purchases are on. | No |
| 402 | `usage_cap_reached` | The account unlocked 40,000 distinct stores in the last 30 days. | Wait for the window to roll, or Talk to us: `error.offers.talkToUs.url`. | No |
| 402 | `spend_cap_reached` | Purchases would pass $1,000 in 30 days. | Talk to us for more: `error.offers.talkToUs.url`. | No |
| 402 | `paid_plan_required` | Webhooks need an account that has bought a credit pack. | Buy any pack: `error.offers.pack.buyUrl`. Then create the webhook again. | No |
| 402 | `payment_failed` | The saved card was declined. | Ask the account owner to update the card at installgraph.com/account. | No |
| 403 | `insufficient_scope` | The key lacks this endpoint's scope. | Use a key with the scope the endpoint lists (`GET /v1/usage` shows yours). | No |
| 403 | `api_key_suspended` | The key is suspended. | Talk to us, quoting `requestId`. | No |
| 403 | `purchase_not_allowed` | Agent purchases are off for this account, the pack is over the owner's limits, or the key has no account. | Hand `error.offers.pack.buyUrl` to a person; the owner sets limits at `error.settings`. | No |
| 404 | `not_found` | No such endpoint, app, or store (or not scanned yet). | Check the slug or domain. Store 404s are free within the lookup-miss allowance. | No |
| 405 | `method_not_allowed` | Wrong HTTP method for this path. | Use the method in the `Allow` header. | No |
| 409 | `account_exists` | That email already has an account. | Sign in at installgraph.com/login to make more keys. | No |
| 409 | `no_payment_method` | No card is saved, so nothing can be bought off-session. | Buy a pack once at installgraph.com/account to save a card. | No |
| 409 | `webhook_limit_reached` | The account already has 25 active webhooks. | Delete one with `DELETE /v1/webhooks/{id}`, or Talk to us: `error.offers.talkToUs.url`. | No |
| 429 | `rate_limited` | Too many requests this minute. | Wait `Retry-After` seconds, then retry. | Yes |
| 500 | `internal_error` | Something failed on our side. Nothing was charged. | Retry with backoff; quote `requestId` if it persists. | Yes |
| 503 | `not_configured` | A feature this request needs (such as billing) is unavailable. | Retry later. | Yes |
| 503 | `service_unavailable` | The API is briefly unavailable. | Retry with backoff. | Yes |

## Handling them

```js
async function call(url, init = {}, attempt = 0) {
  const res = await fetch(url, {
    ...init,
    headers: { Authorization: `Bearer ${process.env.INSTALLGRAPH_API_KEY}`, ...init.headers },
  });
  if ((res.status === 429 || res.status >= 500) && attempt < 5) {
    const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt;
    await new Promise((r) => setTimeout(r, wait * 1000));
    return call(url, init, attempt + 1);
  }
  const body = await res.json();
  if (!res.ok) throw Object.assign(new Error(body.error.message), body.error);
  return body;
}

try {
  const { data } = await call("https://api.installgraph.com/v1/companies/example.com");
} catch (err) {
  if (err.code === "not_found") { /* not a store we know: skip it */ }
  else if (err.code === "insufficient_credits") { /* show err.offers.pack.buyUrl */ }
  else throw err;
}
```
