Skip to content
Markets open

API reference

Verify

One-time passcodes by SMS or voice call: send a code, then check what the user typed. We keep only a hash of the code.

5 operationsBase URL https://packetexchange.io/api/v1Postman collection

Read a one-time passcode to a phone by voice call

POST/api/v1/comms/voice-otp

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.

Calls to and, when answered, reads the code one digit at a time: "Your {brand} verification code is 4, 8, 2, 9, 1, 3. Again, 4, 8, 2, 9, 1, 3." Repeated repeat times (default 2, max 3), then the call hangs up. Pass your own code (4-10 digits) or let us generate one of length digits (default 6); a generated code is returned only when you set returnCode: true. You check the code yourself; use POST /verify/start instead to have us check it. Languages: en, es, fr, de, pt, hi. brand defaults to your company name and is cleaned to letters, digits and spaces (max 30). Returns as soon as the call is ringing; the outcome and cost arrive with the call.completed webhook and on GET /comms/voice-otp/{id}. Billed exactly like POST /comms/calls: the route rate per billing increment plus the platform fee. No text-to-speech surcharge. A test key simulates the call against test credit. Omit strategy and the code goes out over Smart Routing's best_quality route for the destination (plain calls and SMS default to balanced). Limits, shared by voice passcodes and the Verify API: 5 codes per number per hour, 30 seconds between codes to the same number, a per-account daily cap, and a cap on codes into one number range per hour. A limit answers 429 with details.retryAfterSeconds. Embargoed destinations, premium-rate / satellite / high-risk ranges and numbers on your Do-Not-Contact list are refused with 400.

Parameters

NameInTypeDescription
X-Idempotency-KeyheaderstringAny unique string (a UUID is ideal). Replays within 24 hours return the first response instead of acting twice.

Request body (application/json)

FieldTypeDescription
torequiredstring-
codestring-
lengthinteger-4 to 10
languagestring-
repeatinteger-1 to 3
fromstring-
strategystring-One of cheapest, best_quality, balanced
brandstringBrand read out / sent. Cleaned to letters, digits and spaces, max 30 charactersmax 120 chars
returnCodeboolean-

Response 200

FieldTypeDescription
datarequiredobject-
voiceOtpIdrequireddata.voiceOtpIdstring (uuid)Use with GET /comms/voice-otp/{id}
callIdrequireddata.callIdstringThe call carrying the code; it also appears in GET /comms/calls and the call.completed webhook
statusrequireddata.statusstringinitiated: the call is ringing. accepted: simulated on a test keyOne of initiated, accepted
torequireddata.tostring-
fromrequireddata.fromstringThe caller ID presented
languagerequireddata.languagestring-
codeLengthrequireddata.codeLengthinteger-
repeatrequireddata.repeatintegerHow many times the code is read
codedata.codestringPresent only when we generated the code AND you sent returnCode: true
costrequireddata.costmoney | nullTest keys: the simulated charge. Live: null here; the final cost arrives with call.completed and on GET USD as a decimal string with exactly 6 places, e.g. "0.012500".
simulateddata.simulatedbooleanPresent and true for test keys: nothing was dialled
createdAtrequireddata.createdAtstring (date-time)ISO-8601 timestamp (UTC)

Errors

  • 400VALIDATION_ERROR, INVALID_INPUT or BAD_REQUEST. For VALIDATION_ERROR, error.details is an array of { path, message }.
  • 401UNAUTHORIZED: missing, invalid, expired or revoked credential.
  • 403FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only.
  • 409CONFLICT (or a code-specific 409): the change clashes with existing state.
  • 429RATE_LIMITED: slow down and retry after the Retry-After seconds.
  • 500INTERNAL_ERROR: unexpected failure. Quote X-Request-Id to support.

Get the outcome of a voice passcode call

GET/api/v1/comms/voice-otp/{id}

Access
API key. Scoped keys need voice:send.
Rate limit
100 requests per second (the default)

Whether the call was answered, how long it ran and what it cost. Never returns the code.

Parameters

