Skip to content
Menu

Build with Switchgear

API reference

Find each endpoint’s parameters, request body, and responses. This reference is generated from Switchgear’s OpenAPI contract, the same source used by the API and SDK. For a first send, follow getting started, or download the OpenAPI 3.1 specification to generate a client.

API essentials

Sandboxhttps://sandbox.switchgear.app

Use sg_test_ keys. Test against simulators without sending real messages or incurring charges.

Productionhttps://api.switchgear.app

Use sg_live_ keys after business approval and channel activation. Live usage is billed.

Authenticate every request

Send Authorization: Bearer <key>. Each key identifies one project environment and carries its own scopes. Required scopes are listed in the endpoint descriptions. Use GET /v1/me to check your key’s project, environment, and permissions:

Check your sandbox key · curl
curl https://sandbox.switchgear.app/v1/me \
  -H "Authorization: Bearer $SWITCHGEAR_KEY"

Make retries safe

Send an Idempotency-Key for endpoints that require one. Use a fresh value, such as a UUID, for each new operation. A retry with the same key and body returns the original response for 24 hours. Replayed message responses include Idempotent-Replayed: true.

A reused key with a different body, or one outside its replay window, returns 409. If a send returns 503 after being stored, retry with the same key so it can be queued safely.

Read responses and errors

For message sends, 202 Accepted means queued. Follow delivery through GET /v1/messages/{id} or signed webhooks. Error responses contain an error object with a stable code, a human-readable message, and details when available. Use the code, rather than the message text, in your application logic.

Each endpoint lists its success and error responses below. For common fixes, see quickstart troubleshooting.

Handle rate limits

Limits apply per API key, per minute. Defaults are 1,200 requests in sandbox and 6,000 in production; a key can have a custom limit. Each message in a batch counts as one request.

A limit response uses 429 rate_limited. Wait for Retry-After when supplied. A batch that exceeds the key’s entire limit cannot fit in any window: split it into smaller batches.

Trace a request

Every response includes X-Request-Id. You can supply your own value using 1–128 letters, digits, or . _ : -; otherwise the API creates one. Related webhook deliveries carry it too. Include this ID when contacting Switchgear support.

Authentication

GET/v1/me

The project, environment and scopes this API key resolves to

Responses

  • 200The key is valid · Me
  • 401Missing or invalid key · ErrorEnvelope

API keys

POST/v1/keys/{id}/rotate

Rotate this API key

A key rotates itself: id is the calling key's id (key_id from GET /v1/me); other keys are managed in the business portal. No scope is needed. The answer holds the new key, shown once, with the same scopes and rate limit. The old key keeps working for overlap_hours (default 24, at most 168; 0 ends it now), so deploy the new key within that window; then it is revoked. A key can be rotated once.

POST /v1/keys/{id}/rotate parameters
FieldTypeDescription
idrequiredstring (uuid) · path

Request body (RotateApiKeyRequest)

POST /v1/keys/{id}/rotate request body
FieldTypeDescription
overlap_hoursinteger
namestring

Responses

  • 201The new key (key is its plaintext, shown once) and the old one with its new expires_at · RotateApiKeyResponse
  • 400An overlap outside 0–168 hours · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 404id is not the calling key · ErrorEnvelope
  • 409This key was already rotated (invalid_transition) · ErrorEnvelope

Messages

POST/v1/messages

Send one message

Accepts the message once it is durably queued (scope messages:send). Delivery is reported through message.* events and GET /v1/messages/{id}. Send body, or template (a key) with variables: the newest approved version for the channel and locale is rendered, and every placeholder is required. Tag OTP messages otp to send them ahead of everything else, and campaigns marketing to send them behind. When the environment has quiet hours (business portal → Suppressions), a message tagged marketing that falls due inside them waits until they end in the recipient's local time; scheduled_at says until when. Messages also tagged otp or transactional never wait. Bulk email (tagged marketing, or sent in a batch unless the environment is set to marketing only) carries a one-click unsubscribe: List-Unsubscribe and List-Unsubscribe-Post headers, and a link at the foot of the email layout. An unsubscribe adds a bulk suppression and emits email.unsubscribed; email tagged otp or transactional never carries it and is never stopped by it. A push may carry push.fallback ({ sms } or { email }): if it reaches no device, the same message is sent there once, as a message of its own, billed at that channel's price. The fallback must be routable when the push is accepted. WhatsApp: whatsapp.media sends an image, document, audio or video by https url or by the media_id of an upload (POST /v1/media), with body as its caption; with a template that has a media header, it is the header's file. Without a template, a WhatsApp message is free-form, which Meta allows only within 24 hours of the person's last message (outside_service_window otherwise) and which is billed as a service conversation. The simulator fails numbers ending in 00.

POST /v1/messages parameters
FieldTypeDescription
Idempotency-Keyrequiredstring · headerUnique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours.

Request body (SendMessageRequest)

