Verify a one-time passcode
Consume one verify attempt against a live code.
Attempt budget — once the per-code attempt budget is spent, otp_attempts_exceeded answers 410 Gone, not 429: the state is permanently terminal, waiting never makes the code work again and no Retry-After is emitted for it. Spending the last attempt also settles the code — it does not sit pending at attempts=5/5 forever. Branch on meta.details.code, not on the HTTP status. A 429 from this endpoint only ever means the request rate limit (details.code=rate_limited).
Selector semantics — supply exactly one of otp_id or to. Both selectors resolve the same row state to the same meta.details.code. A by-to lookup skips codes whose TTL has lapsed and, when purpose is supplied, is scoped to that purpose — so verifying a login code cannot burn an attempt on a concurrent password_reset code for the same address.
Code validation — code is trimmed and must be 4–10 ASCII digits. A structurally impossible value is 422 and does NOT consume an attempt.
Request body
{
"otp_id": "00000000-0000-0000-0000-000000000000",
"to": "user@example.com",
"purpose": "string",
"code": "string"
}
Responses
Example response · 200
{
"status": "success",
"http_code": 0,
"payload": {
"verified": true,
"attempts_remaining": 0
},
"meta": []
}
Example response · 410
{
"status": "error",
"http_code": 0,
"payload": [],
"meta": {
"reason": "VALIDATION_ERROR",
"validation_error": [
{
"field": "string",
"message": "string"
}
],
"details": []
}
}
Example response · 422
{
"status": "error",
"http_code": 0,
"payload": [],
"meta": {
"reason": "VALIDATION_ERROR",
"validation_error": [
{
"field": "string",
"message": "string"
}
],
"details": []
}
}
Example response · 429
{
"status": "error",
"http_code": 0,
"payload": [],
"meta": {
"reason": "VALIDATION_ERROR",
"validation_error": [
{
"field": "string",
"message": "string"
}
],
"details": []
}
}