message.comDevelopers

Verify voice quickstart Disabled

Verify a phone number by placing a call that reads a one-time passcode aloud, digit by digit. Voice is the accessibility and fallback channel for phone verification: it reaches landlines, works when SMS is filtered or undelivered, and helps users who prefer a spoken code. The API contract below is committed and stable; the disabled-for-sending state is called out where it applies.

Setup time: 1 hour$0.03 per successful verification

Voice is currently disabled for sending. POST /v1/verify with channel: "voice" returns 422 with { "error": "channel_not_available", "channel": "voice" }. Every request and response shape on this page is the committed contract; the check call and all read endpoints work identically across channels.

When to choose voice

Choose voice when SMS is not reaching a user, when the number is a landline that cannot receive text, or when you want a fallback path after an SMS attempt goes unanswered. It is also the more accessible option for users who cannot read a text. For the widest reach and lowest friction on mobile numbers, start with SMS; use email when the identifier is an email address rather than a phone.

Prerequisites

  • A message.com workspace with Verify activated and a positive credit balance. See Get started with Verify.
  • A Verify Service with voice in its enabledChannels.
  • An API key with the verify scope (mk_live_...). See Authentication.
  • The destination country in the service's allowedCountries allowlist: voice is a phone channel and subject to the same allowlist as SMS.
  • Voice OTP delivery runs on message.com's own callout stack and is provisioned per workspace, which is the 1-hour setup step. See setup times.

Full verification walk

Three steps every time: start a verification, the user answers a call and hears the code, then check the code they typed. The shapes are identical to SMS; only the delivery channel differs.

1. Send the code

Call POST /v1/verify with the phone number as to and "voice" as the channel. E.164 format is preferred.

cURL (macOS/Linux)
curl -X POST https://api.message.com/v1/verify \
  -H 'Authorization: Bearer mk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "to": "+14155550142",
    "channel": "voice"
  }'
cURL (Windows)
curl -X POST https://api.message.com/v1/verify ^
  -H "Authorization: Bearer mk_live_..." ^
  -H "Content-Type: application/json" ^
  -d "{\"to\": \"+14155550142\", \"channel\": \"voice\"}"
Node
const start = await fetch('https://api.message.com/v1/verify', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer mk_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ to: '+14155550142', channel: 'voice' }),
});
const verification = await start.json();
// When live:
// { id, status: 'pending', channel: 'voice', hint: '•••0142', dispatch: 'sent', expiresAt }
// Today:
// { error: 'channel_not_available', channel: 'voice' }  (HTTP 422)
Python
import requests

start = requests.post(
    "https://api.message.com/v1/verify",
    headers={"Authorization": "Bearer mk_live_..."},
    json={"to": "+14155550142", "channel": "voice"},
)
verification = start.json()
# When live:
# { "id": ..., "status": "pending", "channel": "voice", "hint": "•••0142", "dispatch": "sent", "expiresAt": ... }
# Today:
# { "error": "channel_not_available", "channel": "voice" }  (HTTP 422)

When voice is live, a successful start returns 201 with the same flat body every channel uses:

201 Created (when live)
{
  "id": "1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f",
  "status": "pending",
  "channel": "voice",
  "hint": "•••0142",
  "dispatch": "sent",
  "expiresAt": "2026-07-25T18:14:11.000Z"
}

Today, the same call returns 422:

422 Unprocessable (today)
{
  "error": "channel_not_available",
  "channel": "voice"
}

2. User receives the code

The user answers a call and hears the code read out digit by digit, twice, from the Verify Service. See the spoken template below for the exact script.

3. Check the code

The check call is channel-agnostic and works today. When the user types the code they heard, send it back with the id from step 1.

cURL (macOS/Linux)
curl -X POST https://api.message.com/v1/verify/check \
  -H 'Authorization: Bearer mk_live_...' \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f",
    "code": "482913"
  }'
cURL (Windows)
curl -X POST https://api.message.com/v1/verify/check ^
  -H "Authorization: Bearer mk_live_..." ^
  -H "Content-Type: application/json" ^
  -d "{\"id\": \"1f6a2e4c-9d3b-4a1e-8f2c-6b7a9e0d5c3f\", \"code\": \"482913\"}"
Node
const check = await fetch('https://api.message.com/v1/verify/check', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer mk_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ id: verification.id, code: userSubmittedCode }),
});
const result = await check.json();
// success: { id: '1f6a2e4c-...', verified: true }
// failure: { id: '1f6a2e4c-...', verified: false, reason: 'bad_code' }
Python
check = requests.post(
    "https://api.message.com/v1/verify/check",
    headers={"Authorization": "Bearer mk_live_..."},
    json={"id": verification["id"], "code": user_submitted_code},
)
result = check.json()
# success: {"id": "1f6a2e4c-...", "verified": True}
# failure: {"id": "1f6a2e4c-...", "verified": False, "reason": "bad_code"}

A correct code returns 200 with { id, verified: true } and bills $0.03. A wrong code also returns 200, with verified: false and a reason. Read the verified field, not the HTTP status.

The spoken template

Voice reads the code one digit at a time and repeats it once, so the user has time to write it down. The script is built from the service friendlyName and the generated code:

Voice script
Your Acme verification code is: 4, 8, 2, 9, 1, 3. Again, 4, 8, 2, 9, 1, 3. Goodbye.

The Acme is the service friendlyName, the sender brand for the call. The spoken script is fixed (there are no voice template variants), and the digits are read out individually and then repeated for clarity.

Fraud interactions

Voice is a phone channel, so it inherits the same fraud controls as SMS. When voice is live these apply on every send:

  • Country allowlist. A number whose country is not in the service's allowedCountries returns 403 country_not_allowed.
  • Per-destination throttle. More than 3 sends to the same number in 60 seconds returns 429 rate_limited with scope: "destination".
  • Per-service velocity. The per-minute and per-day caps return 429 rate_limited with scope: "workspace".
  • Attempt cap. Five wrong codes lock the verification with too_many_attempts.

Country allowlists, velocity caps, and destination throttles are covered in depth in the Verify fraud guide.

Troubleshooting

SymptomCause and fix
422 channel_not_availableExpected today: voice is disabled for sending. Use SMS or email now. Do not retry this call for the same channel; it will not become available mid-session.
400 channel_not_enabledThe resolved service does not list voice in enabledChannels. This is a per-service setting, distinct from the platform-wide channel_not_available.
403 country_not_allowed (when live)The number's country is not in the service's allowedCountries. Add the country to the allowlist.
400 invalid_numberThe to value fails the phone-shape check. Send digits, optionally with a leading +.
Check returns reason: "expired"The code is past expiresAt. Start a new verification. There is no separate resend endpoint.

Every reason above, with its exact firing condition, is on the Verify API error codes page.