OTP
WhatsApp/SMS phone verification, send and verify.
WhatsApp phone verification, send and verify.
await connect.otp.send({ phone: "+919876543210", workspaceId });
const { verified } = await connect.otp.verify({ phone: "+919876543210", code });Use a test-mode key while you build. A key's mode decides whether a send is real: test returns a fixed code in the response and sends nothing, live sends a real WhatsApp message and costs real money. Set it when you create the key - the field only appears once an OTP scope is selected.
Sending is rate-limited per key and per phone number, which is what stops a leaked key becoming somebody else's SMS bill. Treat a 429 as "try later" and surface a wait to the user rather than retrying in a loop; the response carries Retry-After in seconds.
Split scopes on purpose: a frontend-adjacent service that only ever checks codes can hold otp:verify without being able to send.
Endpoints
POST /api/v1/application/otp/send
Send a WhatsApp (or SMS) OTP
Scope otp:send · Rate limit 20 requests / min per key, 5 requests / hour per key+phone
Starts a phone verification via WhatsApp (or SMS, once wired to a provider). A test-mode key (ApiKey.mode) never reaches a real provider and echoes the code back in the response, so you can build and test without spending a real send.
Implementation
1. Prerequisite - create (or edit) an API key scoped to otp:send in the Integration tab.
2. Collect the phone number in E.164 format (e.g. +919876543210).
3. Send the request - an Idempotency-Key header (any string unique per logical attempt) lets you safely retry a timed-out request without double-sending:
curl -X POST https://connect.cosmoops.com/api/v1/application/otp/send \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: otp-attempt-abc123" \
-d '{"phoneNumber":"+919876543210","channel":"whatsapp"}'4. Store verificationId from the response and show the user a code-entry screen. expiresIn (seconds) tells you how long the code is valid for.
5. Move on to /otp/verify (below) once the user submits the code.
Errors
validation_error (400 - bad phone/channel), forbidden (403 - key missing otp:send, unknown, or its allowedIps allowlist doesn't include your egress IP), key_revoked/key_expired (403 - this key is dead, reissue it), idempotency_key_reused/request_in_progress (409), rate_limited (429 - both a per-key and a tighter per-key+phone limit, the real defense against a leaked key spamming one number), delivery_failed (502 - retry).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
phoneNumber | string | yes | |
channel | whatsapp | sms | no |
Responses
| Status | Meaning |
|---|---|
201 | Verification created |
400 | validation_error - bad phoneNumber/channel |
403 | forbidden - key missing otp:send, unknown, or IP not allowlisted. key_revoked / key_expired if the key itself is dead |
409 | idempotency_key_reused / request_in_progress |
429 | rate_limited |
502 | delivery_failed - upstream WhatsApp/SMS provider error |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
verificationId | string | yes | |
status | string | yes | |
channel | whatsapp | sms | yes | |
expiresIn | integer | yes | |
code | string | no |
POST /api/v1/application/otp/verify
Verify an OTP code
Scope otp:verify · Rate limit 30 requests / min per key
Checks a 6-digit code against a pending verificationId from /otp/send. Max 3 attempts per verification before it's exhausted.
Implementation
1. Prerequisite - an API key scoped to otp:verify (can be the same key as otp:send, or a separate one).
2. Collect the code the user typed in.
3. Send the request:
curl -X POST https://connect.cosmoops.com/api/v1/application/otp/verify \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
-H "Content-Type: application/json" \
-d '{"verificationId":"ver_...","code":"482913"}'4. Branch on status in the response: "approved" means the phone number is verified - proceed with whatever the verification unblocks (e.g. account creation, a sensitive action). "expired"/"max_attempts_exceeded" mean the user needs to request a fresh code via /otp/send.
Errors
An unknown, expired, or attempt-exhausted verificationId returns invalid_verification (400, deliberately not 404 - it shouldn't be distinguishable from any other bad request). forbidden (403 - key missing otp:verify), rate_limited (429 - a coarser per-key throttle against hammering many different verificationIds; per-verification attempt exhaustion is handled separately, above).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
verificationId | string | yes | |
code | string | yes |
Responses
| Status | Meaning |
|---|---|
200 | Verification result |
400 | invalid_verification - unknown/expired/exhausted verificationId (deliberately not 404, see the route's doc comment) |
403 | forbidden - key missing otp:verify |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
verificationId | string | yes | |
status | pending | approved | expired | max_attempts_exceeded | yes | |
phoneNumber | string | yes |