NameInTypeDescription
idrequiredpathstring (uuid)-

Response 200

FieldTypeDescription
datarequiredobject-
voiceOtpIdrequireddata.voiceOtpIdstring (uuid)-
callIdrequireddata.callIdstring | null-
statusrequireddata.statusstring-One of initiated, answered, no_answer, busy, failed, accepted
torequireddata.tostring-
languagerequireddata.languagestring-
codeLengthrequireddata.codeLengthinteger-
costrequireddata.costmoney | nullWhat the call cost you, platform fee included. Null until it ends USD as a decimal string with exactly 6 places, e.g. "0.012500".
durationSecondsrequireddata.durationSecondsinteger | null-
createdAtrequireddata.createdAtstring (date-time)ISO-8601 timestamp (UTC)
completedAtrequireddata.completedAtstring (date-time) | nullISO-8601 timestamp (UTC)
simulateddata.simulatedboolean-

Errors

  • 400VALIDATION_ERROR, INVALID_INPUT or BAD_REQUEST. For VALIDATION_ERROR, error.details is an array of { path, message }.
  • 401UNAUTHORIZED: missing, invalid, expired or revoked credential.
  • 403FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only.
  • 404NOT_FOUND: no such resource on your account.
  • 429RATE_LIMITED: slow down and retry after the Retry-After seconds.
  • 500INTERNAL_ERROR: unexpected failure. Quote X-Request-Id to support.

Send a verification code by SMS or voice call

POST/api/v1/verify/start

Access
API key. Scoped keys need verify:write.
Rate limit
10 requests per second
Safe retries
Send X-Idempotency-Key; a replay within 24 hours returns the first response.
Real traffic
Sends a real SMS or places a real voice call carrying the code, billed as that message or call. A test key simulates it.

Generates a code of length digits (default 6), sends it over channel, and returns a verificationId. We store only a keyed hash (HMAC) of the code; the code itself is never stored or logged. The code expires after expirySeconds (default 600). SMS languages: en, es, fr, de, pt, hi, ar. Voice languages: en, es, fr, de, pt, hi. from is the SMS sender ID (defaults to your brand) or, for voice, the caller ID. Billed as the SMS or call that carries the code: the route rate plus the platform fee, no verification fee. A test key sends nothing, charges test credit and returns testCode so you can finish the flow. Omit strategy and the code goes out over Smart Routing's best_quality route for the destination (plain calls and SMS default to balanced). Limits, shared by voice passcodes and the Verify API: 5 codes per number per hour, 30 seconds between codes to the same number, a per-account daily cap, and a cap on codes into one number range per hour. A limit answers 429 with details.retryAfterSeconds. Embargoed destinations, premium-rate / satellite / high-risk ranges and numbers on your Do-Not-Contact list are refused with 400.

Parameters

NameInTypeDescription
X-Idempotency-KeyheaderstringAny unique string (a UUID is ideal). Replays within 24 hours return the first response instead of acting twice.

Request body (application/json)

FieldTypeDescription
torequiredstring-
channelrequiredstring-One of sms, voice
lengthinteger-4 to 10
languagestring-
brandstringBrand read out / sent. Cleaned to letters, digits and spaces, max 30 charactersmax 120 chars
expirySecondsinteger-60 to 3600
fromstring-max 30 chars
strategystring-One of cheapest, best_quality, balanced

Response 200

FieldTypeDescription
datarequiredobject-
verificationIdrequireddata.verificationIdstring (uuid)Pass to POST /verify/check
torequireddata.tostring-
channelrequireddata.channelstring-One of sms, voice
statusrequireddata.statusstring-One of pending
expiresAtrequireddata.expiresAtstring (date-time)ISO-8601 timestamp (UTC)
maxAttemptsrequireddata.maxAttemptsintegerWrong guesses allowed before the verification is dead (5)
sendRefrequireddata.sendRefstringThe messageId (SMS) or callId (voice) that carried the code
sendStatusrequireddata.sendStatusstringSMS send-time status, or initiated for a voice call
simulateddata.simulatedbooleanPresent and true for test keys: nothing was sent
testCodedata.testCodestringTest keys ONLY: the code, so a sandbox can complete the check. Never present on a live key
createdAtrequireddata.createdAtstring (date-time)ISO-8601 timestamp (UTC)

