API reference
Voice and SMS
Place single calls and send single SMS over the routes you bought, and read their history.
7 operationsBase URL https://packetexchange.io/api/v1Postman collection
BrowseVoice and SMS
List your API call history
GET/
- Access
- API key. Full-access keys only; not reachable by scoped keys.
- Rate limit
- 100 requests per second (the default)
Ledger entries for calls placed through POST /comms/calls, newest first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
type | query | string | Accepted but not applied: each endpoint already fixes its own typeOne of api_call, api_sms, smpp_sms, dialer_campaign |
from | query | string (date-time) | Only rows created at or after this instant |
to | query | string (date-time) | Only rows created before this instant |
cursor | query | string (uuid) | The nextCursor from the previous page |
limit | query | integer | -Default 25 |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object[] | - |
idrequireddata[].id | string (uuid) | - |
userIdrequireddata[].userId | string (uuid) | - |
typerequireddata[].type | string | Ledger entry type, e.g. charge |
amountrequireddata[].amount | money | Signed: a charge is negative USD as a decimal string with exactly 6 places, e.g. "0.012500". |
balanceAfterrequireddata[].balanceAfter | money | USD as a decimal string with exactly 6 places, e.g. "0.012500". |
referencerequireddata[].reference | string | null | Human-readable description of the call or message |
relatedEntityTyperequireddata[].relatedEntityType | string | null | api_call for calls; api_sms or smpp_sms for messages |
relatedEntityIdrequireddata[].relatedEntityId | string (uuid) | null | The callId or messageId |
callIdrequireddata[].callId | string | null | - |
createdAtrequireddata[].createdAt | string (date-time) | ISO-8601 timestamp (UTC) |
nextCursorrequired | string | null | Pass as cursor to fetch the next page; null on the last page. |
hasMorerequired | boolean | True when another page exists. |
Errors
- 400
VALIDATION_ERROR,INVALID_INPUTorBAD_REQUEST. ForVALIDATION_ERROR,error.detailsis an array of{ path, message }. - 401
UNAUTHORIZED: missing, invalid, expired or revoked credential. - 403
FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only. - 429
RATE_LIMITED: slow down and retry after theRetry-Afterseconds. - 500
INTERNAL_ERROR: unexpected failure. QuoteX-Request-Idto support.
Place a single outbound call
POST/
- Access
- API key. Scoped keys need
voice:send. - Rate limit
- 10 requests per second
- Safe retries
- Send
X-Idempotency-Key; a replay within 24 hours returns the first response. - Real traffic
- Places a real call or sends a real message, billed to your balance. A test key simulates it against test credit.
Dials to over a route you purchased (or one Smart Routing selects). By default the request BLOCKS until the call ends - answer and hangup, no answer, or maxDuration - then returns the outcome and what it cost (200); set a client timeout above maxDuration. With async: true it returns at once with 202 and status: ringing; follow the call on GET /comms/calls/{id} or with the call.ringing, call.answered, call.gathered and call.completed webhooks. Validation, routing and balance errors are still returned straight away in both modes. actions make the answered call speak (say, in en, es, fr, de, pt or hi), play an MP3 (play), collect keypad digits (gather), wait (pause) or end (hangup), in order; audio is prepared before dialling, so a bad URL costs nothing. Billed per the route increment at the route price plus the platform fee, after the call completes; there is no charge for spoken text. A test key simulates the call against test credit and runs no actions.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
X-Idempotency-Key | header | string | Any unique string (a UUID is ideal). Replays within 24 hours return the first response instead of acting twice. |
Request body (application/json)
| Field | Type | Description |
|---|---|---|
torequired | string | Destination number (E.164) |
fromrequired | string | Caller ID to present (E.164) |
routeId | string (uuid) | A route you purchased. Omit to let Smart Routing pick one |
strategy | string | Smart Routing strategy used when routeId is omittedOne of cheapest, best_quality, balanced |
maxDuration | integer | Hang up after this many seconds10 to 3600, default 300 |
async | boolean | Return at once (202, status ringing) instead of waiting for the call to end. Follow the call on GET /comms/calls/{id} or with the call.* webhooks |
actions | object[] | What the call does once answered, in order: say, play, gather, pause, hangup. At most 10 actions, 1,500 characters of spoken text, 3 play URLs and 5 gathers. The call ends when the actions finish |
sayactions[].say | string | Text spoken by our text-to-speech voicemax 500 chars |
languageactions[].language | string | Overrides the call default for this actionOne of en, es, fr, de, pt, hi |
playactions[].play | string (uri) | HTTPS URL of an MP3 file, at most 2 MB. We download it once before dialling (public addresses only, no redirects) and play our copymax 2048 chars |
gatheractions[].gather | object | - |
digitsactions[].gather.digits | integer | Most keys to collect1 to 20, default 1 |
timeoutactions[].gather.timeout | integer | Seconds to wait for the first key1 to 30, default 5 |
finishOnKeyactions[].gather.finishOnKey | string | Key that ends input early; empty for nonedefault "#"One of #, *, |
triesactions[].gather.tries | integer | Prompt and wait again this many times in total when nothing is pressed1 to 3, default 1 |
sayactions[].gather.say | string | Prompt spoken while listening; a key press interrupts itmax 500 chars |
playactions[].gather.play | string (uri) | Prompt played while listening (instead of say)max 2048 chars |
languageactions[].gather.language | string | -One of en, es, fr, de, pt, hi |
pauseactions[].pause | integer | Seconds of silence1 to 10 |
hangupactions[].hangup | boolean | -One of true |
language | string | Default language for say actions (default en)One of en, es, fr, de, pt, hi |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object | - |
callIddata.callId | string (uuid) | Use with GET /comms/calls/{id}; the same id appears on the call.* webhooks |
todata.to | string | - |
fromdata.from | string | - |
statusdata.status | string | The call has been handed to the network and is being dialledOne of ringing |
sipResponseCodedata.sipResponseCode | integer | null | - |
hangupCausedata.hangupCause | string | null | - |
durationSecondsdata.durationSeconds | integer | - |
billableSecondsdata.billableSeconds | integer | Duration rounded up to the route billing increment |
costdata.cost | money | What this call cost you, platform fee included USD as a decimal string with exactly 6 places, e.g. "0.012500". |
billingIncrementdata.billingIncrement | string | The route billing increment, e.g. "6/6" or "60/60" |
startedAtdata.startedAt | string (date-time) | ISO-8601 timestamp (UTC) |
completedAtdata.completedAt | string (date-time) | ISO-8601 timestamp (UTC) |
simulateddata.simulated | boolean | Present and true for test-key calls: nothing was dialled |
routeIddata.routeId | string (uuid) | null | Present on calls placed over a failover chain (Smart Routing calls with automatic failover, and Switch sub-account calls): the route that carried the call, or the last one tried |
attemptsdata.attempts | object[] | Present when the call was tried on more than one route: each route tried, in order. Only the one that connected is billed; a wrong number (404/484), a busy or declined call and no answer are never retried |
routeIdrequireddata.attempts[].routeId | string (uuid) | - |
sipResponseCoderequireddata.attempts[].sipResponseCode | integer | null | - |
statusrequireddata.attempts[].status | string | - |
hangupCauserequireddata.attempts[].hangupCause | string | null | - |
modedata.mode | string | -One of async |
actionsdata.actions | integer | How many call actions will run on answer |
statusUrldata.statusUrl | string | Path of GET /comms/calls/{id} for this call |
Errors
- 400
VALIDATION_ERROR,INVALID_INPUTorBAD_REQUEST. ForVALIDATION_ERROR,error.detailsis an array of{ path, message }. - 401
UNAUTHORIZED: missing, invalid, expired or revoked credential. - 403
FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only. - 409
CONFLICT(or a code-specific 409): the change clashes with existing state. - 429
RATE_LIMITED: slow down and retry after theRetry-Afterseconds. - 500
INTERNAL_ERROR: unexpected failure. QuoteX-Request-Idto support.
Get one call: live status, cost and gathered digits
GET/
- Access
- API key. Scoped keys need
voice:send. - Rate limit
- 100 requests per second (the default)
Works for any call placed through POST /comms/calls, while it runs and after it ends: status, timestamps, duration, cost, the hangup cause in plain words, and the digits collected by gather actions. A reseller sub-account key sees only its own calls. Unknown ids answer 404.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | - |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object | - |
callIdrequireddata.callId | string (uuid) | - |
statusrequireddata.status | string | queued, ringing and answered are live states; completed (answered, then ended), no_answer, busy and failed are finalOne of queued, ringing, answered, completed, no_answer, busy, failed |
moderequireddata.mode | string | -One of sync, async |
torequireddata.to | string | - |
fromrequireddata.from | string | - |
simulatedrequireddata.simulated | boolean | True for test-key calls: nothing was dialled |
createdAtrequireddata.createdAt | string (date-time) | ISO-8601 timestamp (UTC) |
ringingAtrequireddata.ringingAt | string (date-time) | null | ISO-8601 timestamp (UTC) |
answeredAtrequireddata.answeredAt | string (date-time) | null | ISO-8601 timestamp (UTC) |
endedAtrequireddata.endedAt | string (date-time) | null | ISO-8601 timestamp (UTC) |
durationSecondsrequireddata.durationSeconds | integer | null | - |
billableSecondsrequireddata.billableSeconds | integer | null | Duration rounded up to the route billing increment |
costrequireddata.cost | money | null | What the call cost you, platform fee included. Null until it ends USD as a decimal string with exactly 6 places, e.g. "0.012500". |
billingIncrementrequireddata.billingIncrement | string | null | - |
sipResponseCoderequireddata.sipResponseCode | integer | null | - |
hangupCauserequireddata.hangupCause | string | null | The switch cause code, e.g. NORMAL_CLEARING or USER_BUSY |
hangupReasonrequireddata.hangupReason | string | null | Why the call ended, in plain words. Null while it is live |
errorrequireddata.error | string | null | Why the call could not be placed, when it failed before reaching the network |
actionsrequireddata.actions | any[] | null | The call actions as you sent them |
gatheredrequireddata.gathered | object[] | null | Keypad input from gather actions, filled in when the call ends |
indexrequireddata.gathered[].index | integer | Which gather action (0 for the first) |
digitsrequireddata.gathered[].digits | string | null | The keys pressed, without the finish key |
statusrequireddata.gathered[].status | string | no_input: nothing was pressed, or the call ended before this stepOne of received, no_input |
routeIddata.routeId | string (uuid) | null | Present when the call was placed over a failover chain: the route that carried it, or the last one tried |
attemptsdata.attempts | object[] | Present when the call was tried on more than one route: every attempt in order. Only the attempt that connected is billed |
attemptrequireddata.attempts[].attempt | integer | 1 for the first route tried |
routeIdrequireddata.attempts[].routeId | string (uuid) | null | - |
statusrequireddata.attempts[].status | string | answered, failed, busy or no_answer |
sipResponseCoderequireddata.attempts[].sipResponseCode | integer | null | - |
hangupCauserequireddata.attempts[].hangupCause | string | null | - |
Errors
- 400
VALIDATION_ERROR,INVALID_INPUTorBAD_REQUEST. ForVALIDATION_ERROR,error.detailsis an array of{ path, message }. - 401
UNAUTHORIZED: missing, invalid, expired or revoked credential. - 403
FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only. - 404
NOT_FOUND: no such resource on your account. - 429
RATE_LIMITED: slow down and retry after theRetry-Afterseconds. - 500
INTERNAL_ERROR: unexpected failure. QuoteX-Request-Idto support. - Also
NOT_FOUND
List your SMS history
GET/
- Access
- API key. Full-access keys only; not reachable by scoped keys.
- Rate limit
- 100 requests per second (the default)
Ledger entries for messages sent over the API and SMPP, newest first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
type | query | string | Accepted but not applied: each endpoint already fixes its own typeOne of api_call, api_sms, smpp_sms, dialer_campaign |
from | query | string (date-time) | Only rows created at or after this instant |
to | query | string (date-time) | Only rows created before this instant |
cursor | query | string (uuid) | The nextCursor from the previous page |
limit | query | integer | -Default 25 |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object[] | Same fields as CommsHistoryEntry, shown earlier on this page. |
nextCursorrequired | string | null | Pass as cursor to fetch the next page; null on the last page. |
hasMorerequired | boolean | True when another page exists. |
Errors
- 400
VALIDATION_ERROR,INVALID_INPUTorBAD_REQUEST. ForVALIDATION_ERROR,error.detailsis an array of{ path, message }. - 401
UNAUTHORIZED: missing, invalid, expired or revoked credential. - 403
FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only. - 429
RATE_LIMITED: slow down and retry after theRetry-Afterseconds. - 500
INTERNAL_ERROR: unexpected failure. QuoteX-Request-Idto support.
Send a single SMS
POST/
- Access
- API key. Scoped keys need
sms:send. - Rate limit
- 10 requests per second
- Safe retries
- Send
X-Idempotency-Key; a replay within 24 hours returns the first response. - Real traffic
- Places a real call or sends a real message, billed to your balance. A test key simulates it against test credit.
Forwards the message to the route seller and returns straight away with a message id. status is the send-time outcome (accepted, sent or failed). The final outcome comes later from the carrier's delivery receipt, when the route returns one: read it with GET /comms/sms/{messageId} or subscribe to the sms.delivered and sms.failed webhooks. A message that fails on its receipt is refunded. Billed per segment at the route price plus the platform fee. A test key simulates the send against test credit.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
X-Idempotency-Key | header | string | Any unique string (a UUID is ideal). Replays within 24 hours return the first response instead of acting twice. |
Request body (application/json)
| Field | Type | Description |
|---|---|---|
torequired | string | Destination number (E.164) |
fromrequired | string | Sender ID: a number or an alphanumeric sender (letters, digits, spaces and . + _ -)max 30 chars |
routeId | string (uuid) | A route you purchased. Omit to let Smart Routing pick one |
strategy | string | Smart Routing strategy used when routeId is omittedOne of cheapest, best_quality, balanced |
messagerequired | string | Message body. Long messages are sent as concatenated segmentsmax 1600 chars |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object | - |
messageIdrequireddata.messageId | string | Use with GET /comms/sms/{messageId} |
torequireddata.to | string | - |
fromrequireddata.from | string | - |
statusrequireddata.status | string | Send-time outcome when we forwarded the message, not a handset delivery receiptOne of sent, delivered, failed, pending, accepted |
segmentsrequireddata.segments | integer | Billed segments (160 GSM-7 / 70 UCS-2 characters each, fewer when concatenated) |
costrequireddata.cost | money | USD as a decimal string with exactly 6 places, e.g. "0.012500". |
submittedAtrequireddata.submittedAt | string (date-time) | ISO-8601 timestamp (UTC) |
simulateddata.simulated | boolean | Present and true for test-key messages: nothing was sent |
networkdata.network | object | null | Present when the route prices SMS to this country per destination network. The network is determined from the number's range (ported numbers may be priced at the network the range belongs to) |
mccMncrequireddata.network.mccMnc | string | null | Mobile network code (MCC-MNC) the message was priced as, e.g. "234-10" |
operatorrequireddata.network.operator | string | null | Network name from public number-range data |
sourcerequireddata.network.source | string | range = number-range data; hlr = a live network lookup; none = not determinedOne of range, hlr, none |
rateBasisrequireddata.network.rateBasis | string | network = that network's own rate; all_operators = the route's rate for networks it does not list separately; country = the route's price for other or unknown networksOne of network, all_operators, country |
Errors
- 400
VALIDATION_ERROR,INVALID_INPUTorBAD_REQUEST. ForVALIDATION_ERROR,error.detailsis an array of{ path, message }. - 401
UNAUTHORIZED: missing, invalid, expired or revoked credential. - 403
FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only. - 409
CONFLICT(or a code-specific 409): the change clashes with existing state. - 429
RATE_LIMITED: slow down and retry after theRetry-Afterseconds. - 500
INTERNAL_ERROR: unexpected failure. QuoteX-Request-Idto support.
Get the delivery status and timeline of a sent SMS
GET/
- Access
- API key. Scoped keys need
sms:send. - Rate limit
- 100 requests per second (the default)
Returns the message's current status and its timeline: queued (charged), sent (the route accepted the hand-off), then delivered or failed, each with a timestamp, and an errorCode on failure.
Where delivered comes from. Only from a carrier delivery receipt. Receipts come back on SMPP routes whose supplier returns them, and on HTTP routes whose seller posts them back to us. A route that returns none leaves its messages at sent with awaitingReceipt: true; routeReturnsReceipts tells you whether the route has returned any in the last 30 days, so you know whether to wait. A receipt that reports the message undelivered, expired or rejected marks it failed and refunds it.
Covers messages sent with POST /comms/sms and POST /comms/sms/bulk. An unknown id answers 200 with status not_found rather than 404.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
messageIdrequired | path | string | - |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object | - |
messageIdrequireddata.messageId | string | - |
statusrequireddata.status | string | Current state: sent, accepted (test key), delivered or failed; not_found when the id is not on your account. delivered only ever comes from a carrier receipt |
todata.to | string | null | - |
fromdata.from | string | null | - |
segmentsdata.segments | integer | null | - |
errorCodedata.errorCode | string | null | Set when status is failed: SELLER_REJECTED (the route refused the hand-off), NO_ENDPOINT, UNDELIVERABLE, EXPIRED or REJECTED (from the carrier receipt) |
timelinedata.timeline | object[] | Every state the message has been in, oldest first |
statusrequireddata.timeline[].status | string | queued = charged and waiting to be handed off; sent = the route accepted it; accepted = simulated on a test key; delivered / failed = finalOne of queued, sent, accepted, delivered, failed |
atrequireddata.timeline[].at | string (date-time) | ISO-8601 timestamp (UTC) |
sourcerequireddata.timeline[].source | string | Where we learned it: our ledger, the hand-off to the route, the carrier's delivery receipt, or a test-key simulationOne of platform, submit, carrier_receipt, simulated |
errorCodedata.timeline[].errorCode | string | null | On failed: SELLER_REJECTED, NO_ENDPOINT, UNDELIVERABLE, EXPIRED or REJECTED |
carrierStatusdata.timeline[].carrierStatus | string | null | The carrier receipt's own status value, e.g. DELIVRD or UNDELIV |
carrierErrordata.timeline[].carrierError | string | null | The carrier receipt's own error value, when it sent one |
awaitingReceiptdata.awaitingReceipt | boolean | True while the message is sent and no carrier receipt has arrived. It stays true for good on a route that returns no receipts |
routeReturnsReceiptsdata.routeReturnsReceipts | boolean | null | Whether the route that carried this message has returned at least one carrier receipt in the last 30 days. null for test-key messages |
dlrSupporteddata.dlrSupported | boolean | True: the platform collects carrier delivery receipts. Whether one arrives for this message depends on the route (routeReturnsReceipts) |
simulateddata.simulated | boolean | Present and true for test-key messages: nothing was sent |
costdata.cost | money | The ledger amount of the charge (negative) USD as a decimal string with exactly 6 places, e.g. "0.012500". |
referencedata.reference | string | null | - |
sentAtdata.sentAt | string (date-time) | When the message was charged |
messagedata.message | string | Explanation, present when status is not_found |
Errors
- 400
VALIDATION_ERROR,INVALID_INPUTorBAD_REQUEST. ForVALIDATION_ERROR,error.detailsis an array of{ path, message }. - 401
UNAUTHORIZED: missing, invalid, expired or revoked credential. - 403
FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only. - 404
NOT_FOUND: no such resource on your account. - 429
RATE_LIMITED: slow down and retry after theRetry-Afterseconds. - 500
INTERNAL_ERROR: unexpected failure. QuoteX-Request-Idto support.
Send up to 1,000 SMS in one request
POST/
- Access
- API key. Scoped keys need
sms:send. - Rate limit
- 100 requests per second (the default)
- Real traffic
- Places a real call or sends a real message, billed to your balance. A test key simulates it against test credit.
Sends each message in order over one route and returns a result per recipient. Each message is billed exactly as POST /comms/sms bills it: a reseller sub-account API key pays the sub-account's retail rate, a test key simulates. Stops at the first insufficient-balance refusal (account or sub-account) and marks the rest skipped. Not idempotent: retrying resends every message.
Request body (application/json)
| Field | Type | Description |
|---|---|---|
routeIdrequired | string (uuid) | The purchased SMS route every message is sent over |
fromrequired | string | Sender ID for every messagemax 30 chars |
messagesrequired | object[] | - |
torequiredmessages[].to | string | -max 30 chars |
messagerequiredmessages[].message | string | -max 1600 chars |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object | - |
totalrequireddata.total | integer | - |
sentrequireddata.sent | integer | - |
failedrequireddata.failed | integer | Failed plus skipped |
totalCostrequireddata.totalCost | money | USD as a decimal string with exactly 6 places, e.g. "0.012500". |
resultsrequireddata.results | object[] | - |
torequireddata.results[].to | string | - |
statusrequireddata.results[].status | string | The send status, or failed / skipped |
messageIddata.results[].messageId | string | - |
fromdata.results[].from | string | - |
segmentsdata.results[].segments | integer | - |
costdata.results[].cost | money | US dollars as a decimal string with exactly 6 decimal places, e.g. "0.012500". Do money arithmetic with a decimal type, not floating point. USD as a decimal string with exactly 6 places, e.g. "0.012500". |
submittedAtdata.results[].submittedAt | string (date-time) | ISO-8601 timestamp (UTC) |
errordata.results[].error | string | - |
Errors
- 400
VALIDATION_ERROR,INVALID_INPUTorBAD_REQUEST. ForVALIDATION_ERROR,error.detailsis an array of{ path, message }. - 401
UNAUTHORIZED: missing, invalid, expired or revoked credential. - 403
FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only. - 409
CONFLICT(or a code-specific 409): the change clashes with existing state. - 429
RATE_LIMITED: slow down and retry after theRetry-Afterseconds. - 500
INTERNAL_ERROR: unexpected failure. QuoteX-Request-Idto support.