Get an API key
Open an account, then create a key under Dashboard, API keys. Live keys (wmmn_live_sk_) send real traffic and charge your balance. Test keys (wmmn_test_sk_) run the same request through routing and pricing without delivering it, and draw on test credit, which comes with an invite code.
A key restricted to scopes needs verify:write. On a test key no call is placed and the start response includes testCode.
Every sample reads the key from the environment. Keep it out of source control.
export PACKETEXCHANGE_API_KEY=wmmn_live_sk_...Fund the balance
PacketExchange is prepaid. Top up from $5 by card or crypto, or from $100 by wire, under Dashboard, Billing. An agent can top itself up with x402. There is no contract and no monthly fee.
Each start places one short call (capped at 120 seconds), billed like any API call. Unanswered calls cost nothing. Checks are free.
CostThe call that reads the code, at the route rate per billing increment plus 2% of the route rate, never more than $0.001. No text-to-speech surcharge and no per-verification fee.
Make the request
Start with channel "voice", a language (en, es, fr, de, pt or hi) and the caller ID the call should present in from. Then check the code exactly as for SMS.
# 1. Send the code. PacketExchange generates it, reads the code aloud on a call and keeps only a hash.
verification_id=$(curl -s https://packetexchange.io/api/v1/verify/start \
-H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: $(uuidgen)" \
-d '{"to": "+447700900123", "channel": "voice", "from": "+14155550100", "language": "en", "brand": "Acme"}' | jq -r .data.verificationId)
# 2. Check the code the user typed in. A wrong code is an answer ("denied"), not an error.
read -r -p "Code: " code
curl -s https://packetexchange.io/api/v1/verify/check \
-H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"verificationId\": \"$verification_id\", \"code\": \"$code\"}" | jq .dataHandle the response
Start returns as soon as the call is dialled: sendStatus is "initiated" and sendRef is the call id. Check answers the same way as for SMS.
{
"success": true,
"data": {
"verificationId": "<uuid>",
"to": "+447700900123",
"channel": "voice",
"status": "pending",
"expiresAt": "<ISO-8601 UTC>",
"maxAttempts": 5,
"sendRef": "<callId of the code call>",
"sendStatus": "initiated",
"createdAt": "<ISO-8601 UTC>"
}
}{
"success": true,
"data": {
"verificationId": "<uuid>",
"status": "approved",
"attemptsRemaining": <integer>
}
}Values in angle brackets are placeholders for your own ids, times and amounts.
- language
- en, es, fr, de, pt or hi for voice. SMS codes also support ar.
- from
- The E.164 caller ID the code call presents.
- sendStatus
- "initiated" for a voice start: the call is on its way. The final outcome is recorded when it ends.
- status (check)
- approved, denied, expired or max_attempts.
When something goes wrong
- The same limits as SMS apply: 5 starts per number per hour, a 30-second resend cooldown and 1,000 per account per day.
- Already generate your own codes? POST /comms/voice-otp reads a code you supply and returns a voice_otp id you can look up later.
- Errors use the standard envelope: { "success": false, "error": { "code", "message", "details" } }.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{ "path": "to", "message": "<why the field was refused>" }
]
}
}