Skip to content

Quickstart, Verification

Verify a phone number by call-in

Call-in verification turns the check around: your user calls one of your own phone numbers from the phone they are verifying. The call is matched by its caller ID and ended without being answered, so the caller pays nothing and there is no text message to deliver, filter or pump.
Calls
GET/verify/call-in/numbersPOST/verify/call-in/numbersPOST/verify/startGET/verify/{verificationId}
Scope
verify:write
Cost
The monthly price of the phone number you use, plus $0.01 per approved verification taken from your balance. A verification that expires without a call costs nothing, and there are no message or minute fees.
On this page
  1. Step 1Get an API key
  2. Step 2Fund the balance
  3. Step 3Make the request
  4. Step 4Handle the response
  5. Step 5Go further

Step 1

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 verify:write. On a test key no real call is needed: POST /verify/{verificationId}/simulate-call approves a pending verification as if the user had called, and the response says simulated: true.

Every sample reads the key from the environment. Keep it out of source control.

export PACKETEXCHANGE_API_KEY=wmmn_live_sk_...

Step 2

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.

Set up once. Buy a phone number in the DID Store, in the country of the people you verify (each call-in number verifies phone numbers in its own country). Switch on call-in for it on the Phone verification page of the control panel, or with POST /verify/call-in/numbers and its phoneNumberId from GET /verify/call-in/numbers. While call-in is on, calls to that number are used for verification only and do not ring. Then keep at least $0.01 in your balance.

Cost

The monthly price of the phone number you use, plus $0.01 per approved verification taken from your balance. A verification that expires without a call costs nothing, and there are no message or minute fees.

Step 3

Make the request

Start with channel "call_in" and the E.164 number to verify. reference is your own id for the user or session: starting again with the same reference, or the same number, while one is pending returns the pending verification instead of a new one. Then show the user callNumberDisplay, which is your call-in number for their country, and wait.

# 0. Once: switch on call-in for a phone number you bought in the DID Store, in the
#    country you verify. Pick an active number with callInEnabled false; while call-in is
#    on, calls to it are used for verification only and do not ring.
curl -s https://packetexchange.io/api/v1/verify/call-in/numbers \
  -H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" | jq '.data[] | {phoneNumberId, numberDisplay, verifiesDialCode, status, callInEnabled}'
# curl -s -X POST https://packetexchange.io/api/v1/verify/call-in/numbers \
#   -H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" -H "Content-Type: application/json" \
#   -d '{"phoneNumberId": "<phoneNumberId>"}'

# 1. Start a call-in verification. reference is your own id for this user; starting again
#    with the same reference while one is pending returns the pending one.
started=$(curl -s 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": "+17735550100", "channel": "call_in", "reference": "user-8812"}')
verification_id=$(echo "$started" | jq -r .data.verificationId)

# 2. Show your user the number to call: your call-in number for their country. The call
#    ends by itself and is free for them.
echo "$started" | jq -r .data.instructions

# 3. Wait for the call: poll every 3 seconds (or handle the verification.approved webhook).
while :; do
  status=$(curl -s "https://packetexchange.io/api/v1/verify/$verification_id" \
    -H "Authorization: Bearer $PACKETEXCHANGE_API_KEY" | jq -r .data.status)
  [ "$status" != "pending" ] && break
  sleep 3
done
echo "$status"
Save as request.sh, run bash request.shNeeds curl, plus jq for the multi-step samplesThe sample prints the number to call, then waits until you call it from the phone you are verifying. It needs a call-in number switched on for that phone's country.

Step 4

Handle the response

Start returns the number to call and a ready-made instruction. Read the verification with GET /verify/{verificationId} until status leaves "pending", or subscribe to the verification.approved webhook and skip polling.

200 OKPOST /verify/call-in/numbers
{
  "success": true,
  "data": {
    "phoneNumberId": "<uuid>",
    "number": "+17735550142",
    "numberDisplay": "+1 (773) 555-0142",
    "country": "United States",
    "verifiesDialCode": "+1",
    "type": "Local",
    "status": "active",
    "callInEnabled": true,
    "hasCallRouting": false
  }
}
200 OKPOST /verify/start
{
  "success": true,
  "data": {
    "verificationId": "<uuid>",
    "reference": "user-8812",
    "to": "+17735550100",
    "channel": "call_in",
    "status": "pending",
    "callNumber": "+17735550142",
    "callNumberDisplay": "+1 (773) 555-0142",
    "callerNumber": null,
    "callReceivedAt": null,
    "callerIdAttestation": null,
    "expiresAt": "<ISO-8601 UTC>",
    "createdAt": "<ISO-8601 UTC>",
    "approvedAt": null,
    "price": "0.010000",
    "instructions": "Call +1 (773) 555-0142 from the phone you are verifying. The call ends by itself and is free; you are verified as soon as it connects."
  }
}
200 OKGET /verify/{verificationId}
{
  "success": true,
  "data": {
    "verificationId": "<uuid>",
    "reference": "user-8812",
    "to": "+17735550100",
    "channel": "call_in",
    "status": "approved",
    "callNumber": "+17735550142",
    "callNumberDisplay": "+1 (773) 555-0142",
    "callerNumber": "+17735550100",
    "callReceivedAt": "<ISO-8601 UTC>",
    "callerIdAttestation": "A",
    "expiresAt": "<ISO-8601 UTC>",
    "createdAt": "<ISO-8601 UTC>",
    "approvedAt": "<ISO-8601 UTC>",
    "price": "0.010000",
    "instructions": "<as above>"
  }
}

Values in angle brackets are placeholders for your own ids, times and amounts.

Response fields

verifiesDialCode
The dial code of the phone numbers a call-in number verifies: its own country.
hasCallRouting
true when the number rings somewhere today. Switching on call-in stops that ringing until you switch call-in off with DELETE /verify/call-in/numbers/{phoneNumberId}.
status
pending, approved or expired. Only approved proves a call arrived from the number.
callNumberDisplay
Your call-in number for the user's country, formatted for reading. callNumber is the same number in E.164, for a tap-to-call link.
instructions
A one-line instruction you can show as it is, or replace with your own words.
expiresAt
Call-in verifications expire after 300 seconds by default; set expirySeconds from 60 to 3600.
callerIdAttestation
When approved: the STIR/SHAKEN attestation the calling network attached (A, B or C), another verification status string from the network, or null when none was sent.
reference
Your own id, up to 128 letters, digits and . _ : @ - characters. Echoed on every read and webhook, and filterable with GET /verify?reference=.

When something goes wrong

  • Call-in works in any country where you hold one of our phone numbers switched on for call-in. Starting one for a country where you have none is a 400 that says how to add a number, or to use channel sms or voice, so fall back to a code.
  • Only an active number can be switched on; POST /verify/call-in/numbers on a number still being set up is a 400.
  • A 402 INSUFFICIENT_BALANCE means the balance cannot cover the $0.01. Starts are rate limited per number and per account; a refusal is a 429 with RATE_LIMITED.
  • POST /verify/check on a call-in verification is a 400: there is no code to check.
  • A caller ID is a weaker proof than a code, because some networks let callers set it. Use call-in for sign-up and phone number confirmation; for account recovery, payments and other high-risk actions, ask for an SMS or voice code.
400The error envelope
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": [
      { "path": "to", "message": "<why the field was refused>" }
    ]
  }
}

Step 5

Go further

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.