{
  "info": {
    "name": "Switchgear API",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "description": "Generated from Switchgear’s OpenAPI contract.\n\n1. Import this file into Postman.\n2. Keep base_url on https://sandbox.switchgear.app and set api_key locally to your sandbox key.\n3. Set idempotency_key to a fresh UUID before a NEW create operation; reuse the same value and body for retries.\n4. Fill resource IDs and required parameters from your API responses. Review each example body before sending. Optional query parameters start disabled.\n\nStart with Authentication → GET /v1/me, then Messages → Send one message. Sandbox delivery is simulated and free. Choose a local file for the multipart media upload. Signed media downloads use exp and sig from an inbound media URL and do not send your bearer key.\n\nProduction: https://api.switchgear.app; use production credentials only after business approval and channel activation. This is a request toolkit, not a sequential collection-runner workflow. Incoming webhook payloads are documented in /docs/api#webhook-events."
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{api_key}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://sandbox.switchgear.app",
      "type": "string",
      "description": "Sandbox by default. Production: https://api.switchgear.app (requires approval and channel activation)."
    },
    {
      "key": "api_key",
      "value": "",
      "type": "string",
      "description": "Set your own sg_test_ key locally in Postman. Keep it private."
    },
    {
      "key": "idempotency_key",
      "value": "",
      "type": "string",
      "description": "Set a fresh UUID for each NEW operation. Keep the SAME value and body when retrying that operation within 24 hours."
    },
    {
      "key": "keys_id",
      "value": "",
      "type": "string",
      "description": "The id for API keys. Fill this with the real value returned by the API."
    },
    {
      "key": "cursor",
      "value": "",
      "type": "string",
      "description": "Replace with the UUID returned by the relevant create or list operation."
    },
    {
      "key": "messages_id",
      "value": "",
      "type": "string",
      "description": "The id for Messages. Fill this with the real value returned by the API."
    },
    {
      "key": "challenge_id",
      "value": "",
      "type": "string",
      "description": "Replace with the UUID returned by the relevant create or list operation."
    },
    {
      "key": "verifications_id",
      "value": "",
      "type": "string",
      "description": "The id for Identity verification. Fill this with the real value returned by the API."
    },
    {
      "key": "push_id",
      "value": "",
      "type": "string",
      "description": "The id for Push. Fill this with the real value returned by the API."
    },
    {
      "key": "suppressions_id",
      "value": "",
      "type": "string",
      "description": "The id for Suppressions. Fill this with the real value returned by the API."
    },
    {
      "key": "media_id",
      "value": "",
      "type": "string",
      "description": "The id for Media. Fill this with the real value returned by the API."
    },
    {
      "key": "exp",
      "value": "",
      "type": "string",
      "description": "Required query parameter for /v1/media/{id}/content."
    },
    {
      "key": "sig",
      "value": "",
      "type": "string",
      "description": "Required query parameter for /v1/media/{id}/content."
    },
    {
      "key": "inbound_id",
      "value": "",
      "type": "string",
      "description": "The id for Inbound messages. Fill this with the real value returned by the API."
    },
    {
      "key": "ussd_id",
      "value": "",
      "type": "string",
      "description": "The id for USSD. Fill this with the real value returned by the API."
    },
    {
      "key": "webhook_endpoints_id",
      "value": "",
      "type": "string",
      "description": "The id for Webhook endpoints. Fill this with the real value returned by the API."
    }
  ],
  "item": [
    {
      "name": "Authentication",
      "item": [
        {
          "name": "GET · The project, environment and scopes this API key resolves to",
          "request": {
            "method": "GET",
            "description": "Responses:\n\n- 200: The key is valid\n\n- 401: Missing or invalid key",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/me",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "me"
              ],
              "query": [],
              "variable": []
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "API keys",
      "item": [
        {
          "name": "POST · Rotate this API key",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 201: The new key (`key` is its plaintext, shown once) and the old one with its new `expires_at`\n\n- 400: An overlap outside 0–168 hours\n\n- 401: Missing or invalid key\n\n- 404: `id` is not the calling key\n\n- 409: This key was already rotated (`invalid_transition`)",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/keys/:id/rotate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "keys",
                ":id",
                "rotate"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{keys_id}}",
                  "description": "Required id path parameter."
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "Messages",
      "item": [
        {
          "name": "POST · Send one message",
          "request": {
            "method": "POST",
            "description": "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`.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nSet idempotency_key to a fresh UUID for a NEW operation; reuse it unchanged for retries of that operation.\n\nResponses:\n\n- 202: Queued. On a replay the response carries `Idempotent-Replayed: true`.\n\n- 400: Invalid body (`validation_failed`) or no Idempotency-Key (`idempotency_key_missing`)\n\n- 401: Missing or invalid key\n\n- 402: Production 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\n\n- 403: The key lacks `messages:send`, or the account is suspended\n\n- 409: The Idempotency-Key was used with a different body, or more than 24 hours ago\n\n- 413: The body is over 100 KB (`payload_too_large`)\n\n- 415: WhatsApp: the uploaded file is not a type WhatsApp sends as that kind (`unsupported_media_type`)\n\n- 422: The 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`)\n\n- 429: Over the key's rate limit (`rate_limited`, with `Retry-After`)\n\n- 503: Stored but not queued; retry with the same Idempotency-Key",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{idempotency_key}}",
                "disabled": false,
                "description": "Unique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours."
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/messages",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "messages"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"channel\": \"sms\",\n  \"to\": \"+260971234567\",\n  \"body\": \"Your order A-17 is on its way.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "GET · List messages",
          "request": {
            "method": "GET",
            "description": "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.\n\nResponses:\n\n- 200: A page of messages\n\n- 400: An unknown status or channel, `since` not before `until`, or a cursor that is not a message in this environment\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `messages:read`",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/messages",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "messages"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "example",
                  "disabled": true,
                  "description": "Comma-separated, any of: queued, sent, delivered, failed, bounced, expired"
                },
                {
                  "key": "channel",
                  "value": "example",
                  "disabled": true,
                  "description": "Comma-separated, any of: sms, email, whatsapp, push"
                },
                {
                  "key": "tag",
                  "value": "example",
                  "disabled": true,
                  "description": "Only messages sent with this tag"
                },
                {
                  "key": "since",
                  "value": "2026-10-26T10:00:00.000Z",
                  "disabled": true,
                  "description": "Created at or after"
                },
                {
                  "key": "until",
                  "value": "2026-10-26T10:00:00.000Z",
                  "disabled": true,
                  "description": "Created before"
                },
                {
                  "key": "limit",
                  "value": "50",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "{{cursor}}",
                  "disabled": true
                }
              ],
              "variable": []
            }
          },
          "response": []
        },
        {
          "name": "POST · Send up to 1,000 messages",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nSet idempotency_key to a fresh UUID for a NEW operation; reuse it unchanged for retries of that operation.\n\nResponses:\n\n- 200: Every item has a result\n\n- 400: No `messages` array, an empty one or more than 1,000 items (`validation_failed`), or no Idempotency-Key (`idempotency_key_missing`)\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `messages:send`, or the account is suspended\n\n- 413: The body is over 5 MB (`payload_too_large`); split the batch\n\n- 429: The 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",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{idempotency_key}}",
                "disabled": false,
                "description": "Unique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours."
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/messages/batch",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "messages",
                "batch"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"messages\": [\n    {\n      \"channel\": \"sms\",\n      \"to\": \"+260971234567\",\n      \"body\": \"Your order A-17 is on its way.\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "GET · Current status and event history of a message",
          "request": {
            "method": "GET",
            "description": "Scope `messages:read`. Only messages in the API key's environment are visible.\n\nResponses:\n\n- 200: The message\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `messages:read`\n\n- 404: No such message in this environment",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/messages/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "messages",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{messages_id}}",
                  "description": "Required id path parameter."
                }
              ]
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "Templates",
      "item": [
        {
          "name": "POST · Save a template",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nSet idempotency_key to a fresh UUID for a NEW operation; reuse it unchanged for retries of that operation.\n\nResponses:\n\n- 201: The saved version. On a replay the response carries `Idempotent-Replayed: true`.\n\n- 400: Invalid body or placeholders, or no Idempotency-Key\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `templates:write`\n\n- 409: The Idempotency-Key was used with a different body, or more than 24 hours ago",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{idempotency_key}}",
                "disabled": false,
                "description": "Unique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours."
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/templates",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "templates"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"key\": \"example\",\n  \"channel\": \"sms\",\n  \"body\": \"Hello from Switchgear\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "GET · List templates with their newest version",
          "request": {
            "method": "GET",
            "description": "Scope `templates:read`. Oldest first; pass `next_cursor` as `cursor` for the next page.\n\nResponses:\n\n- 200: A page of templates\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `templates:read`",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/templates",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "templates"
              ],
              "query": [
                {
                  "key": "channel",
                  "value": "example",
                  "disabled": true
                },
                {
                  "key": "key",
                  "value": "example",
                  "disabled": true
                },
                {
                  "key": "locale",
                  "value": "example",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "{{cursor}}",
                  "disabled": true
                }
              ],
              "variable": []
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "One-time codes",
      "item": [
        {
          "name": "POST · Issue a one-time code",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nSet idempotency_key to a fresh UUID for a NEW operation; reuse it unchanged for retries of that operation.\n\nResponses:\n\n- 202: The challenge; the code is queued for delivery. On a replay the response carries `Idempotent-Replayed: true`.\n\n- 400: Invalid body, or no Idempotency-Key\n\n- 401: Missing or invalid key\n\n- 402: Production and staging: the wallet can’t cover the code’s price (`insufficient_balance`)\n\n- 403: The key lacks `otp:send`\n\n- 409: The Idempotency-Key was used with a different body, or more than 24 hours ago\n\n- 422: The destination is suppressed (`suppressed_destination`), no routing rule matches (`no_route`), or the named template is missing or not approved\n\n- 429: Inside the resend cooldown for this destination (`rate_limited`, with `Retry-After`)",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{idempotency_key}}",
                "disabled": false,
                "description": "Unique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours."
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/otp/send",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "otp",
                "send"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"destination\": \"+260971234567\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "POST · Check a code",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 200: The result of this attempt\n\n- 400: Invalid body\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `otp:verify`\n\n- 404: No such challenge in this environment",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/otp/verify",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "otp",
                "verify"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"challenge_id\": \"{{challenge_id}}\",\n  \"code\": \"123456\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "Identity verification",
      "item": [
        {
          "name": "POST · Start an identity check",
          "request": {
            "method": "POST",
            "description": "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`).\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nSet idempotency_key to a fresh UUID for a NEW operation; reuse it unchanged for retries of that operation.\n\nResponses:\n\n- 202: The pending verification On a replay the response carries `Idempotent-Replayed: true`.\n\n- 400: Invalid body (including images over 10 MB together, or a kyc_basic without the NRC its NRC lookup needs), or no Idempotency-Key\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `verifications:create`\n\n- 409: The Idempotency-Key was used with a different body, or more than 24 hours ago\n\n- 422: No verification provider runs this type, or one of kyc_basic's steps, in the environment (`no_route`, with `type` and `flow`)\n\n- 503: Saved but not queued (`dependency_unavailable`): retry with the same Idempotency-Key",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{idempotency_key}}",
                "disabled": false,
                "description": "Unique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours."
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/verifications",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "verifications"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"type\": \"nrc_lookup\",\n  \"subject\": {\n    \"nrc\": \"123456/10/1\",\n    \"first_name\": \"example\",\n    \"last_name\": \"example\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "GET · List verifications",
          "request": {
            "method": "GET",
            "description": "Scope `verifications:read`. Newest first; no evidence links (open one verification for those).\n\nResponses:\n\n- 200: A page of verifications\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `verifications:read`",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/verifications",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "verifications"
              ],
              "query": [
                {
                  "key": "type",
                  "value": "nrc_lookup",
                  "disabled": true
                },
                {
                  "key": "status",
                  "value": "pending",
                  "disabled": true
                },
                {
                  "key": "decision",
                  "value": "pass",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "{{cursor}}",
                  "disabled": true
                }
              ],
              "variable": []
            }
          },
          "response": []
        },
        {
          "name": "POST · Start an active liveness challenge",
          "request": {
            "method": "POST",
            "description": "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).\n\nResponses:\n\n- 201: The challenge\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `verifications:create`",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/verifications/liveness/challenge",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "verifications",
                "liveness",
                "challenge"
              ],
              "query": [],
              "variable": []
            }
          },
          "response": []
        },
        {
          "name": "GET · Get a verification",
          "request": {
            "method": "GET",
            "description": "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.\n\nResponses:\n\n- 200: The verification\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `verifications:read`\n\n- 404: No such verification in this environment",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/verifications/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "verifications",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{verifications_id}}",
                  "description": "Required id path parameter."
                }
              ]
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "Push",
      "item": [
        {
          "name": "PUT · Register or update a push app",
          "request": {
            "method": "PUT",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 200: The app and its check\n\n- 400: Invalid identifier or file\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `push:manage`\n\n- 422: The file names another project or app than the credentials (`push_config_mismatch`)",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/push/apps",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "push",
                "apps"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"route\": \"fcm\",\n  \"identifier\": \"com.example.app\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "GET · List push apps",
          "request": {
            "method": "GET",
            "description": "Responses:\n\n- 200: The environment’s push apps\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `push:manage`",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/push/apps",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "push",
                "apps"
              ],
              "query": [],
              "variable": []
            }
          },
          "response": []
        },
        {
          "name": "PUT · Register a push device",
          "request": {
            "method": "PUT",
            "description": "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>\" }`.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 200: The device (never its address)\n\n- 400: Invalid address or app id\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `push:devices`\n\n- 422: The app is not registered (`push_app_not_registered`)",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/push/devices",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "push",
                "devices"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"user_id\": \"example\",\n  \"app_id\": \"example\",\n  \"address\": {\n    \"kind\": \"fcm_fid\",\n    \"fid\": \"examplex\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "POST · List push devices",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 200: A page of devices\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `push:devices`",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/push/devices/query",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "push",
                "devices",
                "query"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "DELETE · Stop sending to a device (logout)",
          "request": {
            "method": "DELETE",
            "description": "Responses:\n\n- 204: Invalidated\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `push:devices`\n\n- 404: No live device with this id",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/push/devices/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "push",
                "devices",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{push_id}}",
                  "description": "Required id path parameter."
                }
              ]
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "Suppressions",
      "item": [
        {
          "name": "POST · Suppress a destination",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 200: Already suppressed: the existing suppression\n\n- 201: Suppressed (never the destination itself: a masked form and a hint)\n\n- 400: Invalid destination for the channel\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `suppressions:write`",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/suppressions",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "suppressions"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"destination\": \"+260971234567\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "POST · List suppressions",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 200: A page of suppressions\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `suppressions:write`",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/suppressions/query",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "suppressions",
                "query"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "DELETE · Remove a suppression",
          "request": {
            "method": "DELETE",
            "description": "Scope `suppressions:write`. Sends to the destination work again. The removal is kept on record with the key that made it.\n\nResponses:\n\n- 204: Removed\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `suppressions:write`\n\n- 404: No active suppression with this id",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/suppressions/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "suppressions",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{suppressions_id}}",
                  "description": "Required id path parameter."
                }
              ]
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "Sandbox",
      "item": [
        {
          "name": "POST · Read what the simulators delivered to a destination",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 200: Deliveries, newest first\n\n- 400: Invalid body\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `messages:read` (`insufficient_scope`), or is not a sandbox key (`sandbox_only`)",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/sandbox/inbox/query",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "sandbox",
                "inbox",
                "query"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"to\": \"+260971234567\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "POST · Simulate a person writing to the sandbox on WhatsApp",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 202: Queued; find it by `provider_ref`\n\n- 400: Invalid body\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `messages:send` (`insufficient_scope`), or is not a sandbox key (`sandbox_only`)\n\n- 503: Not queued (`dependency_unavailable`); retry",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/sandbox/inbound",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "sandbox",
                "inbound"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"channel\": \"whatsapp\",\n  \"from\": \"+260971234567\",\n  \"body\": \"Hello Switchgear\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "Media",
      "item": [
        {
          "name": "POST · Upload a file to send on WhatsApp",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 201: The stored file\n\n- 400: No `file`\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `messages:send`\n\n- 413: Over WhatsApp's limit for the kind (`payload_too_large`)\n\n- 415: A type WhatsApp doesn't send as that kind (`unsupported_media_type`)",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/media",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "media"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "file",
                  "type": "file",
                  "src": [],
                  "disabled": false
                },
                {
                  "key": "kind",
                  "type": "text",
                  "value": "image",
                  "disabled": true
                },
                {
                  "key": "filename",
                  "type": "text",
                  "value": "example",
                  "disabled": true
                }
              ]
            }
          },
          "response": []
        },
        {
          "name": "GET · Download a file a person sent in",
          "request": {
            "method": "GET",
            "description": "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`.\n\nResponses:\n\n- 200: The file, as an attachment, in its own type\n\n- 404: The link has expired or is wrong, or the file is gone",
            "auth": {
              "type": "noauth"
            },
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/media/:id/content?exp={{exp}}&sig={{sig}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "media",
                ":id",
                "content"
              ],
              "query": [
                {
                  "key": "exp",
                  "value": "{{exp}}",
                  "disabled": false
                },
                {
                  "key": "sig",
                  "value": "{{sig}}",
                  "disabled": false
                }
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "{{media_id}}",
                  "description": "Required id path parameter."
                }
              ]
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "Usage",
      "item": [
        {
          "name": "GET · Usage and charges for a month",
          "request": {
            "method": "GET",
            "description": "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.\n\nResponses:\n\n- 200: The month’s usage\n\n- 400: `period` is not `YYYY-MM`\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `usage:read`",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/usage",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "usage"
              ],
              "query": [
                {
                  "key": "period",
                  "value": "example",
                  "disabled": true,
                  "description": "`YYYY-MM`; defaults to the current UTC month"
                }
              ],
              "variable": []
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "packages",
      "item": [
        {
          "name": "GET · Prepaid packages and what they have left",
          "request": {
            "method": "GET",
            "description": "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.\n\nResponses:\n\n- 200: Running packages\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `usage:read`",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/packages",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "packages"
              ],
              "query": [],
              "variable": []
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "Inbound messages",
      "item": [
        {
          "name": "GET · List inbound messages (replies and MO traffic)",
          "request": {
            "method": "GET",
            "description": "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`.\n\nResponses:\n\n- 200: A page of inbound messages\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `inbound:read`",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/inbound",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "inbound"
              ],
              "query": [
                {
                  "key": "channel",
                  "value": "example",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "50",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "{{cursor}}",
                  "disabled": true
                }
              ],
              "variable": []
            }
          },
          "response": []
        },
        {
          "name": "GET · One inbound message",
          "request": {
            "method": "GET",
            "description": "Scope `inbound:read`. For example the `inbound_id` of an `inbound.received` event, which carries no text.\n\nResponses:\n\n- 200: The message\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `inbound:read`\n\n- 404: No such inbound message in this environment",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/inbound/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "inbound",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{inbound_id}}",
                  "description": "Required id path parameter."
                }
              ]
            }
          },
          "response": []
        },
        {
          "name": "POST · Reply on WhatsApp inside the customer-service window",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nSet idempotency_key to a fresh UUID for a NEW operation; reuse it unchanged for retries of that operation.\n\nResponses:\n\n- 202: The queued reply. On a replay the response carries `Idempotent-Replayed: true`.\n\n- 400: Invalid body, a message on another channel, or no Idempotency-Key\n\n- 401: Missing or invalid key\n\n- 402: The wallet can’t cover the estimated price (`insufficient_balance`)\n\n- 403: The key lacks `messages:send`\n\n- 404: No such inbound message in this environment\n\n- 409: The Idempotency-Key was used with a different body, or more than 24 hours ago\n\n- 415: The uploaded file is not a type WhatsApp sends as that kind (`unsupported_media_type`)\n\n- 422: More 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`)",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{idempotency_key}}",
                "disabled": false,
                "description": "Unique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours."
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/inbound/:id/reply",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "inbound",
                ":id",
                "reply"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{inbound_id}}",
                  "description": "Required id path parameter."
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"body\": \"Thanks for your message. We’ll be in touch.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "USSD",
      "item": [
        {
          "name": "POST · End a live USSD session",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 200: Ended\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `ussd:manage`\n\n- 404: No live session with that id in this environment (sessions live in the gateway only while active)\n\n- 409: The session has already ended (`ussd_session_ended`)",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/ussd/sessions/:id/end",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "ussd",
                "sessions",
                ":id",
                "end"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{ussd_id}}",
                  "description": "Required id path parameter."
                }
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "POST · Drive a USSD session on the simulator (sandbox)",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nResponses:\n\n- 200: The screen to show\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `ussd:manage` (`insufficient_scope`), or is not a sandbox key (`sandbox_only`)\n\n- 404: No simulator shortcode matches the dial, or the session handle is unknown\n\n- 409: The emulator session has ended; dial again",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/ussd/emulator",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "ussd",
                "emulator"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"dial\": \"*384*55#\",\n  \"msisdn\": \"+260971234567\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        }
      ]
    },
    {
      "name": "Webhook endpoints",
      "item": [
        {
          "name": "POST · Register a webhook endpoint",
          "request": {
            "method": "POST",
            "description": "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.\n\nSample body: replace example values, IDs and URLs before sending. Optional fields are documented in the API reference.\n\nSet idempotency_key to a fresh UUID for a NEW operation; reuse it unchanged for retries of that operation.\n\nResponses:\n\n- 201: The endpoint with its secret. On a replay the response carries `Idempotent-Replayed: true`.\n\n- 400: Invalid body or URL, or no Idempotency-Key\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `webhooks:manage`\n\n- 409: The Idempotency-Key was used with a different body, or more than 24 hours ago",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{idempotency_key}}",
                "disabled": false,
                "description": "Unique per request, for example a UUID. A retry with the same key and body returns the original response for 24 hours."
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "disabled": false,
                "description": "JSON request body."
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/webhook-endpoints",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhook-endpoints"
              ],
              "query": [],
              "variable": []
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://example.com/switchgear/webhook\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": []
        },
        {
          "name": "GET · List webhook endpoints (without secrets)",
          "request": {
            "method": "GET",
            "description": "Responses:\n\n- 200: The environment's endpoints\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `webhooks:manage`",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/webhook-endpoints",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhook-endpoints"
              ],
              "query": [],
              "variable": []
            }
          },
          "response": []
        },
        {
          "name": "DELETE · Turn a webhook endpoint off",
          "request": {
            "method": "DELETE",
            "description": "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.\n\nResponses:\n\n- 204: Off\n\n- 401: Missing or invalid key\n\n- 403: The key lacks `webhooks:manage`\n\n- 404: No webhook endpoint with this id in this environment",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/webhook-endpoints/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhook-endpoints",
                ":id"
              ],
              "query": [],
              "variable": [
                {
                  "key": "id",
                  "value": "{{webhook_endpoints_id}}",
                  "description": "Required id path parameter."
                }
              ]
            }
          },
          "response": []
        }
      ]
    }
  ]
}