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. A test key simulates the call against test credit and returns status "accepted" with simulated: true.
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.
The worst-case cost for maxDuration is reserved from your balance when the call starts, and the unused part is refunded at hangup.
CostBillable seconds at the route rate, rounded to the route billing increment, plus 2% of the route rate, never more than $0.001 per call. Unanswered calls cost nothing.
Make the request
Dial an E.164 number with the caller ID to present in from. maxDuration (10 to 3,600 seconds, default 300) hangs up for you. Leave routeId out and Smart Routing picks the route; set your HTTP timeout longer than maxDuration.
# The request stays open until the call ends, so allow longer than maxDuration.
curl --max-time 180 https://packetexchange.io/api/v1/comms/calls \
-H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: $(uuidgen)" \
-d '{
"to": "+447700900123",
"from": "+14155550100",
"maxDuration": 60
}'Handle the response
The response arrives when the call is over, so it already carries the billed result. To get the call id at once instead, send async: true and follow the call with GET /comms/calls/{id} or the call webhooks.
{
"success": true,
"data": {
"callId": "<call id>",
"to": "+447700900123",
"from": "+14155550100",
"status": "answered",
"sipResponseCode": 200,
"hangupCause": "<cause>",
"durationSeconds": <integer>,
"billableSeconds": <integer>,
"cost": "<USD, 6 decimals>",
"billingIncrement": "<e.g. 6/6 or 60/60>",
"startedAt": "<ISO-8601 UTC>",
"completedAt": "<ISO-8601 UTC>"
}
}Values in angle brackets are placeholders for your own ids, times and amounts.
- status
- answered, no_answer, busy or failed. accepted appears only on a simulated test-key call.
- billableSeconds
- The duration rounded up to the route billing increment.
- cost
- What the call cost you, fee included, as a 6-decimal USD string.
- hangupCause, sipResponseCode
- Why the call ended, for your logs and retries.
When something goes wrong
- A call that is not answered is a 200 with status no_answer, busy or failed, not an HTTP error.
- Send an X-Idempotency-Key so a retried request can never dial twice.
- If the balance cannot cover maxDuration at the route rate, the call is refused before anything is dialled.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{ "path": "to", "message": "<why the field was refused>" }
]
}
}Go further
- Make the call talkAdd say, play, gather and hangup actions, or pass async: true and follow the call with GET /comms/calls/{id}.
- Get completed calls as webhooksSubscribe to call.completed and verify each delivery.
- Check the caller ID firstTest what a route actually shows on a handset before you put traffic on it.
- Put an AI agent on the callBuild an agent and attach it to an outbound campaign.
- Voice on PacketExchangeCaller ID types, billing increments and codecs.