Reliability
Delivery webhooks
Register an endpoint with POST /v1/webhooks/endpoints and Sendozi posts delivery events to it as networks report them. Each endpoint has a signing secret, shown once at creation, which you use to verify every payload before acting on it.
Managing endpoints
| Method and path | Purpose |
|---|---|
| GET /v1/webhooks/endpoints | List your endpoints |
| POST /v1/webhooks/endpoints | Create one. The signing secret is returned once. |
| PATCH /v1/webhooks/endpoints/{id} | Update the URL or the subscribed events |
| DELETE /v1/webhooks/endpoints/{id} | Remove it |
| POST /v1/webhooks/endpoints/{id}/test | Send a test payload to it |
curl -X POST https://api.sendozi.com/v1/webhooks/endpoints \
-H "Authorization: Bearer $SENDOZI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/hooks/sendozi",
"events": ["message.delivered", "message.failed"]
}'
curl -X POST https://api.sendozi.com/v1/webhooks/endpoints/whk_example/test \
-H "Authorization: Bearer $SENDOZI_API_KEY"Events
| Event | Meaning |
|---|---|
| message.delivered | A network confirmed the handset received the message |
| message.failed | The route or the network rejected it, or it expired undelivered |
Subscribe to both. An integration that listens only for success cannot distinguish a failure from an event that has not arrived yet.
Verifying a payload
Your endpoint is a public URL that anyone can POST to. Compute HMAC-SHA256 over x-sendozi-timestamp + "." + raw request body using the endpoint's signing secret, compare it in constant time with x-sendozi-signature, and reject anything that does not match before parsing the body.
import express from "express";
import crypto from "node:crypto";
const app = express();
const SECRET = process.env.SENDOZI_WEBHOOK_SECRET;
app.post("/hooks/sendozi", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("x-sendozi-timestamp") ?? "";
const provided = Buffer.from(req.get("x-sendozi-signature") ?? "");
const computed = Buffer.from(crypto.createHmac("sha256", SECRET).update(timestamp + "." + req.body).digest("hex"));
if (provided.length !== computed.length || !crypto.timingSafeEqual(provided, computed)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString("utf8"));
res.status(200).end(); // acknowledge fast
void handleAsync(event); // then do the work
});<?php
// routes/web.php - exclude this route from CSRF verification.
Route::post('/hooks/sendozi', function (Illuminate\Http\Request $request) {
$raw = $request->getContent();
$signed = $request->header('x-sendozi-timestamp') . '.' . $raw;
$expected = hash_hmac('sha256', $signed, config('services.sendozi.webhook_secret'));
if (! hash_equals($expected, (string) $request->header('x-sendozi-signature'))) {
abort(401);
}
ProcessSendoziDelivery::dispatch(json_decode($raw, true));
return response()->noContent();
});Rules for a receiver that holds up
- Respond in milliseconds
- Acknowledge with 200, queue the event, and process out of band. A receiver that does database work inline will time out during a campaign.
- Be idempotent
- Treat an event id as unique and make a repeat a no-op. This lets you safely retry test deliveries and any event your own receiver replays.
- Tolerate reordering
- A delivered event can arrive before the sent event that preceded it. Order by the timestamps in the payload, not by arrival.
- Never trust the payload as authorisation
- A valid signature proves the event came from Sendozi. It does not authorise action on an account - look the message up against your own records first.
- Return 5xx to ask for a retry
- If you cannot process an event, fail loudly. A 200 tells Sendozi the event was handled, and it will not come back.
- Log the request_id
- It ties the event to the original send, which is what support needs when a delivery is disputed.
If you cannot receive webhooks
Some environments cannot expose a public endpoint. In that case poll GET /v1/messages with a cursor, keyed on created_at, and store the last cursor you processed. Poll on a schedule measured in minutes rather than seconds: delivery reports take as long as they take, and a tight loop only consumes your rate limit.
curl "https://api.sendozi.com/v1/messages?limit=100&cursor=2026-08-21T09:00:00.000Z" \
-H "Authorization: Bearer $SENDOZI_API_KEY"Frequently asked questions
- How do I verify a Sendozi webhook?
- Compute an HMAC-SHA256 over the raw request body using the endpoint's signing secret, and compare it in constant time with the signature header. Reject mismatches before parsing the body. Use the endpoint's test call to confirm your receiver works.
- Which events does Sendozi send?
- message.delivered and message.failed. Subscribe to both, so a failure is not indistinguishable from silence.
- Will the same webhook event arrive twice?
- It can. Retries exist so a temporary outage on your side does not lose an event, which means your handler must be idempotent - key on message id and event type.
- What should my endpoint return?
- 200 as soon as you have accepted the event, then process asynchronously. Return 5xx if you genuinely could not accept it, so it is retried.
- Can I test a webhook before going live?
- Yes. POST /v1/webhooks/endpoints/{id}/test sends a payload to your endpoint so you can confirm signature verification and handling before real traffic arrives.
Related reading
Getting started
Quickstart: send your first SMS
Get an API key, send a sandbox SMS, read the response envelope, register a delivery webhook and move to production. A complete first integration in one page.
Channels
SMS API reference
Endpoints, request fields, routing, batching, sender ID management and page-based cost for sending SMS to Nigerian numbers through the Sendozi API.
Reliability
Errors and the response envelope
The Sendozi response envelope, every error code with its status and meaning, which errors are worth retrying, and how request_id is used to trace a failure.
Reliability
Idempotency
How Idempotency-Key works on Sendozi send endpoints, what happens on a repeat with the same or a different body, how long keys are retained, and how to choose a key.