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

# Errors and rate limits

> The error envelope, every status code Sink uses, and the request budgets.

## The error envelope

Every failure comes back in one shape:

```json theme={null}
{
  "detail": "Invalid API key",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
```

`detail` is a string for simple failures and an object or array for structured ones — validation errors, or a partial success that needs to name what worked. `trace_id` is echoed in the **`X-Trace-Id`** response header and is on every response, not just errors. Quote it in a bug report; it is the only thing linking your request to the span that holds the details.

## Status codes

| Status | When                                                                                                         | What to do                                                                     |
| ------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
| `207`  | A secret list where some values could not be decrypted.                                                      | Read `detail.succeeded` and `detail.failed_ids`. This is a partial success.    |
| `400`  | Malformed input a schema cannot catch — a bad workspace id in a header, an empty `secret_ids`.               | Fix the request.                                                               |
| `401`  | No credential, or one that is invalid, revoked or expired.                                                   | Re-authenticate. The message distinguishes an invalid key from an expired one. |
| `402`  | The plan does not allow this — a quota is full, or the feature is not on the tier.                           | Upgrade, or free up the resource. See [Plans and limits](/guides/plans).       |
| `403`  | Authenticated, but not a member of the workspace or team — or not the owner, on an owner-only route.         | Get added, or call as someone who is.                                          |
| `404`  | Not found, or not visible from this scope. A resource borrowed from another workspace reads as absent.       | Check the ids in the path.                                                     |
| `409`  | Conflicts with something that exists — a duplicate key in an environment, a taken email, a team name in use. | Pick another name, or update instead of creating.                              |
| `410`  | A share link whose views are spent, or whose secrets are gone.                                               | Mint a new link.                                                               |
| `422`  | The body failed validation. `detail` is a list of field errors.                                              | Fix the named fields.                                                          |
| `429`  | Rate limited.                                                                                                | Wait out `Retry-After`.                                                        |
| `500`  | Something broke. `trace_id` identifies it.                                                                   | Retry; report it with the trace id if it persists.                             |
| `503`  | A dependency is unavailable — the share store, or a deployment with no published CLI release.                | Retry shortly.                                                                 |

<Note>
  `402` rather than `403` is deliberate for plan limits: it is not a permission you can be granted, it is a tier you can change. The CLI surfaces the two differently for the same reason.
</Note>

## Rate limits

Two layers. A broad ceiling on every request from one caller, plus tighter budgets on the handful of routes where the cost of a *single* request is what matters — credential guessing, outbound email, share redemption, billing calls.

### Who a budget belongs to

| Credential | Bucket                                              |
| ---------- | --------------------------------------------------- |
| API key    | One per key — keys are long-lived and machine-held. |
| Session    | One per user, covering all their browsers together. |
| Neither    | One per client address.                             |

The first two require a credential that survived a signature check, so neither can be minted to escape a bucket. Anonymous callers share an address with everyone behind the same NAT, which is why the unauthenticated budgets are set generously.

### The budgets

| Scope                       | Per minute   | Per hour |
| --------------------------- | ------------ | -------- |
| Everything (backstop)       | 300          | 5,000    |
| Secret reads and writes     | 120          | 2,000    |
| Login                       | 10           | 60       |
| Signup                      | 5            | 20       |
| Token refresh               | 20           | 200      |
| OAuth redirect and callback | 20           | 100      |
| Verification code requests  | 3 per 10 min | 10       |
| Verification code attempts  | 5            | 20       |
| Sensitive account actions   | 10           | 60       |
| Share link page views       | 30           | 300      |
| Share link redemptions      | 10           | 60       |
| Invite token routes         | 20           | 100      |
| Billing calls               | 10           | 100      |
| CLI downloads               | 30           | 200      |

Sensitive account actions covers minting an API key, revoking sessions, updating a profile and deleting an account.

### Headers

Every limited response carries the state of the budget it was checked against:

```http theme={null}
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 43
```

A `429` adds **`Retry-After`** in seconds. Wait it out rather than retrying immediately — the window is a fixed one, so a retry inside it just spends another attempt.

### What is exempt

The payment webhook and `/health` are outside the limiter entirely. The webhook arrives from a small pool of addresses, so a burst of legitimate events looks exactly like one caller flooding the API, and a dropped delivery means a subscription silently stops being billed — the signature check is the right gate there. `/health` is polled on a fixed cadence by load balancers and uptime monitors, where a `429` reads as an outage. Static assets are exempt for the same reason.

<Warning>
  Rate limiting **fails open**. If the counter store is unreachable, requests proceed and the failure is logged. Sink is a secrets manager that deployments read from at boot; an outage in the counter store should cost abuse protection, not lock every caller out of their own configuration.
</Warning>

## Retrying well

* Treat `429` and `503` as retryable, with backoff, honouring `Retry-After`.
* Treat `401`, `402`, `403`, `404`, `409` and `422` as terminal — retrying changes nothing.
* Treat `207` as success with a gap, and log the `failed_ids`.
* Batch. One bulk create costs one request no matter how many keys it carries; a loop over 200 secrets costs 200.
