---
title: Errors
description: The error envelope, and the codes worth branching on.
---

import Callout from '../../../components/Callout.astro';

Every error has the same shape, including unexpected ones:

```json
{ "error": { "code": "invalid_request", "message": "Country must be a two-letter ISO code." } }
```

Branch on `code`. `message` is written for a person to read and may be
reworded; `code` is part of the contract.

## Codes

| Status | Code | Means |
| --- | --- | --- |
| 400 | `invalid_request` | The request was malformed or a value was rejected. |
| 401 | `unauthorized` | No credential, or one that does not verify. |
| 403 | `forbidden` | Authenticated, but not allowed to do this. |
| 404 | `not_found` | No such thing, or nothing you may see. |
| 409 | `conflict` | Something already exists: a taken username, a duplicate. |
| 429 | `rate_limited` | Too many requests. Back off and retry. |
| 500 | `internal_error` | Ours. Quote the reference if you contact support. |

## What errors deliberately do not tell you

Sign-in and passwordless endpoints answer **identically** whether or not an
address belongs to anybody. A wrong password and an unknown account produce the
same error, and requesting a magic link for an unknown address succeeds.

That is not vagueness for its own sake: a distinguishable response turns any of
those endpoints into a way to test whether a given person has an account with
you.

## Internal errors say nothing about the cause

A `500` carries a generic message and a reference:

```json
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong on our side. Quote reference 4f2a9c11 if you get in touch."
  }
}
```

<Callout type="info">
The real error is in our logs against that reference. Database errors carry the
SQL statement and its parameters, so forwarding them would hand out a map of
the schema. The reference gets support to the exact failure in seconds without
it.
</Callout>