API reference
Route checks
Check up to three routes with a short real call before you buy them, then buy the ones that passed with the routing order set.
7 operationsBase URL https://packetexchange.io/api/v1Postman collection
BrowseRoute checks
Check up to three routes before buying
POST/
- Access
- API key. Scoped keys need
voice:send. - Rate limit
- 100 requests per second (the default)
Places one short real call over each route (1 to 3, in your order: the first is your primary, the rest backups) and reports whether it works. Returns 202 with a checkId at once; poll GET /routes/check/{checkId}.
- network (default): the call goes to a public line that answers by itself (a speaking clock or information line) in the destination, chosen from the ones the route's rates cover and, where we have one, of the same line type as number (mobile or fixed). The call is answered, held about two seconds and ended. Where no such line exists yet for a destination, a lighter check runs instead: it confirms only that the route accepts calls, places no call to anyone, and says so (accepts). - ring_me: the route calls number, which must be your own phone in a country the route serves. Ringing counts as working. Satellite and premium-rate ranges are refused; one number can be rung by checks 3 times an hour, and an account can ring 3 different numbers a day (429).
number (network mode) narrows the check to a destination: a dial code (92) or a full number (+923001234567); for an A-Z rate sheet pass it, or the route's own country is used.
Billing: each connected leg is charged at the route's rate (your own effective rate for that number) for the billable seconds, plus the normal platform fee, from your test credit first and then your balance (paidFrom says which). Unanswered legs and suspected false answers cost nothing; a check of your own listing is free. A funded balance is required (400 otherwise). Limits: 10 checks a minute and 60 an hour per account (429); when the limiter cannot be reached checks pause (503) rather than run unlimited. Send X-Idempotency-Key to make a retry safe: the retry returns the first check instead of placing a second. Blended routes cannot be checked yet (400). A test key returns a simulated result immediately: no call, no charge.
You may check routes you have not bought. Private routes need an access grant.
Request body (application/json)
| Field | Type | Description |
|---|---|---|
routeIdsrequired | string (uuid)[] | 1 to 3 route ids, in your order: the first is your primary, the rest backups |
number | string | network mode: a dial code or full number to check for (narrows an A-Z rate sheet to that destination and line type). ring_me mode: your own number, requiredmax 32 chars |
mode | string | network = our test lines (default); ring_me = the route calls your own numberdefault "network"One of network, ring_me |
Response 202
| Field | Type | Description |
|---|---|---|
datarequired | object | - |
checkIdrequireddata.checkId | string (uuid) | - |
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. - Also
RATE_LIMITED,SERVICE_UNAVAILABLE
Send a test SMS over an SMS route
POST/
- Access
- API key. Scoped keys need
sms:send. - Rate limit
- 100 requests per second (the default)
Sends ONE short fixed message over exactly this SMS listing (never Smart Routing) to your own mobile, by the same transport a real message uses, and reports what happened. Returns 202 with a checkId at once; poll GET /routes/check/{checkId} (the test is its only route). The text is fixed and so is the sender: "PacketExchange test message: this route delivered to your phone. Ref K7QD", from a platform sender chosen by the route's sender rules (numeric on a route that carries numeric senders only).
Results: accepted (the route took it; no carrier receipt yet), delivered (a carrier receipt confirmed it) or refused (the route refused it, or the receipt said it failed; errorCode says which). Receipts usually arrive within a minute; on a route that returns none the test stays accepted, and routeReturnsReceipts says whether the route has returned any lately. A later receipt updates the test, so it also appears in GET /routes/checks.
number must be a mobile in international format that the route covers (by its rate sheet or its prefixes, longest prefix first); a number it does not cover is refused (400), as are premium-rate, satellite, special-service and short-code ranges, embargoed destinations and numbers on your Do-Not-Contact list.
Billing: the route's normal price for one message for that number (your own effective rate), plus the normal platform fee, from your test credit first and then your balance (paidFrom). A message the route refuses is not charged, and one whose receipt says it failed is refunded. Testing your own listing is free. Limits: 3 test messages to one number an hour (across all accounts), 5 different numbers and 20 test messages per account a day (429); when the limiter cannot be reached tests pause (503). Send X-Idempotency-Key to make a retry safe. A test key returns a simulated result at once: nothing is sent and nothing is charged.
Test messages never count in SMS volume, revenue or quality figures. A buyer's delivered test counts as working and a refused one as a failure on the listing's check health, which the seller sees without learning who ran it.
Request body (application/json)
| Field | Type | Description |
|---|---|---|
routeIdrequired | string (uuid) | The SMS listing to test (a marketplace route id) |
numberrequired | string | Your own mobile number in international format (E.164), e.g. +35056012345. The route must cover itmax 32 chars |
Response 202
| Field | Type | Description |
|---|---|---|
datarequired | object | - |
checkIdrequireddata.checkId | string (uuid) | - |
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. - Also
RATE_LIMITED,SERVICE_UNAVAILABLE
Is there a test line for these routes?
GET/
- Access
- API key. Scoped keys need
routes:read. - Rate limit
- 100 requests per second (the default)
For each route (1 to 3), whether we have a registered public test line for its destination (or for number's destination on an A-Z rate sheet). When there is none, check the route with mode: ring_me and your own number. Says yes or no only.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
routeIdsrequired | query | object | Comma-separated route ids (1 to 3) |
number | query | string | Optional dial code or number, to ask about one destination of an A-Z rate sheet |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object[] | - |
routeIdrequireddata[].routeId | string (uuid) | - |
testLinerequireddata[].testLine | boolean | - |
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.
Get a route check's results
GET/
- Access
- API key. Scoped keys need
routes:read. - Rate limit
- 100 requests per second (the default)
Per-route verdicts, in your order. message is the sentence to show a person. Routes are named without seller details.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | - |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object | - |
idrequireddata.id | string (uuid) | - |
statusrequireddata.status | string | Poll GET /routes/check/{id} until done (usually under 40 seconds)One of running, done |
moderequireddata.mode | string | sms = a test message (POST /routes/check/sms)One of network, ring_me, sms |
simulatedrequireddata.simulated | boolean | True for a test key: no call was placed and nothing was charged |
numberrequireddata.number | string | null | The number or dial code you checked for (+digits) |
createdAtrequireddata.createdAt | string (date-time) | ISO-8601 timestamp (UTC) |
completedAtrequireddata.completedAt | string (date-time) | null | ISO-8601 timestamp (UTC) |
totalCostrequireddata.totalCost | money | USD as a decimal string with exactly 6 places, e.g. "0.012500". |
routesrequireddata.routes | object[] | - |
positionrequireddata.routes[].position | integer | 0 = your first choice (primary), then backups |
rolerequireddata.routes[].role | string | -One of primary, backup |
routeIdrequireddata.routes[].routeId | string (uuid) | - |
routeNamerequireddata.routes[].routeName | string | Cleaned of seller names and contact details |
countryrequireddata.routes[].country | string | - |
ownRouterequireddata.routes[].ownRoute | boolean | A listing you sell yourself (checked free) |
resultrequireddata.routes[].result | string | pending/checking = in progress. working = the call was answered: the route delivers. no_capacity = the route refused the call or did not respond. not_delivered = the route signalled ringing but the call never reached the number. false_answer = the route answered faster than a real line can be reached (under 1.5 s from our INVITE, or under 2.5 s with no ringing): it almost certainly answered the call itself. Not working, and not charged. inconclusive = no verdict (our test lines were busy, the number was reported invalid, or the check was interrupted); nothing counted against the route. accepts = the lighter check for a destination without a full check yet: the route accepts calls. Test SMS only: accepted = the route took the message and no carrier receipt has arrived yet (on a route that returns none it stays accepted); delivered = a carrier receipt confirmed delivery to the phone; refused = the route refused the message, or the carrier receipt said it failed (see errorCode). A refused message is not charged.One of pending, checking, working, no_capacity, not_delivered, false_answer, inconclusive, accepts, accepted, delivered, refused |
messagerequireddata.routes[].message | string | The plain-English verdict to show, e.g. "This route is working for your needs." |
checkTyperequireddata.routes[].checkType | string | null | full = a call to one of our test lines; light = acceptance only; ring_me = your own number; sms = a test message to your own mobile; simulated = test keyOne of full, light, ring_me, sms, simulated |
destinationrequireddata.routes[].destination | string | null | The country the check call went to |
lineTyperequireddata.routes[].lineType | string | null | Line type of the test line used (full checks)One of fixed, mobile |
rangrequireddata.routes[].rang | boolean | null | - |
answeredrequireddata.routes[].answered | boolean | null | - |
pddMsrequireddata.routes[].pddMs | integer | null | Post-dial delay: dial to first ring (or answer) |
sipCoderequireddata.routes[].sipCode | integer | null | Final SIP response from the route, when one came back |
billableSecondsrequireddata.routes[].billableSeconds | integer | - |
costrequireddata.routes[].cost | money | What this leg cost you (route rate for the connected seconds, plus the platform fee) USD as a decimal string with exactly 6 places, e.g. "0.012500". |
paidFromrequireddata.routes[].paidFrom | string | null | Which wallet paid: test credit is used first, then your balance. Null when nothing was chargedOne of test_credit, balance, mixed |
answerMsrequireddata.routes[].answerMs | integer | null | Milliseconds from our INVITE to the answer, when it answered (the false-answer test) |
completedAtrequireddata.routes[].completedAt | string (date-time) | null | ISO-8601 timestamp (UTC) |
messageRefrequireddata.routes[].messageRef | string | null | Test SMS: the 4-character reference printed at the end of the message, so you can find it on your phone |
senderrequireddata.routes[].sender | string | null | Test SMS: the sender the platform used (set from the route's sender rules; you cannot choose it) |
errorCoderequireddata.routes[].errorCode | string | null | Test SMS refused: SELLER_REJECTED = the route refused the message; NO_ENDPOINT = the route has no delivery endpoint; UNDELIVERABLE, EXPIRED, REJECTED = the carrier receipt said it failed. The same codes as GET /comms/sms/{id}One of SELLER_REJECTED, NO_ENDPOINT, UNDELIVERABLE, EXPIRED, REJECTED |
carrierStatusrequireddata.routes[].carrierStatus | string | null | Test SMS: the carrier receipt's own stat value (DELIVRD, UNDELIV, ...), when a receipt arrived |
carrierErrorrequireddata.routes[].carrierError | string | null | Test SMS: the carrier receipt's own err value, when it carried one |
acceptedAtrequireddata.routes[].acceptedAt | string (date-time) | null | Test SMS: when the route accepted the message |
receiptAtrequireddata.routes[].receiptAt | string (date-time) | null | Test SMS: when the carrier receipt arrived |
routeReturnsReceiptsrequireddata.routes[].routeReturnsReceipts | boolean | null | Test SMS still accepted: whether this route has returned any carrier receipt in the last 30 days. false = a confirmation may never come; check your phone |
workingrequireddata.working | string (uuid)[] | Route ids that passed (working or accepts; delivered for a test SMS), in your order |
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.
List your recent route checks
GET/
- Access
- API key. Scoped keys need
routes:read. - Rate limit
- 100 requests per second (the default)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
limit | query | integer | -Default 20 |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object[] | Same fields as RouteCheck, shown earlier on this page. |
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.
Buy the routes that passed a check and set the routing order
POST/
- Access
- API key. Scoped keys need
purchases:write. - Rate limit
- 100 requests per second (the default)
Buys every route in the check that came back working, in your order, and puts them at the top of your routing order: the first is the primary, the rest its backups (your other ranked routes keep their relative order below them). Routes you already bought are reused. A route that cannot be bought (for example email not verified, a private route, or a US route that needs compliance review) is reported in purchases[].error and does not stop the others. Needs a live key.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | - |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object | - |
purchasesrequireddata.purchases | object[] | - |
routeIdrequireddata.purchases[].routeId | string (uuid) | - |
routeNamerequireddata.purchases[].routeName | string | - |
purchaseIdrequireddata.purchases[].purchaseId | string (uuid) | null | - |
statusrequireddata.purchases[].status | string | null | The purchase status (active, or pending_review for a route that needs review) |
alreadyOwnedrequireddata.purchases[].alreadyOwned | boolean | - |
errorrequireddata.purchases[].error | string | null | - |
routingOrderrequireddata.routingOrder | object[] | - |
positionrequireddata.routingOrder[].position | integer | 1 = primary |
rolerequireddata.routingOrder[].role | string | -One of primary, backup |
purchaseIdrequireddata.routingOrder[].purchaseId | string (uuid) | - |
routeIdrequireddata.routingOrder[].routeId | string (uuid) | - |
routeNamerequireddata.routingOrder[].routeName | 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. - 404
NOT_FOUND: no such resource on your account. - 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.
Check health of a route you sell
GET/
- Access
- API key. Scoped keys need
routes:read. - Rate limit
- 100 requests per second (the default)
For your own listing: the last 24 hours of full route checks (working vs failed), the recent checks, and any suspension made by checks with its reason and evidence. A listing whose checks fail 10 or more times in 24 hours with none working is confirmed once more and then suspended from the marketplace; run POST /routes/check with its id (free for your own listing) after fixing it, and a working result puts it back. A call answered under 1.5 s after our INVITE (or under 2.5 s with no ringing) is a false answer and counts as a failure; answerMs is the evidence. Your own checks, checks to a buyer's own number (ring_me) and lighter checks never count. On an SMS listing the window counts buyers' test messages instead: delivered is working, refused is a failure, a message still awaiting a receipt does not count, and an SMS listing is never suspended automatically.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | - |
Response 200
| Field | Type | Description |
|---|---|---|
datarequired | object | - |
windowrequireddata.window | object | - |
hoursrequireddata.window.hours | integer | - |
workingrequireddata.window.working | integer | - |
failedrequireddata.window.failed | integer | - |
totalrequireddata.window.total | integer | - |
suspendAfterrequireddata.window.suspendAfter | integer | Failures (with none working) that trigger the confirmation check |
suspensionrequireddata.suspension | object | null | - |
statusrequireddata.suspension.status | string | notice = an A-Z rate sheet warned for one country; nothing was suspendedOne of suspended, notice |
sincerequireddata.suspension.since | string (date-time) | ISO-8601 timestamp (UTC) |
reasonrequireddata.suspension.reason | string | - |
destinationrequireddata.suspension.destination | string | null | - |
evidencerequireddata.suspension.evidence | object | Counts, SIP codes, causes and times of the failed checks |
lastReinstatedAtrequireddata.lastReinstatedAt | string (date-time) | null | ISO-8601 timestamp (UTC) |
recentrequireddata.recent | object[] | - |
atrequireddata.recent[].at | string (date-time) | null | ISO-8601 timestamp (UTC) |
resultrequireddata.recent[].result | string | pending/checking = in progress. working = the call was answered: the route delivers. no_capacity = the route refused the call or did not respond. not_delivered = the route signalled ringing but the call never reached the number. false_answer = the route answered faster than a real line can be reached (under 1.5 s from our INVITE, or under 2.5 s with no ringing): it almost certainly answered the call itself. Not working, and not charged. inconclusive = no verdict (our test lines were busy, the number was reported invalid, or the check was interrupted); nothing counted against the route. accepts = the lighter check for a destination without a full check yet: the route accepts calls. Test SMS only: accepted = the route took the message and no carrier receipt has arrived yet (on a route that returns none it stays accepted); delivered = a carrier receipt confirmed delivery to the phone; refused = the route refused the message, or the carrier receipt said it failed (see errorCode). A refused message is not charged.One of pending, checking, working, no_capacity, not_delivered, false_answer, inconclusive, accepts, accepted, delivered, refused |
checkTyperequireddata.recent[].checkType | string | sms = a test message on an SMS listingOne of full, light, sms |
errorCoderequireddata.recent[].errorCode | string | null | A test message's refusal code; null on calls |
sipCoderequireddata.recent[].sipCode | integer | null | - |
causerequireddata.recent[].cause | string | null | - |
destinationrequireddata.recent[].destination | string | null | - |
byYourequireddata.recent[].byYou | boolean | - |
runByrequireddata.recent[].runBy | string | Who ran it: you, a buyer (never named), or the platform (confirmation / new test line)One of you, buyer, platform |
answerMsrequireddata.recent[].answerMs | integer | null | Milliseconds from our INVITE to the answer |
rangrequireddata.recent[].rang | boolean | 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.