Skip to content

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.

  1. Go to Settings → Developers → Webhooks and choose Add endpoint: an HTTPS URL on your server, with an optional description such as “CRM sync”.
  2. Choose the events it should receive.
  3. 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.
  4. Use Send test to check your endpoint answers.
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.
POST /renotify/webhooks HTTP/1.1
Content-Type: application/json
User-Agent: Renotify-Webhooks/1.0
Renotify-Event: message.failed
Renotify-Delivery: 0f6b6c2e-3a51-4a8e-9a1f-2b7d4c8e5f10
Renotify-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": { }
}
"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.

"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"
}
"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.

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'] ?? '');
}

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.