Verify API · SMS and voice OTP
One-time codes,no verification fee
The Verify API makes the code, sends it by text or reads it aloud on a call, and checks what your user types. You pay for the message or call that carried it, and nothing per verification. Limits per number, per number range and per account are built in, and premium-rate ranges are refused before anything is sent.
2 routes serve +447911123456, cheapest first
United Kingdom (All Operators)
1/1 billing
$0.02025/msg
United Kingdom (All Operators)
1/1 billing
$0.0396/msg
- Two requests: start and check
- Voice codes in six languages
- Codes kept only as a hash
Pumping attacks hit a wall before they hit your balance
Defaults on every account. Premium-rate, satellite and known pumping ranges are refused before any code is sent, and so is any number on your do-not-contact list. If the limit store cannot be reached, the API refuses to send rather than send without limits.
Pay for the message, not the verification
A code is one short message or one short call. That is all it costs here, and the limits that stop it being abused are already switched on.

No per-verification fee
A code costs the SMS segment or the billed seconds of the call that carried it, plus the platform fee of 2% capped at $0.001. Nothing per verification and nothing extra for text-to-speech.
Limits against SMS pumping
A 30-second resend cooldown, 5 codes per number an hour, 20 per number range an hour and a daily cap per account. Premium-rate, satellite and known pumping ranges are refused outright.
A voice channel when SMS will not do
Set channel to voice and a call reads the digits aloud, twice, in English, Spanish, French, German, Portuguese or Hindi, from the caller ID you pass.
A code you never have to store
We generate it, keep only an HMAC hash, allow 5 attempts and expire it after 10 minutes, or anything from 1 to 60 minutes that you set.
A prepaid ceiling
Your balance is the hard stop: an attack cannot spend money you have not topped up. Add a daily spend alert and an auto-recharge with a daily cap.
Verify a number in two requests
Start
POST /verify/start with the number and a channel, sms or voice. We generate the code, send it and return a verification ID.
Your user types the code
It arrives as a text in their language, or as a call that reads it out twice.
Check
POST /verify/check with the ID and the digits. The answer is approved, denied, expired or max_attempts, and a wrong code is an answer, not an error.
# 1. Send the code. We generate it and keep only a hash.
curl -X POST 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": "+14155550100",
"channel": "sms"
}'
# 2. Check what the user typed.
curl -X POST https://packetexchange.io/api/v1/verify/check \
-H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "verificationId": "<from step 1>", "code": "482913" }'import { randomUUID } from 'node:crypto';
const px = (path, body, headers = {}) =>
fetch(`https://packetexchange.io/api/v1${path}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PACKETEXCHANGE_API_KEY}`,
'Content-Type': 'application/json',
...headers,
},
body: JSON.stringify(body),
}).then((r) => r.json());
// 1. Send the code. We generate it and keep only a hash.
const start = await px(
'/verify/start',
{ to: '+14155550100', channel: 'sms' },
{ 'X-Idempotency-Key': randomUUID() },
);
// 2. Check what the user typed: approved, denied, expired or max_attempts.
const check = await px('/verify/check', {
verificationId: start.data.verificationId,
code: userInput, // the digits your user typed
});
console.log(check.data.status);import os, uuid
import httpx
px = httpx.Client(
base_url="https://packetexchange.io/api/v1",
headers={"Authorization": f"Bearer {os.environ['PACKETEXCHANGE_API_KEY']}"},
)
# 1. Send the code. We generate it and keep only a hash.
start = px.post(
"/verify/start",
json={"to": "+14155550100", "channel": "sms"},
headers={"X-Idempotency-Key": str(uuid.uuid4())},
).json()["data"]
# 2. Check what the user typed: approved, denied, expired or max_attempts.
check = px.post(
"/verify/check",
json={"verificationId": start["verificationId"], "code": user_input}, # what they typed
).json()["data"]
print(check["status"])A test key returns the code as testCode, so the whole flow runs in CI without a phone (sandbox credit is added with an invite code).
A real market, in the open
Fails closed
If the limit store cannot be reached, the API refuses to send codes rather than send them without limits.
Refused before routing
Embargoed destinations, high-risk ranges and numbers on your do-not-contact list are refused before anything is sent or billed.
Every code accounted for
The cost of each send is recorded against its verification, so finance can reconcile codes to the cent.
Scoped keys
A key limited to the verify permission can start and check codes and do nothing else.
Live interconnects through Minutes Network
OTP API, answered
Anything else is in the help centre, or ask the team directly.
What does one code cost?
By SMS, the route's per-segment rate for the destination plus 2%, capped at $0.001; a code message fits in one segment. By voice, the route's per-minute rate for the billed seconds of a short call, plus the same capped fee. There is no fee per verification.
Is there a fee per verification or per successful check?
No. Starting and checking a verification costs nothing in itself. You pay only for the SMS or call that delivered the code.
How does the API stop SMS pumping?
Premium-rate, satellite and known pumping ranges are refused before any send. Codes are capped per number and per number range each hour, with a resend cooldown and a daily account cap. Your prepaid balance is the final ceiling.
Which languages are supported?
Voice codes are spoken in English, Spanish, French, German, Portuguese and Hindi. SMS codes can also be sent in Arabic.
Does it fall back from SMS to voice on its own?
No, you decide. If a carrier refuses the SMS, the start request returns an error that suggests the voice channel, and the refused attempt does not use up the hourly limit. Start again with channel voice.
Which route carries the code?
Verify picks the best-quality route for the number by default. Pass strategy cheapest or balanced to change that.
Can I send my own code instead?
Yes. Send it in your own words with POST /comms/sms, or have a call speak it with POST /comms/voice-otp. The price is the same: the message or the call.
Add phone verification in two requests
Free account and API key. Prepaid from $5, and every code costs only the message or call that carries it.