USSD
Stable
Verify requests
Every request to your backend is signed. Check the signature before you trust the phone number or anything else in it.
Why it matters
Your backend URL is public. Without the check, anyone who finds it could post a made-up
msisdn and act as one of your customers, for example to read their balance.The signature
Bash
X-Sendexa-Signature: t=1792513110,v1=5d41402abc4b2a76b9719d911017c592...
tis when the request was signed, in Unix seconds.v1is the HMAC-SHA256, in hex, of<t>.<raw request body>using your code's signing key.
To verify a request:
- Read the raw body as text, before any JSON parsing (re-encoding it changes the bytes).
- Compute the HMAC of
t + "." + bodywith your key and compare it to v1 in constant time. - Refuse it if t is more than 5 minutes from now, so an old request can't be replayed.
- On failure, return 401 and don't run your menu.
The check in your language
JavaScript
const crypto = require("crypto");function signatureIsValid(rawBody, header, key) {const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");if (!key || !match) return false;const [, t, v1] = match;if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;const expected = crypto.createHmac("sha256", key).update(`${t}.${rawBody}`).digest("hex");return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));}// Express: keep the raw body while parsing JSONapp.use(express.json({ verify: (req, _res, buf) => (req.rawBody = buf.toString("utf8")) }));// then: signatureIsValid(req.rawBody, req.get("x-sendexa-signature"), process.env.SENDEXA_SIGNING_KEY)
The complete servers in Sample code already do this, and include a script that sends signed test requests to your machine.
Your signing key
Each code has its own key. Find it in USSD → Codes → Backend & signing key. If it leaks, click Rotate and put the new key on your server. The old key stops working straight away, so rotate when you can update the server right then.