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.
Searching needs numbers:read and buying needs numbers:write on a restricted key. Where identity verification applies, it must be complete before you buy.
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.
Fund at least the setup price plus one month. Keep the balance above the monthly price: a renewal that fails suspends the number for a grace period before it is released.
CostEach SKU lists its setup price and monthly price. Buying charges the setup price plus the first month; after that the number renews monthly from your balance.
Make the request
Run the sample with no arguments to search. Run it again with a groupId and a skuId from the results to buy. The idempotency key makes sure a retry orders one number, not two.
# 1. Search the catalogue. Results are top-level "hits", each with a groupId and SKUs.
curl -s "https://packetexchange.io/api/v1/dids/search?pattern=1415&limit=5" \
-H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" |
jq '.hits[] | {groupId, dialingPrefix, country, city, skus}'
# 2. Buy one. This charges the setup price plus the first month to your balance.
curl https://packetexchange.io/api/v1/dids/buy \
-H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: $(uuidgen)" \
-d "{\"groupId\": \"$GROUP_ID\", \"skuId\": \"$SKU_ID\"}"Handle the response
Search results are top-level hits, not wrapped in data. A new number starts pending and gets its digits when provisioning completes.
{
"success": true,
"hits": [
{
"groupId": "<group id>",
"country": "<country>",
"city": "<city or null>",
"typeName": "<Local, National, Mobile, Toll-free...>",
"dialingPrefix": "<country and area prefix>",
"skus": [
{
"skuId": "<sku id>",
"channels": <integer>,
"setupPrice": <USD>,
"monthlyPrice": <USD>
}
]
}
],
"scannedPages": <integer>,
"truncated": false
}{
"success": true,
"data": {
"id": "<uuid>",
"number": null,
"status": "pending",
"pointMode": "unrouted",
"setupPrice": "<USD, 6 decimals>",
"monthlyPrice": "<USD, 6 decimals>",
"autoRenew": <boolean>,
"nextRenewalAt": "<ISO-8601 UTC or null>",
"graceDays": <integer>
}
}Values in angle brackets are placeholders for your own ids, times and amounts.
- status
- pending, active, suspended (a renewal failed), released or failed.
- number
- null until provisioning completes, then the E.164 number.
- pointMode, pointsTo
- Where calls go: unrouted, sip (host[:port]) or forward (an E.164 number).
- data.disabled
- If the number store is unavailable, search answers 200 with disabled: true and a message.
When something goes wrong
- Route the number with PATCH /dids/{id}/routing and { "mode": "sip", "target": "sip.example.com:5060" }, with an optional backup.
- Inbound calls and texts arrive as number.call.received and number.sms.received webhooks.
- Porting existing numbers in is not offered.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{ "path": "to", "message": "<why the field was refused>" }
]
}
}