Skip to content
Markets open
Quickstart · Voice

Make a phone call

One POST dials the number and holds the request open until the call ends, then answers with the final outcome from the call record: answered or not, how long, and what it cost.
Calls
POST /comms/calls
Scope
voice:send
Cost
Billable 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.

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
  }'
Save as request.sh, run bash request.shNeeds curl, plus jq for the multi-step samples

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.

200 OKPOST /comms/calls
{
  "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.
400The error envelope
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": [
      { "path": "to", "message": "<why the field was refused>" }
    ]
  }
}

Go further

Start building

Run it with your own key

Open an account, add credit from $5 and create a key. You pay the carrier's rate plus a fee capped at $0.001.