# Errors

> Every error type the API returns, why it happens and how to fix it.

Errors always come back in the same envelope, with a stable `type` you can
branch on. The `docs` field links straight to the relevant heading on this page.

```json
{
  "error": {
    "type": "insufficient_scope",
    "message": "This API key lacks the 'tables:read' scope.",
    "docs": "https://docs.gimlabs.io/api/errors#insufficient_scope"
  }
}
```

## unauthorized

**401.** No key, an unrecognised key, a revoked key, or an expired one. All
four return the same response on purpose, so nobody can use the error to work
out whether a key was ever real.

Check you are sending `Authorization: Bearer <key>` and that the key has not
been revoked in Product OS. A key that "used to work" has almost always been
revoked or has expired.

## forbidden

**403.** The key is valid but the company behind it is not in a state that can
be served - most often the company record has been removed. If a key stops
working with this and nothing changed at your end, talk to us.

## feature_not_available

**403.** Your company is on the Go plan, which does not include the API. Pro
and legacy licences have it. Talk to us about upgrading.

## subscription_inactive

**403.** The key and the plan are both fine, but the company's subscription
is not currently active. Sort the billing out and it starts working again with
no change to the key.

## insufficient_scope

**403.** The key is valid but was not granted this capability. The message
names the scope it wanted.

Scopes are fixed when the key is created and cannot be edited afterwards. Mint
a new key with the right ones ticked, then revoke the old one.

## not_found

**404.** The thing does not exist, has been deleted, belongs to another
company, or is outside this key's project allow-list.

<Callout type="note">
Those last two are intentionally indistinguishable from a genuine 404. A
restricted key must not be able to use error codes to discover what else you
hold. If you are sure the id is right, check the key's project list in Product
OS or via `/me`.
</Callout>

## table_not_found

**404.** The table in the URL is not configured for that project. Codes are
matched case-insensitively, so `GEOL` and `geol` are the same table.

Note the difference between this and an empty result: `table_not_found` means
the project has no such table at all, whereas a `200` with `"data": []` means
the table exists but holds no rows.

## invalid_id

**400.** The id in the URL is not a 24-character hexadecimal id. Usually this
means a project reference like `GL-2201` has been passed where the API wanted
the `id` field from a list response.

## invalid_cursor

**400.** The `?cursor=` value is not a well-formed cursor. Pass back exactly
what `meta.nextCursor` gave you, unchanged.

We refuse rather than ignore it on purpose: silently restarting at page 1 would
let a client that corrupts its cursor page forever without ever noticing.

## rate_limit_exceeded

**429.** You have gone past a limit. `Retry-After` tells you how many seconds
to wait, and it reflects whichever window you actually blew, so a monthly
breach will not tell you to retry in 40 seconds.

See [rate limits](/api/conventions#rate-limits) for the numbers. If you are
hitting them regularly, `updatedSince` usually means you are pulling far less
data than you think you need to.

## Handling errors well

- Branch on `type`, never on `message` or on the HTTP status alone.
- Treat `401` and `403` as permanent. Retrying will not help and just burns your quota.
- Treat `429` as temporary and honour `Retry-After`.
- Treat `5xx` as temporary and retry with exponential backoff.
- Log the whole `error` object. `type` plus `message` is usually enough for us to tell you what went wrong without needing your key.
