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 voice:send to start a check, routes:read to read it and purchases:write to buy. You can check any public marketplace route before you buy it, or a private route you have access to. A test key returns a simulated result at once: no call, no charge.
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.
A check is billed like a call, so it needs a funded balance. Limits: 10 checks a minute and 60 an hour per account.
CostThe route's rate for the seconds the check call was connected (usually about two), plus 2% of the route rate, never more than $0.001. A route that does not connect costs nothing, and checking your own listing is free.
Make the request
Send 1 to 3 route ids in your order: the first is your primary, the rest backups. Add the number you plan to call (or its dial code) so the check uses a line of the same kind, mobile or fixed. The call goes to a public line in the destination that answers by itself and is ended after about two seconds. Set mode to ring_me with your own number to have the routes ring your phone instead.
# 1. Check up to three routes, in your order: the first is your primary, the rest backups.
check_id=$(curl -s https://packetexchange.io/api/v1/routes/check \
-H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"routeIds\": [\"$ROUTE_1\", \"$ROUTE_2\"], \"number\": \"+923001234567\"}" |
jq -r .data.checkId)
# 2. Read the verdicts until status is done (usually well under a minute).
curl -s "https://packetexchange.io/api/v1/routes/check/$check_id" \
-H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" |
jq '.data | {status, totalCost, routes: [.routes[] | {role, routeName, result, message}]}'
# 3. Buy the routes that passed; they go to the top of your routing order, primary first.
curl -s -X POST "https://packetexchange.io/api/v1/routes/check/$check_id/purchase" \
-H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" |
jq '.data.routingOrder'Handle the response
POST returns 202 with a checkId straight away. Read the check until status is done; each route carries its verdict and the sentence to show.
{
"success": true,
"data": { "checkId": "<uuid>" }
}{
"success": true,
"data": {
"id": "<uuid>",
"status": "done",
"mode": "network",
"simulated": false,
"number": "+923001234567",
"totalCost": "<USD, 6 decimals>",
"routes": [
{
"position": 0,
"role": "primary",
"routeId": "<route id>",
"routeName": "<route name>",
"result": "working",
"message": "This route is working for your needs.",
"checkType": "full",
"destination": "Pakistan",
"lineType": "mobile",
"answerMs": 4210,
"billableSeconds": 2,
"cost": "<USD, 6 decimals>",
"paidFrom": "test_credit"
},
{
"position": 1,
"role": "backup",
"routeId": "<route id>",
"routeName": "<route name>",
"result": "no_capacity",
"message": "No capacity on this route right now. Choose another route.",
"checkType": "full",
"sipCode": 503,
"billableSeconds": 0,
"cost": "0.000000"
}
],
"working": ["<route id>"]
}
}Values in angle brackets are placeholders for your own ids, times and amounts.
- result
- working (answered), no_capacity (refused or no response), not_delivered (rang but never reached the line), false_answer (answered under 1.5 seconds after our INVITE, or under 2.5 seconds with no ringing: faster than a real line can answer, so the route almost certainly answered the call itself; not charged), inconclusive (no verdict, nothing counted against the route) or accepts (the lighter check below).
- answerMs
- Milliseconds from our INVITE to the answer, when the call was answered. The evidence behind false_answer.
- paidFrom
- Which wallet paid a charged leg: test_credit, balance or mixed. A check is a test call, so your test credit is used first and your balance only for the rest. GET /routes/check/coverage tells you, per route, whether we have a test line; when we do not, check with mode ring_me and a number of your own.
- checkType
- full is a call to a test line; light means we have no test line for that destination yet, so we only confirmed the route accepts calls and called no one; ring_me is your own phone; simulated is a test key.
- message
- The plain sentence to show a person.
- working
- The route ids that passed, in your order.
When something goes wrong
- POST /routes/check/{id}/purchase buys the routes that passed and puts them at the top of your routing order, the first as primary. Routes you already bought are reused, and a route that cannot be bought is reported with its reason.
- Route names in the response never identify the seller.
- For an SMS route, POST /routes/check/sms with { "routeId", "number" } sends one fixed test message to your own mobile (scope sms:send). Read it with the same GET /routes/check/{id}: accepted, then delivered once a carrier receipt confirms it, or refused with an errorCode. A refused message is not charged.
- Errors use the standard envelope: { "success": false, "error": { "code", "message", "details" } }; too many checks answer 429 RATE_LIMITED.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{ "path": "to", "message": "<why the field was refused>" }
]
}
}