---
title: "Email a one-time code"
method: POST
path: "/auth/send-otp"
tags: ["Bootstrap"]
---

# Email a one-time code

`POST /auth/send-otp`

## Request

- Base URL: `https://statable.com/api/v1`
- URL: `https://statable.com/api/v1/auth/send-otp`
- Auth: none required
- TypeScript: <https://skmtc.dev/statable/apis/statable-stats-api/docs/auth/send-otp/post.ts> (a function with typed inputs; its response is checked against the schema)

Sends a 6-digit code to the address, valid for a short window and usable once. Takes no credential — this is the entry point for a client that has none. Rate limited per email (2/minute), per IP (5/hour) and per IP across the whole bootstrap branch (20/hour). The response is identical whether or not the address already has an account, so it cannot be used to test for one.

## Request body

- BootstrapSendOTPRequest
  - `email` string, email, required: The address that will receive the code and own the account.

## Response `200`

The code was sent.

- BootstrapSendOTPResponse
  - `sent` boolean, required: Always true. Says nothing about whether the account existed.
  - `email` string, email, required: The normalized (lower-cased, trimmed) address.

## Other responses

- `400`: `invalid_request` — a valid email is required. `email_undeliverable` — the address can never receive mail (a reserved domain such as example.com, or a non-ASCII mailbox no provider accepts), so no code is generated and nothing is sent. `domain_not_allowed` — the address sits in a zone we do not serve.
- `404`: `write_disabled` — the write surface is off (`API_WRITE_ENABLED=false`). 404 rather than 403 so a disabled surface is not advertised.
- `429`: Hourly account (default 2000/h) or per-key (default 600/h) limit hit (`rate_limited`). Retry after the `Retry-After` seconds. The X-RateLimit-* headers report the window that tripped (account on an account limit).
- `500`: Unexpected server error (`internal`). Report it with the `request_id` from the body or the X-Request-ID header: it is what lets the exact log entry be found, and without it a 500 can only be matched by guessing at a time window.

---

[API](https://skmtc.dev/statable/apis/statable-stats-api.md) - [All operations](https://skmtc.dev/statable/apis/statable-stats-api/llms.txt) - [OpenAPI document](https://skmtc.dev/statable/apis/statable-stats-api/revisions/352b6d47c186?raw)
