Delivery
Callbacks
Signed status posts for every terminal delivery state, how to verify them, and how to reconcile when your endpoint was down.
Pass callback_url on any v2 send and Nofy posts every terminal delivery
state to it: the fast path for your own records. The pull
(GET /v2/deliveries) is the guarantee; treat the posts as acceleration,
not the record.
Verifying posts
Every post carries X-ENS-Signature: the hex HMAC-SHA256 of the raw body
under your per-client callback secret. Your operator issues that secret
once. It is never the OAuth client secret, which is stored as a password
hash and would fail every verification.
import hashlib
import hmac
def valid(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
import crypto from "node:crypto";
export function valid(rawBody, signature, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(signature || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
<?php
function valid(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
Reject anything that does not verify
Compare in constant time (hmac.compare_digest, timingSafeEqual,
hash_equals), never with ==, and reject posts with a missing or
mismatched signature before parsing the body.
Clients without an issued secret get no posts at all: an unverifiable callback looks like assurance and provides none.
Payload
{
"delivery_id": "dL91xTo874Y8",
"reference": "shift-8841",
"channel": "sms",
"recipient": "255700000000",
"status": "sent",
"attempts": 1,
"provider_message_id": "beem-req-42",
"sent_at": "2026-09-25T10:01:00+00:00",
"delivered_at": null,
"read_at": null,
"last_error": null
}
delivery_idstringStable id for this recipient on this channel. Merge on it.
statusenum<string>sent or failed.
attemptsintegerHow many tries it took, including the one that succeeded.
provider_message_idstringnullableThe provider's id, for example Beem's request id.
delivered_atread_atstring<date-time>nullableFilled in for WhatsApp from Meta's status webhook. A message the handset
acknowledged stays sent, with these timestamps set.
last_errorstringnullableThe provider's reason on a permanent failure.
Reconciliation
Callbacks are best-effort with retries. If your endpoint was down, walk
GET /v2/deliveries?updated_since=<last-seen> page by page with cursor
and merge by delivery_id. Every attempt restamps the row, so nothing
that moved is missing from the pull.