Updates to your systems
Renotify can tell your own systems what happened after an event: a message was delivered, read or failed, a customer reached an automation’s goal, or changed their consent. It sends each update as a signed HTTPS POST, a webhook.
Updates to your systems are on the Business and Agency plans. Sending events to Renotify works on every plan; see Sending events.
Add an endpoint
Section titled “Add an endpoint”- Go to Settings → Developers → Webhooks and choose Add endpoint: an HTTPS URL on your server, with an optional description such as “CRM sync”.
- Choose the events it should receive.
- Copy its signing secret and store it with your receiver. It’s shown only once; Roll signing secret creates a new one if it’s lost.
- Use Send test to check your endpoint answers.
Events
Section titled “Events”| Event | Sent when |
|---|---|
message.sent |
A message was accepted by the channel. |
message.delivered |
A message reached the customer. |
message.read |
The customer read a message. |
message.failed |
A message could not be delivered. |
message.suppressed |
A message was not sent because of consent or suppression. |
link.clicked |
The customer opened a tracked link for the first time. |
outcome.recorded |
A workflow reached its goal. |
consent.granted |
A contact agreed to marketing on a channel. |
consent.revoked |
A contact withdrew marketing consent on a channel. |
alert.triggered |
Delivery monitoring raised an alert. |
The request
Section titled “The request”POST /renotify/webhooks HTTP/1.1Content-Type: application/jsonUser-Agent: Renotify-Webhooks/1.0Renotify-Event: message.failedRenotify-Delivery: 0f6b6c2e-3a51-4a8e-9a1f-2b7d4c8e5f10Renotify-Signature: t=1727600000,v1=5257a869…Every webhook has the same envelope:
{ "id": "0f6b6c2e-3a51-4a8e-9a1f-2b7d4c8e5f10", "type": "message.failed", "created_at": "2026-10-01T09:02:11Z", "data": { }}Message events
Section titled “Message events”"data": { "id": "b3c1…", "status": "failed", "channel": "whatsapp", "recipient": "+15551234567", "template": "payment_due_reminder", "workflow": { "id": 12, "name": "Payment reminder", "run_id": 8831, "step": "first_reminder" }, "campaign_id": null, "fallback_from": null, "failure": { "code": "not_on_channel", "reason": "Not reachable on this channel", "fix": "Add a fallback channel such as SMS or email." }, "contact": { "id": 311, "external_id": "cus_1042", "name": "Jane Doe", "phone": "+15551234567", "email": "jane@example.com" }, "sent_at": "2026-10-01T09:00:03Z", "delivered_at": null, "read_at": null, "failed_at": "2026-10-01T09:00:05Z"}workflow is set for messages from automations, campaign_id for broadcasts. fallback_from is the ID of the message this one replaced when a route moved to the next channel. failure.code is one of the failure reasons.
outcome.recorded
Section titled “outcome.recorded”"data": { "id": 921, "goal": "payment.completed", "workflow": { "id": 12, "name": "Payment reminder", "run_id": 8831 }, "credited_message_id": "b3c1…", "channel": "sms", "contact": { "id": 311, "external_id": "cus_1042", "name": "Jane Doe", "phone": "+15551234567", "email": "jane@example.com" }, "event": { "id": 8840, "data": { "amount": "$120.00" } }, "occurred_at": "2026-10-02T14:31:00Z"}consent.granted and consent.revoked
Section titled “consent.granted and consent.revoked”"data": { "channel": "whatsapp", "status": "revoked", "source": "keyword", "contact": { "id": 311, "external_id": "cus_1042", "name": "Jane Doe", "phone": "+15551234567", "email": "jane@example.com" }}Fields are only ever added to these payloads, never removed or renamed, so ignore fields you don’t use.
Verify the signature
Section titled “Verify the signature”The Renotify-Signature header has a timestamp t and a signature v1: the HMAC-SHA256, in hex, of {t}.{raw body} with your endpoint’s signing secret. Check it against the raw request body, before parsing the JSON, and reject timestamps older than five minutes.
import crypto from 'node:crypto';
export function verifyRenotify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(',').map((part) => part.split('='))); const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; const valid = expected.length === parts.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
return fresh && valid;}function verifyRenotify(string $rawBody, string $header, string $secret): bool{ parse_str(str_replace(',', '&', $header), $parts); $expected = hash_hmac('sha256', "{$parts['t']}.{$rawBody}", $secret);
return abs(time() - (int) $parts['t']) < 300 && hash_equals($expected, $parts['v1'] ?? '');}Responding and retries
Section titled “Responding and retries”Answer with any 2xx status within 10 seconds. Do slow work after responding, in a queue.
If your endpoint fails or times out, Renotify retries with increasing delays for about a day (seven attempts in all). Use the Renotify-Delivery header, or the envelope’s id, to ignore a delivery you’ve already handled.
After 25 failed deliveries in a row, the endpoint is switched off and shown as such in the app and under Operations. Fix it and switch it back on.
Each endpoint’s recent deliveries, with the response your server gave, are listed in the app. Redeliver sends one again.