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 nothing is sent and the start response includes testCode, so a sandbox can complete the flow.
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 sends one SMS, billed like any other message. Checks cost nothing.
CostThe SMS that carries the code, at the route rate plus 2% of the route rate, never more than $0.001. There is no per-verification fee, and checking a code is free.
Make the request
Start a verification for an E.164 number with channel "sms", then check the code the user enters. brand is the name in the message; leave it out and your company name is used.
# 1. Send the code. PacketExchange generates it, texts the code 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": "sms", "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 a verificationId, never the code on a live key. Check returns a status: only "approved" proves the user holds the number.
{
"success": true,
"data": {
"verificationId": "<uuid>",
"to": "+447700900123",
"channel": "sms",
"status": "pending",
"expiresAt": "<ISO-8601 UTC>",
"maxAttempts": 5,
"sendRef": "<messageId of the SMS>",
"sendStatus": "accepted",
"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.
- status (check)
- approved, denied, expired or max_attempts. A wrong code is a normal 200 answer with status denied, not an HTTP error.
- reason
- On a check that was not approved: wrong_code, already_used or not_pending.
- expiresAt
- Codes expire after 600 seconds by default; set expirySeconds from 60 to 3600.
- maxAttempts
- Five wrong guesses and the verification is dead. An approved verification cannot be approved again.
When something goes wrong
- Starts are rate limited: 5 per number per hour, a 30-second resend cooldown, 20 per number range per hour and 1,000 per account per day. A refusal is a 429 with RATE_LIMITED.
- Embargoed, high-risk and do-not-contact destinations are refused before anything is sent.
- Check with the same kind of key (test or live) that started the verification.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{ "path": "to", "message": "<why the field was refused>" }
]
}
}