Two endpoints, both live-verified against the real hosted API. No dashboard configuration needed. See Email OTP for the conceptual model and real values (TTL, attempts, rate limits) — this page is call shapes only.
POST /api/v1/otp/send
Authorization: Bearer th_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{ "email": "user@example.com" }
Real response, captured live:
{ "success": true }
This queues a real email — confirmed live by receiving one. (The route's own source comment still says "writes it to stdout instead of sending a real email" — that's stale, left over from an early build; real delivery has been verified repeatedly this session.)
POST /api/v1/otp/verify
Authorization: Bearer th_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{ "email": "user@example.com", "code": "040602" }
Real response, captured live — richer than you might expect from a quick skim of the route's own comment, which only mentions token/userId:
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiJ9...",
"userId": "274f4b7d-34cb-4770-941f-71f60681bd8b",
"externalUserId": null,
"user": null,
"companyId": null,
"deviceToken": "eyJhbGciOiJIUzI1NiJ9...",
"isNewUser": true
}
token is the session JWT — use it as Authorization: Bearer <token> on every subsequent user-scoped call (see Account & Session).
Real captured error, a wrong code:
{ "success": false, "error": "Invalid or expired code.", "code": "INVALID_CODE" }
401
Worth knowing precisely: this route's code field collapses every non-fatal failure — wrong code, expired code, and too-many-attempts lockout — into the same "INVALID_CODE" string. Confirmed by reading the route's own logic, not assumed. Only the human-readable error text actually differs between those three cases (e.g. "Too many attempts. Request a new code." for the lockout). If your integration needs to branch behavior on which of these three happened, you can't do it from code alone — read error, or track your own attempt count client-side.
See Error Reference for the full list of possible codes on both endpoints.
token you get back.