POST /v1/messages request body
FieldTypeDescription
channelrequiredone of "sms", "email", "whatsapp", "push"
torequiredstring
templatestring
variablesobject
localestring
bodystring
subjectstring
htmlstring
sender_idstring
tagsarray of string
schedule_atstring (date-time)
metadataobject
pushobject
.titlestring
.imagestring (uri)
.deep_linkstring
.dataobject
.priorityone of "high", "normal"
.ttlinteger
.collapse_keystring
.badgeinteger
.soundstring
.android_channel_idstring
.silentboolean
.fallbackobject
.smsstring
.emailstring (email)
.htmlstring
whatsappobject
.mediaobject
.kindrequiredone of "image", "document", "audio", "video"
.urlstring (uri)
.media_idstring (uuid)
.filenamestring

Responses

  • 202Queued. On a replay the response carries Idempotent-Replayed: true. · Message
  • 400Invalid body (validation_failed) or no Idempotency-Key (idempotency_key_missing) · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 402Production and staging: the wallet can’t cover the estimated price (insufficient_balance; prepaid stops at zero, postpaid at the credit limit). Nothing is stored; top up and retry with the same Idempotency-Key · ErrorEnvelope
  • 403The key lacks messages:send, or the account is suspended · ErrorEnvelope
  • 409The Idempotency-Key was used with a different body, or more than 24 hours ago · ErrorEnvelope
  • 413The body is over 100 KB (payload_too_large) · ErrorEnvelope
  • 415WhatsApp: the uploaded file is not a type WhatsApp sends as that kind (unsupported_media_type) · ErrorEnvelope
  • 422The destination is suppressed (suppressed_destination), no routing rule matches (no_route), the template is missing (template_not_found) or not approved (template_not_approved), or the project has reached its monthly budget and the send is marketing or a batch item (budget_exceeded; OTP and transactional sends never are). WhatsApp: free-form outside the customer-service window (outside_service_window), or a media_id that is not an upload of this environment's or has expired (media_not_found) · ErrorEnvelope
  • 429Over the key's rate limit (rate_limited, with Retry-After) · ErrorEnvelope
  • 503Stored but not queued; retry with the same Idempotency-Key · ErrorEnvelope

GET/v1/messages

List messages

Scope messages:read. Newest first; pass next_cursor as cursor for older messages. Never the destination or body: destination_hint is the tail of a keyed hash of the destination, as in the business portal, so recipients can be told apart without being shown. GET /v1/messages/{id} has the events.

GET /v1/messages parameters
FieldTypeDescription
statusstring · queryComma-separated, any of: queued, sent, delivered, failed, bounced, expired
channelstring · queryComma-separated, any of: sms, email, whatsapp, push
tagstring · queryOnly messages sent with this tag
sincestring (date-time) · queryCreated at or after
untilstring (date-time) · queryCreated before
limitinteger · query
cursorstring (uuid) · query

Responses

  • 200A page of messages · MessageList
  • 400An unknown status or channel, since not before until, or a cursor that is not a message in this environment · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks messages:read · ErrorEnvelope

POST/v1/messages/batch

Send up to 1,000 messages

Scope messages:send. Up to 1000 items in a body of up to 5 MB, each a POST /v1/messages body. Every item is validated, checked against the suppression list, routed, held against the wallet and queued on its own: results has one entry per item, in order, with the queued message or that item's error (the codes POST /v1/messages would answer with), so one bad item never fails the rest. Item *i* is stored under the Idempotency-Key <key> <i>: resend the same batch with the same key and the items already accepted replay (replayed: true) while the others are tried again, so keep the items in the same order. A request-level error (below) means no item was looked at.

POST /v1/messages/batch parameters
FieldTypeDescription
Idempotency-Keyrequiredstring · headerUnique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours.

Request body (SendMessageBatchRequest)

POST /v1/messages/batch request body
FieldTypeDescription
messagesrequiredarray of SendMessageRequestEach item is a POST /v1/messages body, validated on its own.
.channelrequiredone of "sms", "email", "whatsapp", "push"
.torequiredstring
.templatestring
.variablesobject
.localestring
.bodystring
.subjectstring
.htmlstring
.sender_idstring
.tagsarray of string
.schedule_atstring (date-time)
.metadataobject
.pushobject
.titlestring
.imagestring (uri)
.deep_linkstring
.dataobject
.priorityone of "high", "normal"
.ttlinteger
.collapse_keystring
.badgeinteger
.soundstring
.android_channel_idstring
.silentboolean
.fallbackobject
.whatsappobject
.mediaobject

Responses

  • 200Every item has a result · MessageBatchResponse
  • 400No messages array, an empty one or more than 1,000 items (validation_failed), or no Idempotency-Key (idempotency_key_missing) · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks messages:send, or the account is suspended · ErrorEnvelope
  • 413The body is over 5 MB (payload_too_large); split the batch · ErrorEnvelope
  • 429The batch's messages don't fit in what is left of the key's rate limit this minute (rate_limited, with Retry-After), or never could (no Retry-After; split it). Nothing was sent · ErrorEnvelope

GET/v1/messages/{id}

Current status and event history of a message

Scope messages:read. Only messages in the API key's environment are visible.

GET /v1/messages/{id} parameters
FieldTypeDescription
idrequiredstring (uuid) · path

Responses

  • 200The message · MessageDetail
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks messages:read · ErrorEnvelope
  • 404No such message in this environment · ErrorEnvelope

