---
title: "Consume the code and receive the first API key"
method: POST
path: "/auth/verify-otp"
tags: ["Bootstrap"]
---

# Consume the code and receive the first API key

`POST /auth/verify-otp`

## Request

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

Verifies the code, creates the account if the address is new, records the terms acceptance, and returns a freshly minted key — the raw token EXACTLY ONCE. `accept_terms` must be true. Requested `scopes` must be within `API_BOOTSTRAP_SCOPES` (default `read,sites:write`); omitting them yields that whole set, and `billing:write` is never obtainable here. Everything that can be rejected without the code is validated first, so a bad field does not burn a valid code. The key is always all-sites and always expires (90 days by default, 365 max).

## Request body

- BootstrapVerifyOTPRequest
  - `email` string, email, required
  - `code` string, required: The code from the email. Single-use; three wrong guesses destroy it.
  - `accept_terms` boolean, required: Must be true. Recorded server-side with the version, channel `api`, IP and User-Agent as the account's acceptance evidence.
  - `terms_version` string: The version being accepted. Defaults to the server's current version.
  - `key_name` string
  - `scopes` string[]: Must be within `API_BOOTSTRAP_SCOPES` (default `read,sites:write`). Omitted = that whole set. `billing:write` is never available here.
  - `expires_in_days` integer

## Response `200`

The account and its first key, including the one-time token.

- BootstrapVerifyOTPResponse
  - `token` string, required: The raw bearer token, returned exactly once.
  - `key` APIKey, required: The non-secret view of a key. The raw token is never stored and never appears here — only in the create/rotate responses.
    - `id` integer, required
    - `name` string, required
    - `prefix` string, required: Non-secret fragment of the token, to identify the key in a list.
    - `website_id` integer, nullable, required: The site the key is locked to; null = all of the owner's sites.
    - `scopes` string, required: Comma-separated scope set, in catalog order.
    - `created_at` string, date-time, required
    - `created_ip` string, nullable
    - `last_used_at` string, date-time, nullable
    - `last_used_ip` string, nullable
    - `expires_at` string, date-time, nullable: null = never expires. Only dashboard-created keys can be non-expiring; keys created over the API always carry an expiry.
  - `user` object, required
    - `id` string, uuid, required
    - `email` string, email, required
  - `created` boolean, required: True when this call registered a new account; false when the address already had one and simply received an additional key.

## Other responses

- `400`: `terms_not_accepted` (accept_terms was not true — no account is created), `invalid_request`, `invalid_scope` (outside the bootstrap allowlist), `invalid_expiry`, or `key_limit_reached` (an existing account already holds 10 active keys).
- `401`: `otp_invalid` — the code is wrong, expired, or already used.
- `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)
