Skip to content
Nofy by Encipher Docs

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.

Python
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)

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

JSON
{
  "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_idstring

Stable id for this recipient on this channel. Merge on it.

statusenum<string>

sent or failed.

attemptsinteger

How many tries it took, including the one that succeeded.

provider_message_idstringnullable

The provider's id, for example Beem's request id.

delivered_atread_atstring<date-time>nullable

Filled in for WhatsApp from Meta's status webhook. A message the handset acknowledged stays sent, with these timestamps set.

last_errorstringnullable

The 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.

Esc
↑↓ to move↵ to open