# Errors

Status codes and the two response shapes, so a script can tell a bad request from a lost key.

Every error is JSON with a `detail` field, in one of two shapes. Match on the status code; the messages are written for people and may change.

## A refused request

For `400`, `401`, `403` and `404`, `detail` is one sentence:

```json
{"detail": "This API key is missing the 'tokens:write' scope."}
```

| Status | When |
|---|---|
| `400` | The request was well-formed and refused: a placement of nothing but whitespace, metadata past its limits, the plan's canary cap reached, or the provider would not do what was asked. |
| `401` | No key, an unknown key, or a key that has been deleted. See [Authentication](https://feintsecurity.com/docs/authentication#when-a-request-is-refused). |
| `403` | The key is valid but lacks the scope this endpoint needs. |
| `404` | No canary or event with that id in your organization, or `X-Feint-Client` names no client of yours. Ids are never shared across organizations, so another tenant's id is a `404` too. |
| `429` | Too many requests with this key, or from your address, in a short window. See [Rate limits](#rate-limits). |

## A malformed request

A `422` means the body or query could not be read as the endpoint expects: a missing field, the wrong type, a value out of range. `detail` is a list with one entry per problem, naming the field and what was wrong. The value you sent is not echoed back.

```json
{
  "detail": [
    {
      "loc": ["body", "placement"],
      "msg": "String should have at most 200 characters",
      "type": "string_too_long"
    }
  ]
}
```

`loc` is the path to the field: `body` for JSON fields, `query` for parameters. `type` is a stable, machine-readable code; `msg` is for people.

## Rate limits

Two limits apply, and both answer the same way: `429`, a JSON `detail`, and a `Retry-After` header saying how many seconds to wait.

| Counted | Limit | Where |
|---|---|---|
| Per API key | **100 requests in 10 seconds** | Feint, once the key is verified |
| Per source address, to `/v1` | **100 requests in any 10 seconds** | The edge, before a request reaches Feint |
| Per source address, requests with no valid key | **30 in 10 seconds** | Feint |

A pipeline that mints a decoy and reads back an alert is a handful of requests, so the limits are there for runaway scripts and scanners, not for normal use. The key limit is a burst of that many, then one more every 0.1 seconds; `Retry-After` says when the next request will be taken.

Two things follow from counting by address. Clients that share an egress address, such as hosted CI runners, share that budget, so a burst from one job can slow another on the same runner; the per-key limit is each job's own. And the limits are on requests, not on outcomes: refused requests count the same as successful ones, and a request with a wrong or missing key costs its address one of the 30.

On a `429`, wait for `Retry-After` and resend the same request; nothing was done with it. A client that polls alerts should poll no more than once a minute per organization, which is well inside every limit and matches how often the platforms report use.

Separately, the plan's canary cap is enforced on `POST /v1/tokens` and answers `400`, not `429`: it is a limit on what the organization may hold, not on how fast it asks.

## Everything else

A `5xx` means Feint could not finish. It has been logged; retry with backoff. A failed `POST /v1/tokens` does not leave a canary behind: if the record could not be written, the token is revoked at the provider before the error is returned.

## In short

- `401`: the key is wrong or gone. Fix the key; do not retry as is.
- `403`: the key needs another scope. Do not retry.
- `400`, `404`, `422`: the request needs changing. Do not retry unchanged.
- `429`: too fast, with this key or from this address. Wait for `Retry-After`, then retry unchanged.
- `5xx`: retry with backoff.
