{"openapi":"3.0.3","info":{"title":"Hellio Messaging API","version":"1.0.0","description":"SMS, OTP, voice, number-lookup and email-verification API. Authenticate with a Sanctum bearer token scoped to per-endpoint abilities. Side-effecting POSTs accept an Idempotency-Key header. List endpoints use opaque cursor pagination."},"servers":[{"url":"https:\/\/api.helliomessaging.com"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Account","description":"Check your wallet balance and look up per-network SMS pricing before you send."},{"name":"SMS","description":"Send single or bulk SMS campaigns and track delivery per message and per campaign."},{"name":"OTP","description":"Send and verify one-time passcodes for 2FA over SMS, voice or email."},{"name":"Voice","description":"Broadcast audio files or AI text-to-speech voice calls to one or many recipients."},{"name":"Lookup","description":"Validate phone numbers and detect their network and portability status (HLR lookup)."},{"name":"Email","description":"Check whether an email address is valid and deliverable before sending to it."},{"name":"Webhooks","description":"Register and manage endpoints that receive real-time delivery receipts."},{"name":"USSD","description":"Run interactive USSD services on the shared short code: manage apps and extensions, view sessions, and drive your callback in the sandbox."},{"name":"Sender IDs","description":"Register the alphanumeric names that appear as the sender of your messages (with the required business certificate) and check their approval status."},{"name":"Sub-accounts","description":"Provision and manage client sub-accounts. In isolated mode each sub-account funds and spends from its own wallet, and can be issued its own API token."},{"name":"Payments","description":"Top up a wallet with Paystack over the API: start a checkout and get the URL, then the wallet is auto-credited by webhook on success."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Sanctum personal access token."}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Retry-safe key. A repeat with the same key replays the original response."},"Cursor":{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor."},"PerPage":{"name":"per_page","in":"query","schema":{"type":"integer","default":50,"maximum":200}},"StatusFilter":{"name":"status","in":"query","schema":{"type":"string"}}},"schemas":{"Error":{"type":"object","properties":{"message":{"type":"string"},"error":{"type":"string"}}}}},"paths":{"\/v1\/balance":{"get":{"summary":"Wallet balance","description":"Returns your current wallet balance, the amount reserved for in-flight sends, and the spendable amount. Requires the `balance` scope.","tags":["Account"],"responses":{"200":{"description":"Current balance.","content":{"application\/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"balance":{"type":"string"},"reserved":{"type":"string"},"available":{"type":"string"},"currency":{"type":"string"}}}},"example":{"data":{"balance":"152.4000","reserved":"3.0000","available":"149.4000","currency":"GHS"}}}}}}}}},"\/v1\/pricing":{"get":{"summary":"Per-network SMS pricing","description":"Lists the price per SMS segment for each network, optionally filtered by ISO-2 country code. Use it to estimate cost before sending. Requires the `balance` scope.","tags":["Account"],"parameters":[{"name":"country","in":"query","schema":{"type":"string"},"description":"ISO-2 country filter."}],"responses":{"200":{"description":"Price list.","content":{"application\/json":{"example":{"data":[{"network":"MTN","country":"Ghana","country_code":"GH","price_per_sms":"0.0300","currency":"GHS"}]}}}}}}},"\/v1\/sms\/send":{"post":{"summary":"Send an SMS campaign","description":"Queues an SMS to one or many recipients from an approved sender ID. The cost is reserved from your wallet up front and a campaign reference is returned for tracking. Pass an `Idempotency-Key` to make retries safe. Requires the `sms:send` scope.","tags":["SMS"],"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["recipients","sender","message"],"properties":{"recipients":{"oneOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}],"description":"Array, or a comma\/newline-separated string."},"sender":{"type":"string","maxLength":11},"message":{"type":"string"},"gateway":{"type":"string","nullable":true}}}}}},"responses":{"202":{"description":"Queued.","content":{"application\/json":{"example":{"data":{"reference":"8f3ac9e1-2b44-4d70-9c2a-1e5f6a7b8c90","campaign_id":1042,"status":"queued","accepted_recipients":1,"invalid_recipients":0,"segments":1,"estimated_cost":"0.0300","currency":"GHS"}}}}},"403":{"description":"Spend limit exceeded, traffic not routable, or IP not allowed.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"message":"This send would exceed your daily spend limit.","error":"spend_limit_exceeded","period":"daily"}}}},"402":{"description":"Insufficient balance."},"422":{"description":"Validation \/ sender not approved \/ no valid recipients."}}}},"\/v1\/messages":{"get":{"summary":"List messages","description":"Cursor-paginated list of your messages, newest first. Filter with `?status=`. Requires the `reports` scope.","tags":["SMS"],"parameters":[{"$ref":"#\/components\/parameters\/StatusFilter"},{"$ref":"#\/components\/parameters\/Cursor"},{"$ref":"#\/components\/parameters\/PerPage"}],"responses":{"200":{"description":"Cursor-paginated messages.","content":{"application\/json":{"example":{"data":[{"reference":"b3f1c2a4-9e8d-4c7b-a1f2-0d6e5c4b3a21","mobile_number":"+233241234567","network":"MTN","status":"delivered","route":"Gateway A","provider_message_id":"abc123","segments":1,"price":"0.0300","error_code":null,"error_message":null,"submitted_at":"2026-06-30T10:24:01+00:00","delivered_at":"2026-06-30T10:24:05+00:00"}],"next_cursor":"eyJpZCI6MTB9","per_page":50}}}}}}},"\/v1\/messages\/{id}":{"get":{"summary":"Message status","description":"Returns the delivery status of a single message by its `reference` (uuid). Requires the `reports` scope.","tags":["SMS"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Message.","content":{"application\/json":{"example":{"data":{"reference":"b3f1c2a4-9e8d-4c7b-a1f2-0d6e5c4b3a21","mobile_number":"+233241234567","network":"MTN","status":"delivered","route":"Gateway A","provider_message_id":"abc123","segments":1,"price":"0.0300","error_code":null,"error_message":null,"submitted_at":"2026-06-30T10:24:01+00:00","delivered_at":"2026-06-30T10:24:05+00:00"}}}}},"404":{"description":"Not found."}}}},"\/v1\/campaigns":{"get":{"summary":"List campaigns","description":"Cursor-paginated list of your campaigns, newest first. Filter with `?status=`. Requires the `reports` scope.","tags":["SMS"],"parameters":[{"$ref":"#\/components\/parameters\/StatusFilter"},{"$ref":"#\/components\/parameters\/Cursor"},{"$ref":"#\/components\/parameters\/PerPage"}],"responses":{"200":{"description":"Cursor-paginated campaigns.","content":{"application\/json":{"example":{"data":[{"reference":"8f3ac9e1-2b44-4d70-9c2a-1e5f6a7b8c90","campaign_id":1042,"type":"bulk","sender":"HELLIO","status":"completed","recipients":1200,"estimated_cost":"36.0000","charged":"35.7600","created_at":"2026-06-30T10:20:00+00:00"}],"next_cursor":null,"per_page":50}}}}}}},"\/v1\/campaigns\/{id}":{"get":{"summary":"Campaign summary","description":"Returns a campaign with a delivery-status breakdown, by its `reference` (uuid) or numeric id. Requires the `reports` scope.","tags":["SMS"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign + status breakdown.","content":{"application\/json":{"example":{"data":{"reference":"8f3ac9e1-2b44-4d70-9c2a-1e5f6a7b8c90","campaign_id":1042,"status":"completed","recipients":1200,"estimated_cost":"36.0000","charged":"35.7600","breakdown":{"delivered":1180,"submitted":12,"failed":8}}}}}}}}},"\/v1\/otp\/send":{"post":{"summary":"Send an OTP","description":"Generates a one-time passcode and delivers it over SMS, voice or email. The returned `reference` is the uuid of the message it spawns, so you can track delivery via GET \/messages\/{reference}. Requires the `otp` scope.","tags":["OTP"],"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","properties":{"mobile_number":{"type":"string"},"email":{"type":"string"},"sender":{"type":"string"},"channel":{"type":"string","enum":["sms","voice","email"]},"length":{"type":"integer"}}}}}},"responses":{"201":{"description":"OTP queued.","content":{"application\/json":{"example":{"data":{"reference":"b3f1c2a4-9e8d-4c7b-a1f2-0d6e5c4b3a21","channel":"sms","mobile_number":"+233241234567","purpose":"verify","expires_in_minutes":5,"expires_at":"2026-06-30T10:29:01+00:00","status":"queued"}}}}},"429":{"description":"Throttled (per-destination or per-IP).","content":{"application\/json":{"example":{"message":"Too many requests.","error":"throttled"}}}}}}},"\/v1\/otp\/verify":{"post":{"summary":"Verify an OTP","description":"Checks a code the user entered against the most recent OTP for that destination. Returns 200 when verified, 422 otherwise. Requires the `otp` scope.","tags":["OTP"],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["code"],"properties":{"mobile_number":{"type":"string"},"email":{"type":"string"},"code":{"type":"string"},"channel":{"type":"string"}}}}}},"responses":{"200":{"description":"Verified.","content":{"application\/json":{"example":{"data":{"verified":true,"status":"verified"}}}}},"422":{"description":"Invalid, expired, or too many attempts.","content":{"application\/json":{"example":{"data":{"verified":false,"status":"invalid"}}}}}}}},"\/v1\/voice\/send":{"post":{"summary":"Send a voice broadcast","description":"Queues a voice call to one or many recipients. Provide either `text` (synthesized to speech, with an optional `voice`) or `audio_url` (an audio file we fetch). The gateway is chosen by Hellio. Billed per second of audio per recipient. Requires the `voice` scope.","tags":["Voice"],"parameters":[{"$ref":"#\/components\/parameters\/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["recipients"],"properties":{"recipients":{"type":"array","items":{"type":"string"}},"text":{"type":"string","description":"Text to synthesize (alternative to audio_url)."},"voice":{"type":"string","enum":["alloy","echo","fable","onyx","nova","shimmer"],"description":"Text-to-speech voice (used with `text`). Defaults to alloy."},"audio_url":{"type":"string","description":"URL of an audio file to broadcast (alternative to text)."},"name":{"type":"string","nullable":true}}}}}},"responses":{"202":{"description":"Queued.","content":{"application\/json":{"example":{"data":{"voice_campaign_id":55,"status":"queued","recipients":300,"audio_seconds":12,"estimated_cost":"30.0000","currency":"GHS"}}}}}}}},"\/v1\/lookup":{"post":{"summary":"Number (HLR) lookup","description":"Queues a batch of HLR number lookups. Lookups run asynchronously; the response confirms how many numbers were accepted. Poll the lookups endpoints for results. Requires the `lookup` scope.","tags":["Lookup"],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["numbers"],"properties":{"numbers":{"oneOf":[{"type":"array","items":{"type":"string"}},{"type":"string"}],"description":"Array, or a comma\/newline-separated string."},"scheduled_at":{"type":"string","format":"date-time","nullable":true}}}}}},"responses":{"202":{"description":"Lookups queued.","content":{"application\/json":{"example":{"data":{"accepted":2,"status":"queued"}}}}}}}},"\/v1\/email\/verify":{"post":{"summary":"Verify email addresses","description":"Checks one or more email addresses for deliverability (syntax, MX records, mailbox existence, disposable and role-account detection). Billed per address checked. Requires the `email:verify` scope.","tags":["Email"],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["emails"],"properties":{"emails":{"type":"array","items":{"type":"string"},"description":"One or more email addresses to verify."}}}}}},"responses":{"200":{"description":"Per-address results.","content":{"application\/json":{"example":{"data":[{"email":"user@example.com","status":"deliverable","syntax_valid":true,"has_mx":true,"exists":true,"disposable":false,"role_account":false}]}}}}}}},"\/v1\/webhooks":{"get":{"summary":"List webhook endpoints","description":"Lists your registered webhook endpoints. Requires the `webhooks` scope.","tags":["Webhooks"],"responses":{"200":{"description":"Endpoints.","content":{"application\/json":{"example":{"data":[{"id":3,"url":"https:\/\/example.com\/hooks\/hellio","events":["message.delivered"],"active":true,"last_failed_at":null}]}}}}}},"post":{"summary":"Create a webhook endpoint","description":"Registers an endpoint to receive delivery receipts. The signing `secret` is returned once, on creation. Pass `events` to filter, or omit for all. Requires the `webhooks` scope.","tags":["Webhooks"],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"201":{"description":"Created.","content":{"application\/json":{"example":{"data":{"id":3,"url":"https:\/\/example.com\/hooks\/hellio","events":["message.delivered"],"secret":"whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","note":"Store this secret \u2014 it is shown only once. Verify the X-Hellio-Signature header (HMAC-SHA256 of the body)."}}}}}}}},"\/v1\/ussd\/pricing":{"get":{"summary":"USSD pricing","description":"Returns the shared short code, the per-network session prices, and the per-length monthly extension rentals. Prices are in GHS. Requires the `ussd` scope.","tags":["USSD"],"responses":{"200":{"description":"Pricing.","content":{"application\/json":{"example":{"data":{"short_code":"*920#","short_codes":[{"code":"*920#","label":"Primary","primary":true}],"currency":"GHS","session_prices":[{"network":"MTN","slug":"mtn-gh","session_price":"0.0300"},{"network":"Telecel","slug":"telecel-gh","session_price":"0.0300"},{"network":"AirtelTigo","slug":"airteltigo-gh","session_price":"0.0300"}],"extension_prices":[{"length":1,"monthly_price":"500.0000"},{"length":2,"monthly_price":"300.0000"},{"length":3,"monthly_price":"150.0000"},{"length":4,"monthly_price":"80.0000"},{"length":5,"monthly_price":"40.0000"},{"length":6,"monthly_price":"20.0000"}]}}}}}}}},"\/v1\/ussd\/pricing\/availability":{"get":{"summary":"Check extension availability","description":"Reports whether a specific extension code is a valid length, free to rent, and its monthly price. Requires the `ussd` scope.","tags":["USSD"],"parameters":[{"name":"code","in":"query","required":true,"schema":{"type":"string"},"description":"The numeric extension code, e.g. `100`."}],"responses":{"200":{"description":"Availability.","content":{"application\/json":{"example":{"data":{"code":"100","valid":true,"available":true,"monthly_price":"150.0000"}}}}}}}},"\/v1\/ussd\/apps":{"get":{"summary":"List USSD apps","description":"Cursor-paginated list of your USSD apps, newest first. Requires the `ussd` scope.","tags":["USSD"],"parameters":[{"$ref":"#\/components\/parameters\/Cursor"},{"$ref":"#\/components\/parameters\/PerPage"}],"responses":{"200":{"description":"Apps.","content":{"application\/json":{"example":{"data":[{"id":"3f2a1b4c-9e8d-4c7b-a1f2-0d6e5c4b3a21","name":"Acme USSD","callback_url":"https:\/\/acme.example\/ussd","mode":"test","test_secret":"ussk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","live_secret":"ussk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","active":true,"is_live":false,"created_at":"2026-07-06T10:24:01+00:00"}],"next_cursor":null,"per_page":50}}}}}},"post":{"summary":"Create a USSD app","description":"Creates an app that points the short code at your callback URL. The app starts in `test` mode and returns two signing secrets, `test_secret` and `live_secret`. Hellio POSTs each session step to `callback_url` and signs it (HMAC-SHA256 of the body, header `X-Hellio-Signature`) with the secret matching the request's `mode`: `test_secret` in the sandbox, `live_secret` on the live short code. The callback URL must be public. Requires the `ussd` scope.","tags":["USSD"],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["name","callback_url"],"properties":{"name":{"type":"string","maxLength":120},"callback_url":{"type":"string","description":"Public HTTPS URL that receives each session step."},"active":{"type":"boolean","default":true}}}}}},"responses":{"201":{"description":"Created.","content":{"application\/json":{"example":{"data":{"id":"3f2a1b4c-9e8d-4c7b-a1f2-0d6e5c4b3a21","name":"Acme USSD","callback_url":"https:\/\/acme.example\/ussd","mode":"test","test_secret":"ussk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","live_secret":"ussk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","active":true,"is_live":false,"created_at":"2026-07-06T10:24:01+00:00"}}}}},"422":{"description":"Validation failed (e.g. callback URL not public)."}}}},"\/v1\/ussd\/apps\/{id}":{"put":{"summary":"Update a USSD app","description":"Updates an app's name, callback URL, or active state. Requires the `ussd` scope.","tags":["USSD"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["name","callback_url"],"properties":{"name":{"type":"string"},"callback_url":{"type":"string"},"active":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated.","content":{"application\/json":{"example":{"data":{"id":"3f2a1b4c-9e8d-4c7b-a1f2-0d6e5c4b3a21","name":"Acme USSD","callback_url":"https:\/\/acme.example\/ussd","mode":"test","test_secret":"ussk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","live_secret":"ussk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","active":true,"is_live":false,"created_at":"2026-07-06T10:24:01+00:00"}}}}},"404":{"description":"Not found."}}},"delete":{"summary":"Delete a USSD app","description":"Deletes a USSD app. Requires the `ussd` scope.","tags":["USSD"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted."},"404":{"description":"Not found."}}}},"\/v1\/ussd\/apps\/{id}\/mode":{"post":{"summary":"Switch an app between test and live","description":"Switches the app to `test` (sandbox) or `live`. A new app is in `test` mode; going `live` requires an active purchased extension for the app, otherwise the call returns `402 extension_required`. Returns the updated app. Requires the `ussd` scope.","tags":["USSD"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["mode"],"properties":{"mode":{"type":"string","enum":["test","live"]}}}}}},"responses":{"200":{"description":"Mode switched.","content":{"application\/json":{"example":{"data":{"id":"3f2a1b4c-9e8d-4c7b-a1f2-0d6e5c4b3a21","name":"Acme USSD","callback_url":"https:\/\/acme.example\/ussd","mode":"live","test_secret":"ussk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","live_secret":"ussk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","active":true,"is_live":true,"created_at":"2026-07-06T10:24:01+00:00"}}}}},"402":{"description":"The app has no active purchased extension, so it cannot go live.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"message":"Purchase an extension for this app before switching it live.","error":"extension_required"}}}},"404":{"description":"Not found."}}}},"\/v1\/ussd\/apps\/{id}\/rotate-secret":{"post":{"summary":"Rotate an app signing secret","description":"Rotates one mode's signing secret. Pass `mode` to choose which secret to replace: `test` rotates `test_secret`, `live` rotates `live_secret`. The old secret stops working immediately. Returns the updated app. Requires the `ussd` scope.","tags":["USSD"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["mode"],"properties":{"mode":{"type":"string","enum":["test","live"]}}}}}},"responses":{"200":{"description":"Secret rotated.","content":{"application\/json":{"example":{"data":{"id":"3f2a1b4c-9e8d-4c7b-a1f2-0d6e5c4b3a21","name":"Acme USSD","callback_url":"https:\/\/acme.example\/ussd","mode":"test","test_secret":"ussk_test_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy","live_secret":"ussk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","active":true,"is_live":false,"created_at":"2026-07-06T10:24:01+00:00"}}}}},"404":{"description":"Not found."}}}},"\/v1\/ussd\/extensions":{"get":{"summary":"List extensions","description":"Cursor-paginated list of your rented extensions, newest first. Requires the `ussd` scope.","tags":["USSD"],"parameters":[{"$ref":"#\/components\/parameters\/Cursor"},{"$ref":"#\/components\/parameters\/PerPage"}],"responses":{"200":{"description":"Extensions.","content":{"application\/json":{"example":{"data":[{"id":"9a1c2b3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d","code":"100","dial_string":"*920*100#","length":3,"status":"active","monthly_price":"150.0000","auto_renew":true,"app_id":"3f2a1b4c-9e8d-4c7b-a1f2-0d6e5c4b3a21","expires_at":"2026-08-06T10:24:01+00:00"}],"next_cursor":null,"per_page":50}}}}}},"post":{"summary":"Rent an extension","description":"Rents a numeric extension on the shared short code, charged from your dedicated USSD balance by digit length (shorter is premium). USSD money is separate from your SMS credit and main money wallet; top up your USSD balance first. Optionally bind the extension to an app. Requires the `ussd` scope.","tags":["USSD"],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","maxLength":12,"description":"Numeric extension, e.g. `100`."},"app_id":{"type":"string","format":"uuid","nullable":true,"description":"An app of yours to route this extension to."},"short_code":{"type":"string","nullable":true,"description":"Which shared short code to rent under, e.g. `*920#`. Defaults to the primary. List the options via GET \/ussd\/pricing."}}}}}},"responses":{"201":{"description":"Rented.","content":{"application\/json":{"example":{"data":{"id":"9a1c2b3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d","code":"100","dial_string":"*920*100#","length":3,"status":"active","monthly_price":"150.0000","auto_renew":true,"app_id":"3f2a1b4c-9e8d-4c7b-a1f2-0d6e5c4b3a21","expires_at":"2026-08-06T10:24:01+00:00"}}}}},"402":{"description":"Insufficient USSD balance.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"message":"Your USSD balance is too low. Top up your USSD balance and try again.","error":"insufficient_ussd_balance"}}}},"409":{"description":"Extension unavailable (already taken).","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"message":"Extension [100] is already taken.","error":"extension_unavailable"}}}},"422":{"description":"Invalid length or unknown app."}}}},"\/v1\/ussd\/extensions\/{id}":{"delete":{"summary":"Release an extension","description":"Releases a rented extension. Requires the `ussd` scope.","tags":["USSD"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Released."},"404":{"description":"Not found."}}}},"\/v1\/ussd\/sessions":{"get":{"summary":"List sessions","description":"Cursor-paginated list of your USSD sessions, newest first. Filter with `?status=` (`active`, `ended`, `timeout`, `failed`). Requires the `ussd` scope.","tags":["USSD"],"parameters":[{"$ref":"#\/components\/parameters\/StatusFilter"},{"$ref":"#\/components\/parameters\/Cursor"},{"$ref":"#\/components\/parameters\/PerPage"}],"responses":{"200":{"description":"Sessions.","content":{"application\/json":{"example":{"data":[{"id":"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d","session_ref":"atx-8f3ac9e1-2b44-4d70","msisdn":"+233241234567","service_code":"*920*100#","status":"ended","steps":3,"charge":"0.0300","sandbox":false,"started_at":"2026-07-06T10:24:01+00:00","ended_at":"2026-07-06T10:24:18+00:00"}],"next_cursor":null,"per_page":50}}}}}}},"\/v1\/ussd\/sessions\/{id}":{"get":{"summary":"Session detail","description":"Returns a single session by its uuid, including the number of developer steps and the amount charged. Requires the `ussd` scope.","tags":["USSD"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Session.","content":{"application\/json":{"example":{"data":{"id":"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d","session_ref":"atx-8f3ac9e1-2b44-4d70","msisdn":"+233241234567","service_code":"*920*100#","status":"ended","steps":3,"charge":"0.0300","sandbox":false,"started_at":"2026-07-06T10:24:01+00:00","ended_at":"2026-07-06T10:24:18+00:00"}}}}},"404":{"description":"Not found."}}}},"\/v1\/ussd\/simulate":{"post":{"summary":"Simulate a session (sandbox)","description":"Drives a USSD session against one of your own apps in test mode (`mode: test`), with no telco, no purchased extension, and no charge. You reference the app by `app_id`, so you can simulate it before renting an extension. Open the session with `new_session: true` and an empty `input`, then repeat with the same `session_id` and the next input until the reply returns `\"action\": \"end\"`. You may only simulate an app you own. Requires the `ussd` scope.","tags":["USSD"],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["app_id","session_id","msisdn"],"properties":{"app_id":{"type":"string","description":"The id of one of your USSD apps to simulate."},"session_id":{"type":"string","maxLength":120,"description":"Any stable id you choose for this simulated session."},"msisdn":{"type":"string","maxLength":20},"service_code":{"type":"string","maxLength":32,"nullable":true,"description":"Optional dialled code shown to your callback. Defaults to the shared short code."},"input":{"type":"string","maxLength":182,"nullable":true,"description":"The subscriber's input for this step. Empty on the first step."},"new_session":{"type":"boolean","default":false,"description":"True to open a fresh session."}}}}}},"responses":{"200":{"description":"Your callback's reply for this step.","content":{"application\/json":{"example":{"data":{"message":"Your balance is GHS 152.40","action":"end","continue":false}}}}},"422":{"description":"The app id does not match one of your apps.","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Error"},"example":{"message":"Unknown app.","error":"unknown_app"}}}}}}},"\/v1\/sender-ids":{"get":{"summary":"List Sender IDs","description":"Returns your Sender IDs and their approval status (`pending`, `approved`, `rejected`). Requires the `sender-ids` scope.","tags":["Sender IDs"],"responses":{"200":{"description":"OK.","content":{"application\/json":{"example":{"data":[{"id":12,"sender_id":"PROMOCO","status":"pending","reason":null,"reviewed_at":null,"created_at":"2026-07-17T10:00:00+00:00"}]}}}}}},"post":{"summary":"Register a Sender ID","description":"Registers an alphanumeric Sender ID (max 11 chars) and uploads the business registration certificate the NCA review requires (required for top-level accounts; optional for sub-accounts, whose parent is already vetted). The value is fraud-screened: scam wording is rejected (422), a protected-brand match is accepted but flagged for review. Approval is manual, poll the status. Send as `multipart\/form-data`. Requires the `sender-ids` scope.","tags":["Sender IDs"],"requestBody":{"required":true,"content":{"multipart\/form-data":{"schema":{"type":"object","required":["sender_id","purpose"],"properties":{"sender_id":{"type":"string","maxLength":11,"example":"PROMOCO"},"purpose":{"type":"string","minLength":20,"maxLength":500,"description":"What you will send with this Sender ID, with a sample message. Forwarded to the network for its review, so a clear purpose speeds up approval.","example":"Order updates for our customers. Sample: Hi Ama, your order #4521 has shipped and arrives tomorrow."},"certificate":{"type":"string","format":"binary","description":"Business registration certificate (pdf, jpg or png, max 5 MB). Required for top-level accounts; optional for sub-accounts (the parent account is already vetted)."}}}}}},"responses":{"201":{"description":"Submitted for approval.","content":{"application\/json":{"example":{"data":{"id":12,"sender_id":"PROMOCO","status":"pending","reason":null,"reviewed_at":null,"created_at":"2026-07-17T10:00:00+00:00"},"message":"Sender ID submitted for approval. Approval is manual and usually completes within 24 hours."}}}},"422":{"description":"Rejected by screening or validation.","content":{"application\/json":{"example":{"error":{"code":"sender_id_blocked","message":"This sender ID contains wording associated with spam or scams and cannot be registered."}}}}}}}},"\/v1\/sender-ids\/{id}":{"get":{"summary":"Get a Sender ID","description":"Returns one Sender ID and its approval status. Requires the `sender-ids` scope.","tags":["Sender IDs"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK.","content":{"application\/json":{"example":{"data":{"id":12,"sender_id":"PROMOCO","status":"pending","reason":null,"reviewed_at":null,"created_at":"2026-07-17T10:00:00+00:00"}}}}}}}},"\/v1\/sub-accounts":{"get":{"summary":"List sub-accounts","description":"Returns your sub-accounts with their balance mode and wallet balance. Requires the `sub-accounts` scope.","tags":["Sub-accounts"],"responses":{"200":{"description":"OK.","content":{"application\/json":{"example":{"data":[{"id":"019f-...","name":"Client One","email":"client.one@example.com","balance_mode":"isolated","status":"active","balance":"0.0000","currency":"GHS","created_at":"2026-07-17T10:00:00+00:00"}]}}}}}},"post":{"summary":"Create a sub-account","description":"Creates a client sub-account. Use `balance_mode: isolated` so the sub-account funds and spends from its own wallet (each client buys their own credit), or `shared` to allocate credit from your wallet via `allocation`. The sub-account is created pre-verified (email + phone) so it is usable immediately. Requires the `sub-accounts` scope.","tags":["Sub-accounts"],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["name","email","password","balance_mode"],"properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"},"password":{"type":"string","format":"password"},"balance_mode":{"type":"string","enum":["isolated","shared"]},"allocation":{"type":"number","description":"Initial credit from your wallet (shared mode only)."}}}}}},"responses":{"201":{"description":"Created.","content":{"application\/json":{"example":{"data":{"id":"019f-...","name":"Client One","email":"client.one@example.com","balance_mode":"isolated","status":"active","balance":"0.0000","currency":"GHS","created_at":"2026-07-17T10:00:00+00:00"}}}}},"422":{"description":"Validation error.","content":{"application\/json":{"example":{"error":{"code":"allocation_exceeds_balance","message":"Allocation exceeds your available balance."}}}}}}}},"\/v1\/sub-accounts\/{id}\/tokens":{"post":{"summary":"Issue a sub-account API token","description":"Creates an API token for one of your sub-accounts so it can call the API on its own behalf. The token is returned once. Requires the `sub-accounts` scope.","tags":["Sub-accounts"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application\/json":{"schema":{"type":"object","properties":{"name":{"type":"string","example":"client key"}}}}}},"responses":{"201":{"description":"Token created.","content":{"application\/json":{"example":{"data":{"sub_account_id":"019f-...","token":"hellio_xxxxxxxxxxxxxxxx","abilities":["sms:send","balance","payments","sender-ids"],"note":"Store this token securely \u2014 it is shown only once."}}}}}}}},"\/v1\/payments":{"post":{"summary":"Start a Paystack top-up","description":"Starts a Paystack checkout to top up the authenticated account's wallet (any account \u2014 a direct customer or a sub-account \u2014 tops up its own wallet) and returns the `authorization_url` to open in a browser. The wallet is auto-credited server-side by the Paystack webhook on success, independent of the browser redirect. Requires the `payments` scope.","tags":["Payments"],"requestBody":{"required":true,"content":{"application\/json":{"schema":{"type":"object","required":["amount"],"properties":{"amount":{"type":"number","description":"Amount to top up in GHS. Paystack fees are added at checkout.","example":100},"callback_url":{"type":"string","description":"Where Paystack returns the payer after checkout (optional)."}}}}}},"responses":{"201":{"description":"Checkout created.","content":{"application\/json":{"example":{"data":{"reference":"ps_abcdef0123456789","authorization_url":"https:\/\/checkout.paystack.com\/abcdef","amount":"100","status":"pending"}}}}}}}},"\/v1\/payments\/{reference}":{"get":{"summary":"Get a payment","description":"Returns the status of a payment you initiated (`pending`, `success`, `failed`). Requires the `payments` scope.","tags":["Payments"],"parameters":[{"name":"reference","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK.","content":{"application\/json":{"example":{"data":{"reference":"ps_abcdef0123456789","amount":"100.0000","status":"success","paid_at":"2026-07-17T10:05:00+00:00","created_at":"2026-07-17T10:00:00+00:00"}}}}}}}}}}