Errors

  • 400VALIDATION_ERROR, INVALID_INPUT or BAD_REQUEST. For VALIDATION_ERROR, error.details is an array of { path, message }.
  • 401UNAUTHORIZED: missing, invalid, expired or revoked credential.
  • 403FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only.
  • 409CONFLICT (or a code-specific 409): the change clashes with existing state.
  • 429RATE_LIMITED: slow down and retry after the Retry-After seconds.
  • 500INTERNAL_ERROR: unexpected failure. Quote X-Request-Id to support.

Check the code a user entered

POST/api/v1/verify/check

Access
API key. Scoped keys need verify:write.
Rate limit
20 requests per second

approved exactly once: a code is single use, and a second check of an approved verification answers denied with reason already_used. Five attempts in total; the fifth wrong guess answers max_attempts and the verification is dead. After expiresAt every check answers expired, even with the right code. Spaces and dashes in the submitted code are ignored. The comparison is constant-time.

Request body (application/json)

FieldTypeDescription
verificationIdrequiredstring (uuid)-
coderequiredstring-max 32 chars

Response 200

FieldTypeDescription
datarequiredobject-
verificationIdrequireddata.verificationIdstring (uuid)-
statusrequireddata.statusstring-One of approved, denied, expired, max_attempts
attemptsRemainingrequireddata.attemptsRemaininginteger-
reasondata.reasonstringWhy a check was not approved, when it is not obvious from statusOne of wrong_code, already_used, not_pending

Errors

  • 400VALIDATION_ERROR, INVALID_INPUT or BAD_REQUEST. For VALIDATION_ERROR, error.details is an array of { path, message }.
  • 401UNAUTHORIZED: missing, invalid, expired or revoked credential.
  • 403FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only.
  • 409CONFLICT (or a code-specific 409): the change clashes with existing state.
  • 429RATE_LIMITED: slow down and retry after the Retry-After seconds.
  • 500INTERNAL_ERROR: unexpected failure. Quote X-Request-Id to support.

Get the state of a verification

GET/api/v1/verify/{id}

Access
API key. Scoped keys need verify:write.
Rate limit
100 requests per second (the default)

Status, attempts used and how the code was delivered. Never returns the code.

Parameters

NameInTypeDescription
idrequiredpathstring (uuid)-

Response 200

FieldTypeDescription
datarequiredobject-
verificationIdrequireddata.verificationIdstring (uuid)-
torequireddata.tostring-
channelrequireddata.channelstring-One of sms, voice
statusrequireddata.statusstring-One of pending, approved, expired, max_attempts, failed
attemptsrequireddata.attemptsinteger-
maxAttemptsrequireddata.maxAttemptsinteger-
expiresAtrequireddata.expiresAtstring (date-time)ISO-8601 timestamp (UTC)
createdAtrequireddata.createdAtstring (date-time)ISO-8601 timestamp (UTC)
approvedAtrequireddata.approvedAtstring (date-time) | nullISO-8601 timestamp (UTC)
sendRefrequireddata.sendRefstring | null-
sendStatusrequireddata.sendStatusstring | null-
simulateddata.simulatedboolean-

Errors

  • 400VALIDATION_ERROR, INVALID_INPUT or BAD_REQUEST. For VALIDATION_ERROR, error.details is an array of { path, message }.
  • 401UNAUTHORIZED: missing, invalid, expired or revoked credential.
  • 403FORBIDDEN: not allowed - a scoped key lacks the scope, or the endpoint is session-only.
  • 404NOT_FOUND: no such resource on your account.
  • 429RATE_LIMITED: slow down and retry after the Retry-After seconds.
  • 500INTERNAL_ERROR: unexpected failure. Quote X-Request-Id to support.