Templates

POST/v1/templates

Save a template

Scope templates:write. The first save of a key, channel and locale creates the template; each later save adds a version. Every {{variable}} in the body (and the email subject) is required when sending. Versions are approved on save, except WhatsApp, which waits for Meta. WhatsApp header gives the template a media header (image, document or video) with a sample file Meta's reviewers see (sample_media_id, an upload); each send then gives its own file in whatsapp.media. Category AUTHENTICATION takes exactly one variable, {{code}}: Meta sends its own text with a copy-code button, and POST /v1/otp/send on WhatsApp uses it.

POST /v1/templates parameters
FieldTypeDescription
Idempotency-Keyrequiredstring · headerUnique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours.

Request body (CreateTemplateRequest)

POST /v1/templates request body
FieldTypeDescription
keyrequiredstring
channelrequiredone of "sms", "email", "whatsapp", "push"
localestring
bodyrequiredstring
subjectstring
htmlstring
descriptionstring
categoryone of "UTILITY", "MARKETING", "AUTHENTICATION"
examplesobject
headerobject
.formatrequiredone of "image", "document", "video"
.sample_media_idrequiredstring (uuid)

Responses

  • 201The saved version. On a replay the response carries Idempotent-Replayed: true. · Template
  • 400Invalid body or placeholders, or no Idempotency-Key · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks templates:write · ErrorEnvelope
  • 409The Idempotency-Key was used with a different body, or more than 24 hours ago · ErrorEnvelope

GET/v1/templates

List templates with their newest version

Scope templates:read. Oldest first; pass next_cursor as cursor for the next page.

GET /v1/templates parameters
FieldTypeDescription
channelstring · query
keystring · query
localestring · query
limitinteger · query
cursorstring (uuid) · query

Responses

  • 200A page of templates · TemplateList
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks templates:read · ErrorEnvelope

One-time codes

POST/v1/otp/send

Issue a one-time code

Scope otp:send. Sends a code through the messaging pipeline with tag otp (so a routing rule can prefer a fast route), using the environment's otp template when it has one. Length, TTL, attempts and resend cooldown default to the environment's settings (6 digits, 5 minutes, 3 attempts, 60 s). The code is never returned; keep id to verify.

POST /v1/otp/send parameters
FieldTypeDescription
Idempotency-Keyrequiredstring · headerUnique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours.

Request body (SendOtpRequest)

POST /v1/otp/send request body
FieldTypeDescription
destinationrequiredstring
channelone of "sms", "whatsapp", "email"
lengthinteger
ttlinteger
templatestring
localestring
metadataobject

Responses

  • 202The challenge; the code is queued for delivery. On a replay the response carries Idempotent-Replayed: true. · OtpChallenge
  • 400Invalid body, or no Idempotency-Key · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 402Production and staging: the wallet can’t cover the code’s price (insufficient_balance) · ErrorEnvelope
  • 403The key lacks otp:send · ErrorEnvelope
  • 409The Idempotency-Key was used with a different body, or more than 24 hours ago · ErrorEnvelope
  • 422The destination is suppressed (suppressed_destination), no routing rule matches (no_route), or the named template is missing or not approved · ErrorEnvelope
  • 429Inside the resend cooldown for this destination (rate_limited, with Retry-After) · ErrorEnvelope

POST/v1/otp/verify

Check a code

Scope otp:verify. Every attempt answers 200 with a result: verified (emits otp.verified), invalid_code (attempts remain), too_many_attempts, expired (emits otp.expired) or already_verified. A challenge is bound to its destination, so a code sent to one number cannot verify another.

Request body (VerifyOtpRequest)

POST /v1/otp/verify request body
FieldTypeDescription
challenge_idrequiredstring (uuid)
coderequiredstring

Responses

  • 200The result of this attempt · VerifyOtpResponse
  • 400Invalid body · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks otp:verify · ErrorEnvelope
  • 404No such challenge in this environment · ErrorEnvelope

Identity verification

POST/v1/verifications

Start an identity check

