Webhooks#
Register an https URL and Barua posts each delivery outcome to it as it is recorded. Polling the log every minute is the alternative, and nobody builds it. Up to 20 endpoints per account, each subscribed to the events it wants:
Every POST is JSON: . is the delivery id, repeated in the header, so a retry you have already handled can be dropped. For email events carries , , , , , and ; a ping's is . The other headers are (the type) and .
{
"id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"type": "email.bounced",
"createdAt": "2026-09-24T08:16:02.000Z",
"data": {
"messageId": "1f2e3d4c-5b6a-4798-8a9b-0c1d2e3f4a5b@dukalangu.co.tz",
"recipient": "juma@example.co.tz",
"status": "bounced",
"dsn": "5.1.1",
"detail": "550 5.1.1 The email account that you tried to reach does not exist",
"occurredAt": "2026-09-24T08:15:41.000Z",
"from": "receipts@dukalangu.co.tz"
}
}
Verifying. The signature header is , where is HMAC-SHA256 over with the endpoint's secret, which is shown once when the endpoint is created and never again. Sign the raw bytes as received, before any JSON parsing, and compare with a constant-time function: a plain string comparison leaks how many bytes matched.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
// The signature covers the bytes exactly as sent, so keep the raw body and
// parse it only after the check has passed.
app.post("/hooks/barua", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Barua-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const { t, v1 } = parts;
if (!t || !v1) return res.status(400).end();
// Your choice, not a Barua rule: refuse a signature older than five minutes,
// so a captured request cannot be replayed at you later.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.status(400).end();
const rawBody = req.body.toString("utf8");
const expected = createHmac("sha256", process.env.BARUA_WEBHOOK_SECRET)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(v1, "hex");
if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(401).end();
const event = JSON.parse(rawBody);
// event.type is "email.bounced" and so on. event.id is the delivery id, also
// in X-Barua-Delivery, so a retry you have already handled can be dropped.
if (event.type === "email.bounced") {
// mark event.data.recipient as undeliverable in your own records
}
// Answer 2xx within 10 seconds. Anything else, or silence, is retried.
res.status(200).end();
});
app.listen(3000);
Retries. The dispatcher runs once a minute, so the first attempt lands within about a minute of the event. Any 2xx within 10 seconds counts as delivered; the body of your response is never read and redirects are not followed. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours: 6 attempts over about fifteen hours, long enough to fix a deploy. After 10 consecutive failures at one URL the endpoint is switched off (, set) and nothing more is posted to it; its pending deliveries wait. A successful test ping switches it back on. Pings are never retried, so testing against a server you are still fixing does not count against the endpoint.
The deliveries list answers why your system did not hear about a bounce: the status your server returned, or why it never answered (, , , and so on), and when the next try is.
GET /api/v1/webhooks
List webhook endpoints. Scope .
curl https://barua.tz/api/v1/webhooks \
-H "Authorization: Bearer barua_YOUR_KEY"
POST /api/v1/webhooks
Create a webhook endpoint. Scope .
The URL must be https and point at a public host; it is resolved at creation and refused if any address is private. Up to 20 endpoints per account. The response carries the signing secret, and this is the only time it is shown: only ciphertext is stored and no endpoint reads it back. A lost secret means a new endpoint.
curl https://barua.tz/api/v1/webhooks \
-H "Authorization: Bearer barua_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://shop.dukalangu.co.tz/hooks/barua",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
]
}'
201
{
"id": "e4d3c2b1-a098-4765-b432-10fedcba9876",
"url": "https://shop.dukalangu.co.tz/hooks/barua",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
],
"active": true,
"failureCount": 0,
"disabledAt": null,
"createdAt": "2026-09-24T10:20:00.000Z",
"secret": "whsec_5Yt2Kq8ZbP0mV7xL3nJ9cW1dR4hF6gT8uA2sE5yB7iO"
}
GET /api/v1/webhooks/{id}
Get a webhook endpoint. Scope .
curl https://barua.tz/api/v1/webhooks/e4d3c2b1-a098-4765-b432-10fedcba9876 \
-H "Authorization: Bearer barua_YOUR_KEY"
200
{
"id": "e4d3c2b1-a098-4765-b432-10fedcba9876",
"url": "https://shop.dukalangu.co.tz/hooks/barua",
"events": [
"email.delivered",
"email.bounced",
"email.complained"
],
"active": true,
"failureCount": 0,
"disabledAt": null,
"createdAt": "2026-09-24T10:20:00.000Z"
}
POST /api/v1/webhooks/{id}/test
Post a ping and wait for the answer. Scope .
Posts a ping event now and returns what your server answered, so a signature check can be seen passing or failing here rather than on the first real bounce. A ping that succeeds also switches back on an endpoint that was disabled after 10 consecutive failures. Pings are never retried, and one endpoint accepts one test every 10 seconds.
curl -X POST https://barua.tz/api/v1/webhooks/e4d3c2b1-a098-4765-b432-10fedcba9876/test \
-H "Authorization: Bearer barua_YOUR_KEY"
200
{
"id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"status": 200,
"delivered": true,
"error": null
}
GET /api/v1/webhooks/{id}/deliveries
List deliveries to an endpoint. Scope .
What was posted to this endpoint and how it went, newest first: the status your server returned, or the reason it never answered, and when the next try is.
curl "https://barua.tz/api/v1/webhooks/e4d3c2b1-a098-4765-b432-10fedcba9876/deliveries?limit=10" \
-H "Authorization: Bearer barua_YOUR_KEY"
DELETE /api/v1/webhooks/{id}
Delete a webhook endpoint. Scope .
Stops posting to the URL now and deletes every delivery queued or kept for it.
curl -X DELETE https://barua.tz/api/v1/webhooks/e4d3c2b1-a098-4765-b432-10fedcba9876 \
-H "Authorization: Bearer barua_YOUR_KEY"