Inbound webhooks
Every MO (mobile-originated) message that matches your keyword or dedicated code is forwarded to your HTTPS endpoint as JSON, as it arrives.
POSThttps://your-app.example.com/inbound (your configured URL)
Payload
{
"provider": "safaricom",
"shortcode": "40770",
"msisdn": "254712345678",
"message": "JOIN Amina Odhiambo Nakuru",
"link_id": "8f3a12bc45",
"received_at": "2026-07-11T10:15:42+03:00"
}Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| provider | string | No | Carrier the message arrived from (safaricom, airtel, …). |
| shortcode | string | No | The destination — your shortcode or long number. |
| msisdn | string | No | The sender's phone, E.164 without +. |
| message | string | No | The full text, keyword included. |
| link_id | string | No | Safaricom LinkID — needed if you reply through on-demand shortcode flows; absent on other carriers. |
| received_at | string | No | RFC3339 timestamp. |
Your response
Return any 2xx quickly. The body is ignored — replies are sent via the send API or per-keyword auto-replies, not the webhook response.
Design for retries and bursts
- Treat delivery as at-least-once — dedupe on
msisdn+received_at(or hash the payload). - A radio mention can spike hundreds of texts a minute — queue internally, respond 200 immediately, process async.
- HTTPS is required; keep the endpoint path unguessable if you don't validate a shared secret.
A minimal receiver
Node.js (Express)
app.post("/inbound", (req, res) => {
res.sendStatus(200); // ack first, work later
const { msisdn, message, shortcode } = req.body;
const [keyword, ...rest] = message.trim().split(/\s+/);
queue.add("inbound-sms", {
from: msisdn,
keyword: keyword.toUpperCase(),
details: rest.join(" "),
via: shortcode,
});
});