Scope verifications:create. type nrc_lookup matches an NRC number and name against the registry; document reads and checks an identity document; liveness checks a live person took 1 to 8 selfie frames (pass, fail or inconclusive, with a score); face_match compares a selfie with a reference photo (passing at 85% similarity unless the environment changes it). Images are base64, up to 5 MB each and 10 MB together, kept as encrypted evidence. kyc_basic runs the environment's steps in order (by default document, face match, liveness and NRC lookup; the business portal sets which, in what order, and which stop the flow when they fail) from one subject: the document (also the face match's reference), the selfie frames (the first is the face match's selfie), and nrc, or an NRC document's expected number and names. It gives one decision: any step failing fails it, a step that couldn't run leaves it failed (step_failed), any review is review. steps and its events carry every step, and each completed step is billed at its own type's rate. Answers 202 with status: pending; the result arrives as verification.completed (with decision pass, review or fail against the environment's thresholds) or verification.failed. callback_url must be one of the environment's webhook endpoints; its events go there too. Sandbox runs against the simulator: NRC and document numbers ending /10/1 match, /10/2 are not found (a tampered document), /10/3 partially match, /10/9 fail; selfies pass and match unless an image carries switchgear-test: with liveness=fail, inconclusive or error, or face_match=no_match, borderline or error (the SDK exports such images as VERIFICATION_TEST_IMAGES).

POST /v1/verifications parameters
FieldTypeDescription
Idempotency-Keyrequiredstring · headerUnique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours.

Request body (CreateVerificationRequest)

POST /v1/verifications request body
FieldTypeDescription
typerequired"nrc_lookup"
subjectrequiredobject
.nrcrequiredstring
.first_namerequiredstring
.last_namerequiredstring
.date_of_birthstring (date)
callback_urlstring (uri)
metadataobject
.document_typerequiredone of "nrc", "passport", "drivers_licence"
.front_imagerequiredstring
.back_imagestring
.content_typerequiredone of "image/jpeg", "image/png"
.expectedobject
.document_numberstring
.first_namestring
.last_namestring
.date_of_birthstring (date)
.framesarray of string
.challengestring
.stepsarray of object
.actionrequiredone of "blink", "turn_left", "turn_right", "smile"
.framesrequiredarray of string
.times_msrequiredarray of integer
.selfierequiredstring
.reference_imagerequiredstring
.documentrequiredobject
.document_typerequiredone of "nrc", "passport", "drivers_licence"
.front_imagerequiredstring
.back_imagestring
.content_typerequiredone of "image/jpeg", "image/png"
.expectedobject
.nrcrequiredstring

Responses

  • 202The pending verification On a replay the response carries Idempotent-Replayed: true. · Verification
  • 400Invalid body (including images over 10 MB together, or a kyc_basic without the NRC its NRC lookup needs), or no Idempotency-Key · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks verifications:create · ErrorEnvelope
  • 409The Idempotency-Key was used with a different body, or more than 24 hours ago · ErrorEnvelope
  • 422No verification provider runs this type, or one of kyc_basic's steps, in the environment (no_route, with type and flow) · ErrorEnvelope
  • 503Saved but not queued (dependency_unavailable): retry with the same Idempotency-Key · ErrorEnvelope

GET/v1/verifications

List verifications

Scope verifications:read. Newest first; no evidence links (open one verification for those).

GET /v1/verifications parameters
FieldTypeDescription
typeone of "nrc_lookup", "document", "liveness", "face_match" · query
statusone of "pending", "completed", "failed" · query
decisionone of "pass", "review", "fail" · query
limitinteger · query
cursorstring (uuid) · query

Responses

  • 200A page of verifications · VerificationList
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks verifications:read · ErrorEnvelope

POST/v1/verifications/liveness/challenge

Start an active liveness challenge

Scope verifications:create. Returns a signed, single-use challenge, valid for 2 minutes, and 2 or 3 steps in a random order (blink, turn_left, turn_right, smile). The app asks the person to do each in turn, takes 2 to 5 frames during each, and sends them as a liveness check with subject.challenge and subject.steps (action, frames, times_ms). A challenge already used, expired, from another environment or with steps in another order is refused (400). Runs on providers that support active liveness (verify-local, and the simulator in sandbox).

Responses

  • 201The challenge · LivenessChallenge
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks verifications:create · ErrorEnvelope

GET/v1/verifications/{id}

Get a verification

Scope verifications:read. The result, decision and reasons, with signed evidence links valid for 5 minutes (each download is audited). The subject is shown masked. Evidence is deleted after 7 days in sandbox and staging, 90 in production; the decision stays. A kyc_basic flow returns its steps inline, each with its result and provider response. A step can also be read by its own id (from steps or the event), with parent_id naming its flow; lists show only the flow.

GET /v1/verifications/{id} parameters
FieldTypeDescription
idrequiredstring (uuid) · path

Responses

  • 200The verification · Verification
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks verifications:read · ErrorEnvelope
  • 404No such verification in this environment · ErrorEnvelope

Push

PUT/v1/push/apps

Register or update a push app

Scope push:manage. One app on one route: an Android package (fcm or hms), an iOS bundle id (apns), or a web origin (web). For FCM and Huawei, pass the parsed google-services.json or agconnect-services.json as config_file: only identifiers are kept (never API keys or the Huawei client secret), and they are checked against the environment’s credentials (push_config_mismatch when the file is for another project). A web app gets the environment’s VAPID public key, made on first use.

Request body (PutPushAppRequest)

PUT /v1/push/apps request body
FieldTypeDescription
routerequiredone of "fcm", "hms", "apns", "web"
identifierrequiredstring
config_fileobject
vapid_subjectstring

Responses

  • 200The app and its check · PushApp
  • 400Invalid identifier or file · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks push:manage · ErrorEnvelope
  • 422The file names another project or app than the credentials (push_config_mismatch) · ErrorEnvelope

GET/v1/push/apps

List push apps

Responses

  • 200The environment’s push apps · PushAppList
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks push:manage · ErrorEnvelope

PUT/v1/push/devices

Register a push device

Scope push:devices. Idempotent: the same address again refreshes it, and under another user_id moves it (a shared phone). Addresses: fcm_fid (preferred) or fcm_token, hms_token, apns_token (with environment from the app build), or a webpush_subscription on a known push service. Outside sandbox, app_id must be a registered app. Send with POST /v1/messages { channel: "push", to: "user:<user_id>" }.

Request body (RegisterPushDeviceRequest)

PUT /v1/push/devices request body
FieldTypeDescription
user_idrequiredstring
app_idrequiredstring
addressrequiredobject

Responses

  • 200The device (never its address) · PushDevice
  • 400Invalid address or app id · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks push:devices · ErrorEnvelope
  • 422The app is not registered (push_app_not_registered) · ErrorEnvelope

POST/v1/push/devices/query

List push devices

Scope push:devices. Filter by user_id; live devices only unless include_invalidated; cursor-paginated. A POST so the user id (often an email or phone number) stays out of URLs and logs.

Request body (PushDeviceQuery)

POST /v1/push/devices/query request body
FieldTypeDescription
user_idstring
include_invalidatedboolean
limitinteger
cursorstring (uuid)

Responses

  • 200A page of devices · PushDeviceList
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks push:devices · ErrorEnvelope

DELETE/v1/push/devices/{id}

Stop sending to a device (logout)

DELETE /v1/push/devices/{id} parameters
FieldTypeDescription
idrequiredstring (uuid) · path

Responses

  • 204Invalidated
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks push:devices · ErrorEnvelope
  • 404No live device with this id · ErrorEnvelope

Suppressions

POST/v1/suppressions

Suppress a destination

Scope suppressions:write. Stop sending to a phone number, email address or push target, on one channel or (null or absent) on every channel. Sends to it then fail with 422 suppressed_destination until the suppression is removed. Idempotent without an Idempotency-Key: an active suppression for the same destination, channel and scope comes back as it is, with 200. scope: bulk (channel email only) stops just the emails that carry an unsubscribe link, for unsubscribes kept in your own systems; codes, receipts and transactional email still go. Opt-outs reported by Africa’s Talking, permanent email bounces and spam complaints reported by Resend, and unsubscribes from bulk email (scope bulk) are added automatically.

Request body (CreateSuppressionRequest)

POST /v1/suppressions request body
FieldTypeDescription
destinationrequiredstring
channelone of "sms", "email", "whatsapp", "push", or null
reasonstring
scopeone of "all", "bulk"

Responses

  • 200Already suppressed: the existing suppression · Suppression
  • 201Suppressed (never the destination itself: a masked form and a hint) · Suppression
  • 400Invalid destination for the channel · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks suppressions:write · ErrorEnvelope

POST/v1/suppressions/query

List suppressions

Scope suppressions:write. Active suppressions, newest first, cursor-paginated. Filter by an exact destination, by channel (its own and the all-channel ones) or by source. A POST so a destination stays out of URLs and logs.

Request body (SuppressionQuery)

POST /v1/suppressions/query request body
FieldTypeDescription
destinationstring
channelone of "sms", "email", "whatsapp", "push"
sourceone of "api", "portal", "opt_out", "bounce", "complaint", "erasure", "unsubscribe"
scopeone of "all", "bulk"
limitinteger
cursorstring (uuid)

Responses

  • 200A page of suppressions · SuppressionList
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks suppressions:write · ErrorEnvelope

DELETE/v1/suppressions/{id}

Remove a suppression

Scope suppressions:write. Sends to the destination work again. The removal is kept on record with the key that made it.

DELETE /v1/suppressions/{id} parameters
FieldTypeDescription
idrequiredstring (uuid) · path

Responses

  • 204Removed
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks suppressions:write · ErrorEnvelope
  • 404No active suppression with this id · ErrorEnvelope

Sandbox

POST/v1/sandbox/inbox/query

Read what the simulators delivered to a destination

Sandbox keys only (scope messages:read), for automated tests: returns simulator deliveries to to, newest first, with the body as the handset got it, so a test can read an OTP code. Only the last 7 days are kept. A POST so the destination stays out of URLs and logs. Delivery is asynchronous: poll, or use sandbox.waitForMessage in the SDK.

Request body (SandboxInboxQuery)

POST /v1/sandbox/inbox/query request body
FieldTypeDescription
torequiredstring
sincestring (date-time)
limitinteger

Responses

  • 200Deliveries, newest first · SandboxInbox
  • 400Invalid body · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks messages:read (insufficient_scope), or is not a sandbox key (sandbox_only) · ErrorEnvelope

POST/v1/sandbox/inbound

Simulate a person writing to the sandbox on WhatsApp

Sandbox keys only (scope messages:send). The message arrives through the same path as Meta's webhook: it appears on GET /v1/inbound, emits inbound.received, and opens the 24-hour window for replies. media makes up a file of size_bytes and mime_type (default 2 KB of the kind's first type), which is downloaded and kept like a real one, or refused if it breaks WhatsApp's limits, so a test can try both.

Request body (SandboxInboundRequest)

POST /v1/sandbox/inbound request body
FieldTypeDescription
channelrequired"whatsapp"
fromrequiredstring
bodystring
mediaobject
.kindrequiredone of "image", "document", "audio", "video", "sticker"
.mime_typestring
.size_bytesinteger
.filenamestring

Responses

  • 202Queued; find it by provider_ref · SandboxInboundResponse
  • 400Invalid body · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks messages:send (insufficient_scope), or is not a sandbox key (sandbox_only) · ErrorEnvelope
  • 503Not queued (dependency_unavailable); retry · ErrorEnvelope

Media

POST/v1/media

Upload a file to send on WhatsApp

Scope messages:send. multipart/form-data with the file in file; kind (image, document, audio, video) defaults from its type, and filename (what a document's recipient sees) from the upload. Send it with whatsapp.media.media_id, or name it as a template header's sample. Limits are Meta's: image up to 5 MB (image/jpeg, image/png); document up to 16 MB (application/pdf, text/plain, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation); audio up to 16 MB (audio/aac, audio/amr, audio/mpeg, audio/mp4, audio/ogg); video up to 16 MB (video/mp4, video/3gpp). Kept encrypted for 7 days in sandbox and staging, 30 in production.

Responses

  • 201The stored file · MediaObject
  • 400No file · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks messages:send · ErrorEnvelope
  • 413Over WhatsApp's limit for the kind (payload_too_large) · ErrorEnvelope
  • 415A type WhatsApp doesn't send as that kind (unsupported_media_type) · ErrorEnvelope

GET/v1/media/{id}/content

Download a file a person sent in

The signed link GET /v1/inbound gives in media.url, valid for 5 minutes; no API key, so it can go straight to a browser or a download tool. Fetch the inbound message again for a new link. Files are kept 7 days in sandbox and staging, 90 in production, then media.status is expired.

GET /v1/media/{id}/content parameters
FieldTypeDescription
idrequiredstring (uuid) · path
exprequiredstring · query
sigrequiredstring · query

Responses

  • 200The file, as an attachment, in its own type
  • 404The link has expired or is wrong, or the file is gone · ErrorEnvelope

Usage

GET/v1/usage

Usage and charges for a month

Scope usage:read. What the API key's environment used in one UTC month, per channel and provider: billable messages (USSD: sessions), units (SMS segments, emails…) and what they were charged, in minor units of the organisation's currency. Sandbox usage is charged at zero. Statements in the business portal cover the whole organisation.

GET /v1/usage parameters
FieldTypeDescription
periodstring · queryYYYY-MM; defaults to the current UTC month

Responses

  • 200The month’s usage · Usage
  • 400period is not YYYY-MM · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks usage:read · ErrorEnvelope

packages

GET/v1/packages

Prepaid packages and what they have left

Scope usage:read. The organisation's running prepaid packages (bought in the business portal), soonest end first, and their units left added up: SMS segments to Zambian numbers, WhatsApp utility and authentication templates, and emails. Package units are used before the wallet; international SMS, WhatsApp marketing, push, USSD and verification are always charged from the wallet. Sandbox never uses packages.

Responses

  • 200Running packages · PackageBalance
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks usage:read · ErrorEnvelope

Inbound messages

GET/v1/inbound

List inbound messages (replies and MO traffic)

Scope inbound:read. Newest first; pass next_cursor as cursor for older messages. from is masked (+260*******01); the full sender is never returned. matched_message_id is the message a reply answers, when the vendor says which (WhatsApp). Each new message also emits inbound.received. A WhatsApp file comes as media: pending while it downloads, then stored with a signed url valid for 5 minutes, or too_large, unsupported or failed. Reply within the window with POST /v1/inbound/{id}/reply.

GET /v1/inbound parameters
FieldTypeDescription
channelstring · query
limitinteger · query
cursorstring (uuid) · query

Responses

  • 200A page of inbound messages · InboundList
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks inbound:read · ErrorEnvelope

GET/v1/inbound/{id}

One inbound message

Scope inbound:read. For example the inbound_id of an inbound.received event, which carries no text.

GET /v1/inbound/{id} parameters
FieldTypeDescription
idrequiredstring (uuid) · path

Responses

  • 200The message · InboundMessage
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks inbound:read · ErrorEnvelope
  • 404No such inbound message in this environment · ErrorEnvelope

POST/v1/inbound/{id}/reply

Reply on WhatsApp inside the customer-service window

Scope messages:send. Free text (body) or a file (media, with body as its caption) back to the person who sent this message, quoting it. Meta allows it only within 24 hours of the person's last message; after that, send an approved template with POST /v1/messages. Billed as a WhatsApp service conversation. The answer is the queued message; follow it like any other.

POST /v1/inbound/{id}/reply parameters
FieldTypeDescription
idrequiredstring (uuid) · path
Idempotency-Keyrequiredstring · headerUnique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours.

Request body (InboundReplyRequest)

POST /v1/inbound/{id}/reply request body
FieldTypeDescription
bodystring
mediaobject
.kindrequiredone of "image", "document", "audio", "video"
.urlstring (uri)
.media_idstring (uuid)
.filenamestring
tagsarray of string
metadataobject

Responses

  • 202The queued reply. On a replay the response carries Idempotent-Replayed: true. · Message
  • 400Invalid body, a message on another channel, or no Idempotency-Key · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 402The wallet can’t cover the estimated price (insufficient_balance) · ErrorEnvelope
  • 403The key lacks messages:send · ErrorEnvelope
  • 404No such inbound message in this environment · ErrorEnvelope
  • 409The Idempotency-Key was used with a different body, or more than 24 hours ago · ErrorEnvelope
  • 415The uploaded file is not a type WhatsApp sends as that kind (unsupported_media_type) · ErrorEnvelope
  • 422More than 24 hours since the person last wrote (outside_service_window, with last_inbound_at), or an unknown or expired media_id (media_not_found) · ErrorEnvelope

USSD

POST/v1/ussd/sessions/{id}/end

End a live USSD session

Scope ussd:manage. Closes the session now and emits ussd.session.ended with reason api_end. The handset still shows the last screen; the next request the telco makes gets text (default "This session has ended.") as a closing screen. The id is the session_id your callback receives. Each step of a session is a signed POST to the shortcode's callback (UssdCallbackRequest), answered with UssdCallbackResponse within the shortcode's budget.

POST /v1/ussd/sessions/{id}/end parameters
FieldTypeDescription
idrequiredstring (uuid) · path

Request body (EndUssdSessionRequest)

POST /v1/ussd/sessions/{id}/end request body
FieldTypeDescription
textstring

Responses

  • 200Ended · EndUssdSessionResponse
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks ussd:manage · ErrorEnvelope
  • 404No live session with that id in this environment (sessions live in the gateway only while active) · ErrorEnvelope
  • 409The session has already ended (ussd_session_ended) · ErrorEnvelope

POST/v1/ussd/emulator

Drive a USSD session on the simulator (sandbox)

Scope ussd:manage, sandbox keys only. Start with dial (a simulator shortcode of this environment, optionally with extra digits: *384*55*1#), then send session with each input. Runs the same session engine and calls the same callback as a telco would. Digits after the code are step 0 input, as from a telco: *388*4*334466*20# on *388*4# gives 334466*20. msisdn (E.164 with the plus, default +260970000001) and network (MCC-MNC such as 64502; default null) go with dial and reach the callback on every step. The answer is { session, msisdn, text, end }, not the callback's { text, action, state }: end is true once the session is over.

Request body (UssdEmulatorRequest)

POST /v1/ussd/emulator request body
FieldTypeDescription
dialstring
sessionstring
inputstring
msisdnstring
networkstring

Responses

  • 200The screen to show · UssdEmulatorResponse
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks ussd:manage (insufficient_scope), or is not a sandbox key (sandbox_only) · ErrorEnvelope
  • 404No simulator shortcode matches the dial, or the session handle is unknown · ErrorEnvelope
  • 409The emulator session has ended; dial again · ErrorEnvelope

Webhook endpoints

POST/v1/webhook-endpoints

Register a webhook endpoint

Scope webhooks:manage. Returns the signing secret once (a replay within 24 hours repeats it). event_types empty means every type. Outside sandbox the URL must be https. See the webhooks section for the events and their signature.

POST /v1/webhook-endpoints parameters
FieldTypeDescription
Idempotency-Keyrequiredstring · headerUnique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours.

Request body (CreateWebhookEndpointRequest)

POST /v1/webhook-endpoints request body
FieldTypeDescription
urlrequiredstring (uri)
event_typesarray of one of "message.queued", "message.sent", "message.delivered", "message.failed", "inbound.received", "otp.verified", "otp.expired", "verification.completed", "verification.failed", "ussd.session.started", "ussd.session.ended", "ussd.flow.event", "ussd.screen.truncated", "email.unsubscribed"
descriptionstring

Responses

  • 201The endpoint with its secret. On a replay the response carries Idempotent-Replayed: true. · CreatedWebhookEndpoint
  • 400Invalid body or URL, or no Idempotency-Key · ErrorEnvelope
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks webhooks:manage · ErrorEnvelope
  • 409The Idempotency-Key was used with a different body, or more than 24 hours ago · ErrorEnvelope

GET/v1/webhook-endpoints

List webhook endpoints (without secrets)

Responses

  • 200The environment's endpoints · WebhookEndpointList
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks webhooks:manage · ErrorEnvelope

DELETE/v1/webhook-endpoints/{id}

Turn a webhook endpoint off

Scope webhooks:manage. The endpoint stops receiving events at once (deliveries already queued for it are dropped) and stays in the list with enabled: false, with its delivery history. Turn it back on in the business portal. Doing it again changes nothing.

DELETE /v1/webhook-endpoints/{id} parameters
FieldTypeDescription
idrequiredstring (uuid) · path

Responses

  • 204Off
  • 401Missing or invalid key · ErrorEnvelope
  • 403The key lacks webhooks:manage · ErrorEnvelope
  • 404No webhook endpoint with this id in this environment · ErrorEnvelope

Webhook events

Each signed event contains id, type, created_at, and data. The fields below describe each event’s data payload. Configure subscriptions in the portal’s Webhooks section and verify deliveries with the endpoint’s signing secret.

message.queued

message.queued data
FieldTypeDescription
message_idrequiredstring (uuid)
channelrequiredone of "sms", "email", "whatsapp", "push"
statusrequiredone of "queued", "sent", "delivered", "failed", "bounced", "expired"
tagsrequiredarray of string
providerrequiredstring or null
provider_refrequiredstring or null
reasonrequiredstring or null
raw_coderequiredstring or null
cost_unitsrequirednumber or null
metadatarequiredobject, or null

message.sent

message.sent data
FieldTypeDescription
message_idrequiredstring (uuid)
channelrequiredone of "sms", "email", "whatsapp", "push"
statusrequiredone of "queued", "sent", "delivered", "failed", "bounced", "expired"
tagsrequiredarray of string
providerrequiredstring or null
provider_refrequiredstring or null
reasonrequiredstring or null
raw_coderequiredstring or null
cost_unitsrequirednumber or null
metadatarequiredobject, or null

message.delivered

message.delivered data
FieldTypeDescription
message_idrequiredstring (uuid)
channelrequiredone of "sms", "email", "whatsapp", "push"
statusrequiredone of "queued", "sent", "delivered", "failed", "bounced", "expired"
tagsrequiredarray of string
providerrequiredstring or null
provider_refrequiredstring or null
reasonrequiredstring or null
raw_coderequiredstring or null
cost_unitsrequirednumber or null
metadatarequiredobject, or null

message.failed

message.failed data
FieldTypeDescription
message_idrequiredstring (uuid)
channelrequiredone of "sms", "email", "whatsapp", "push"
statusrequiredone of "queued", "sent", "delivered", "failed", "bounced", "expired"
tagsrequiredarray of string
providerrequiredstring or null
provider_refrequiredstring or null
reasonrequiredstring or null
raw_coderequiredstring or null
cost_unitsrequirednumber or null
metadatarequiredobject, or null

inbound.received

inbound.received data
FieldTypeDescription
inbound_idrequiredstring (uuid)
channelrequiredone of "sms", "email", "whatsapp", "push"
fromrequiredstring
providerrequiredstring
provider_refrequiredstring
matched_message_idrequiredstring (uuid), or null
media_kindrequiredone of "image", "document", "audio", "video", "sticker", or null
received_atrequiredstring (date-time)

otp.verified

otp.verified data
FieldTypeDescription
challenge_idrequiredstring (uuid)
channelrequiredone of "sms", "whatsapp", "email"
statusrequiredone of "pending", "verified", "expired", "too_many_attempts"
message_idrequiredstring (uuid)
metadatarequiredobject, or null

otp.expired

otp.expired data
FieldTypeDescription
challenge_idrequiredstring (uuid)
channelrequiredone of "sms", "whatsapp", "email"
statusrequiredone of "pending", "verified", "expired", "too_many_attempts"
message_idrequiredstring (uuid)
metadatarequiredobject, or null

verification.completed

verification.completed data
FieldTypeDescription
verification_idrequiredstring (uuid)
typerequiredone of "nrc_lookup", "document", "liveness", "face_match", "kyc_basic"
statusrequiredone of "pending", "completed", "failed", "skipped"
decisionrequiredone of "pass", "review", "fail", or null
confidencerequirednumber or null
reasonsrequiredarray of string
failure_coderequiredstring or null
providerrequiredstring or null
stepsrequiredarray of object, or null
metadatarequiredobject, or null

verification.failed

verification.failed data
FieldTypeDescription
verification_idrequiredstring (uuid)
typerequiredone of "nrc_lookup", "document", "liveness", "face_match", "kyc_basic"
statusrequiredone of "pending", "completed", "failed", "skipped"
decisionrequiredone of "pass", "review", "fail", or null
confidencerequirednumber or null
reasonsrequiredarray of string
failure_coderequiredstring or null
providerrequiredstring or null
stepsrequiredarray of object, or null
metadatarequiredobject, or null

ussd.session.started

ussd.session.started data
FieldTypeDescription
session_idrequiredstring (uuid)
shortcode_idrequiredstring (uuid)
shortcoderequiredstring
msisdnrequiredstring
networkrequiredstring or null
providerrequiredstring
started_atrequiredstring (date-time)

ussd.session.ended

ussd.session.ended data
FieldTypeDescription
session_idrequiredstring (uuid)
shortcode_idrequiredstring (uuid)
shortcoderequiredstring
msisdnrequiredstring
networkrequiredstring or null
providerrequiredstring
started_atrequiredstring (date-time)
reasonrequiredone of "user_end", "app_end", "app_timeout", "app_error", "telco_timeout", "telco_error", "redis_unavailable", "api_end"
stepsrequiredinteger
ended_atrequiredstring (date-time)
duration_msrequiredinteger

ussd.screen.truncated

ussd.screen.truncated data
FieldTypeDescription
session_idrequiredstring (uuid)
shortcode_idrequiredstring (uuid)
steprequiredinteger
lengthrequiredinteger
limitrequiredinteger

email.unsubscribed

email.unsubscribed data
FieldTypeDescription
message_idrequiredstring (uuid)
suppression_idrequiredstring (uuid)
channelrequired"email"
destination_hintrequiredstring
tagsrequiredarray of string
metadatarequiredobject, or null
unsubscribed_atrequiredstring (date-time)