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

# Rate limits

> Where the throttles are, what they count, and which ones fail closed.

Rate limits sit on the endpoints an attacker can reach **without** a credential —
the ones that guess passwords, enumerate addresses or send mail on somebody else's
behalf.

Counting is a fixed window per client IP, except boot, which is additionally
counted per publishable key.

## Being limited

```json 429 theme={null}
{ "error": { "code": "rate_limited", "message": "too many requests, try again later" } }
```

```
Retry-After: 3600
```

`Retry-After` carries the window in seconds. Wait it out; retrying sooner only
consumes the next window.

## The limits

### Staff authentication

| Endpoint                        | Limit      | Why this number                                                                                      |
| ------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------- |
| `POST /v1/auth/signup`          | 10 / hour  | Account creation is not something a person does in bulk                                              |
| `POST /v1/auth/login`           | 20 / 5 min | Generous for a mistyped password, tight enough that credential stuffing from one address is not free |
| `POST /v1/auth/two-factor`      | 20 / 5 min | A wrong code does not consume the challenge, so this is what bounds guessing                         |
| `POST /v1/auth/forgot-password` | 5 / hour   | Every request mails somebody else's inbox                                                            |
| `POST /v1/auth/reset-password`  | 10 / hour  |                                                                                                      |

### Invitations

| Endpoint                      | Limit     |
| ----------------------------- | --------- |
| `GET /v1/invitations`         | 60 / min  |
| `POST /v1/invitations/accept` | 20 / hour |

### Customer plane

| Endpoint                                          | Limit                                                       |
| ------------------------------------------------- | ----------------------------------------------------------- |
| `POST /v1/boot`                                   | 240 / min per IP, **and** 240 / min per publishable key     |
| `GET /v1/jwks/{publishableKey}`                   | 120 / min — against abuse only; a backend may poll it       |
| `GET /v1/config/{publishableKey}`                 | 120 / min — cacheable; a login screen reads it              |
| `POST /v1/waitlist`                               | 10 / hour — it mails a confirmation                         |
| `POST /v1/contact-auth/magic-link`                | 5 / hour                                                    |
| `POST /v1/contact-auth/magic-link/redeem`         | 20 / hour                                                   |
| `POST /v1/contact-auth/email-code`                | 5 / hour — it mails somebody's inbox                        |
| `POST /v1/contact-auth/email-code/verify`         | 20 / 5 min — and each code carries five attempts of its own |
| `POST /v1/contact-auth/signup`                    | 10 / hour                                                   |
| `POST /v1/contact-auth/login`                     | 20 / 5 min                                                  |
| `POST /v1/contact-auth/verify-email`              | 20 / hour                                                   |
| `POST /v1/contact-auth/resend-verification`       | 5 / hour                                                    |
| `POST /v1/contact-auth/forgot-password`           | 5 / hour                                                    |
| `POST /v1/contact-auth/reset-password`            | 10 / hour                                                   |
| `POST /v1/contact-auth/two-factor`                | 20 / 5 min                                                  |
| `POST /v1/contact-auth/oauth/{provider}/start`    | 30 / hour                                                   |
| `POST /v1/contact-auth/oauth/{provider}/callback` | 30 / hour                                                   |

The per-key limit on boot exists because the publishable key is the tenant boundary
a flood arrives through: bots and landing-page traffic would otherwise mint visitor
rows forever.

### The contact's own surface

| Endpoint                                                | Limit                      |
| ------------------------------------------------------- | -------------------------- |
| `/v1/contact/*` (everything behind a `uk_ct_…` session) | 600 / hour **per session** |

Per session, not per IP. Behind carrier NAT one address is thousands of people,
so an IP bucket on a signed-in surface throttles a crowd for what one of them
did — and the session is the thing actually spending the work. `/v1/contact/token`
is why the number exists at all: every call is a key decrypt and a signature.

<Note>
  The rest of the authenticated surface — the panel's session, the organization
  API key — is not throttled. The credential is the gate, and it is revocable.
</Note>

## Failing open, failing closed

Two postures, chosen per endpoint.

The counters live in a store shared by every instance of the API. When that
store cannot be reached, each instance counts on its own instead — the limits
stay, and they get looser rather than disappearing.

That is deliberate, and it is the one place this platform does not degrade
gracefully. Everything else optional turns off without its dependency; a public
sign-up behind no limiter at all is the whole security posture silently off, at
exactly the moment an attacker is most likely to be the reason.

What you may notice while it lasts: a limit briefly allowing more than the
number above, because several instances are each counting to it. Nothing
answers differently, and there is nothing to handle.

## Staying under them

<Steps>
  <Step title="Retry on 429, with backoff">
    Honour `Retry-After` when it is present; back off exponentially when it is not.
  </Step>

  <Step title="Do not retry a 4xx that is not 429">
    A `400` or a `403` will answer the same way every time.
  </Step>

  <Step title="Debounce boot">
    One boot per page load, not one per component that wants the contact.
  </Step>

  <Step title="Rate-limit your own resend buttons">
    Verification and magic-link resends are 5 an hour. A button with no cooldown
    burns them in seconds and the user sees nothing but a failure.
  </Step>
</Steps>
