One webhook for your USSD
Point your USSD extension at a single fixed URL. Every time someone uses your short code, your provider sends a POST here with session and input fields. You return CON or END plus the line of text the phone should show. Implement production menus in lib/ussd/handler-product.ts. This page is the contract your engineers need; bookmark ussd.pewang.company.
serviceCode if you add more short codes later. Demo vs production: the IBC-style test menu lives in lib/ussd/handler-demo.ts and runs on local dev and Vercel Preview by default. On Vercel Production the demo is off unless you set USSD_USE_DEMO_MENU=true — live traffic uses lib/ussd/handler-product.ts only, so the demo does not affect your real launch unless you opt in.Callback URL (set once in the provider dashboard)
Example path in the portal: Shared USSD → Update Extension → callback / webhook URL. Use exactly:
https://pewang.company/api/ussdPOST only for live traffic. Body: HTML form fields (not JSON). Must be reachable over the public internet with HTTPS.
Discovery (optional)
GET https://pewang.company/api/ussd returns JSON with the same field names and CON/END rules — useful for gateways or internal tooling, not for handsets.
Request body (form fields)
Content type: application/x-www-form-urlencoded or multipart/form-data.
| Field | Meaning | Example |
|---|---|---|
sessionId | Unique id for this USSD session (required). | 88867484 |
networkCode | Mobile network identifier from the carrier. | 1 |
serviceCode | The short code the subscriber dialled. | *657*7778# |
phoneNumber | Subscriber MSISDN (required). | 254722002222 |
text | All inputs so far, separated by *. Empty on the first hit. | 1 → 1*1234 → 1*1234*5000 |
Most flows only need to split text on * to know which step you are on. Use sessionId if you add server-side session storage (for example Redis).
Response (plain text)
One line of plain text. It must start with CON or END, a space, then the user-visible message.
| Prefix | When to use | Example |
|---|---|---|
CON | Show another menu or ask for more input. | CON Enter your PIN |
END | Close the session (success or final message). | END Thank you. Goodbye. |
In this codebase you return { continueSession, message } from handleUssd; the HTTP route adds CON or END for you.
Where you plug in your product
lib/ussd/handler-product.ts— production menus, validation, calls to your APIs or database.lib/ussd/handler-demo.ts— IBC-style sample menu for testing only (not used in Vercel production by default).lib/ussd/handler.ts— chooses demo vs product fromVERCEL_ENVandUSSD_USE_DEMO_MENU.lib/ussd/types.ts— inbound/outbound types.app/api/ussd/route.ts— parses the form POST and formats the reply.lib/ussd/public-config.ts— fixed public callback URL and doc constants.
Optional Bearer token
If the server has USSD_WEBHOOK_SECRET set, every POST must include Authorization: Bearer <same value>. Only turn this on if your provider can send that header. If it is unset, only the URL and TLS protect the endpoint.
Try it with curl (local)
First screen — empty text:
curl -sS -X POST 'http://localhost:3000/api/ussd' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'sessionId=demo-1' \
--data-urlencode 'networkCode=1' \
--data-urlencode 'serviceCode=*657*7778#' \
--data-urlencode 'phoneNumber=254722002222' \
--data-urlencode 'text='Change text to 1 or 1*next to simulate later steps. The sample uses serviceCode *657*7778#; your gateway will send the real code your extension uses.
To hit production, use the same form body but POST to https://pewang.company/api/ussd.