Receiving Webhooks
Everything between an event happening and your server trusting it: endpoints, delivery and retries, dead letters, signature verification, and how much data each event carries.
Webhook & API Add-on · $29/month · Requires Creator+ planEndpoints
Configured in the portal under Integrations → Webhook & Send API. What lands where is entirely per endpoint: each has its own URL, event filters, signing secret, payload profile, and delivery settings.
Your endpoints
You can run up to 3 endpoints, for example production, staging, and a partner system. Every event is delivered independently to each enabled endpoint whose filters match it.
-
urlHTTPS only
Up to 2,048 characters. Must resolve to a public address: localhost, private, and reserved IP ranges are rejected, both when you save and again at delivery time.
-
event filters
Pick which event types this endpoint receives. No selection means all of them.
-
signing secretper endpoint
Each endpoint has its own 64-character secret, shown in the portal and rotatable at any time. See Verifying signatures.
-
payload profilefull | minimal
How much customer data this endpoint receives. See Payload profiles.
-
data-sharing declaration
Before an endpoint can be enabled you declare what the destination system is and why it receives customer messages, and attest to it. The declaration is per endpoint, so a partner system and your own backend each carry their own.
-
timeout · attempts
Response timeout defaults to 10 seconds (3 to 30) and retry attempts default to 6 (1 to 10), each adjustable per endpoint.
New endpoints start disabled so you can verify the secret and send test events before real traffic flows. Test events are available per endpoint from the portal.
{
"webhooks": {
"endpoints": [
{
"id": 7,
"name": "Production",
"url": "https://api.example.com/webhooks",
"enabled": true,
"event_filters": ["message.received", "lead.email_captured"],
"envelope_style": "standard"
},
{
"id": 9,
"name": "Staging",
"url": "https://staging.example.com/webhooks",
"enabled": false,
"event_filters": [],
"envelope_style": "standard"
}
]
}
}
Delivery
Delivery & retries
Events arrive as an HTTPS POST with a JSON body, typically within seconds of the event. Any 2xx response inside your endpoint's timeout counts as delivered; anything else is retried on a backoff schedule with ±10% jitter. At the default 6 attempts, retries span roughly 18 hours.
| Attempt | Delay after failure |
|---|---|
| 1 | immediate |
| 2 | ~5 seconds |
| 3 | ~5 minutes |
| 4 | ~30 minutes |
| 5 | ~2 hours |
| 6 | ~5 hours |
| 7+ | ~10 hours each (if attempts raised above 6) |
-
delivery idstable across retries
Every attempt for an event carries the same id (webhook-id, and X-SumGenius-Delivery-Id on legacy-signed deliveries). Deduplicate on it: a retry of something you already processed should be acknowledged with a 2xx and skipped.
-
410 Gone
Responding 410 stops delivery of that event immediately, no further retries.
Acknowledge fast, process async. Return 200 as soon as you have durably queued the event, then do the real work. Slow handlers hit the timeout and get retried, which shows up as duplicates on your side.
POST /webhooks HTTP/1.1
Host: api.example.com
Content-Type: application/json
User-Agent: SumGenius-Webhook/1.0
webhook-id: a9b8c7d6e5f40312a1b2c3d4e5f60718
webhook-timestamp: 1753848000
webhook-signature: v1,K6mGq0XcJ9...
{
"type": "message.received",
"timestamp": "2026-07-30T04:00:00+00:00",
"data": { ... }
}
// queue first, work later
app.post('/webhooks', (req, res) => {
queue.push(req.body);
res.sendStatus(200);
});
Dead letters & the delivery log
An event that exhausts its retries (or hits a permanent failure like a 410 or an invalid URL) becomes a dead letter. Nothing is silently dropped: the portal's Delivery Log records every attempt with its HTTP status, a snippet of your endpoint's response, and the request duration.
-
Retry
Re-queue a dead letter for delivery, individually or up to 50 at once. Retried events keep their original delivery id.
-
Dismiss
Acknowledge a dead letter you do not intend to redeliver. It leaves the actionable dead-letter count but stays in the log and still counts in the success-rate stats.
Events whose body was purged by Inbound Body Purge cannot be redelivered; their Retry action is disabled.
attempt 3
status 503
response "upstream unavailable"
duration 742 ms
next retry "in ~30 minutes"
Security
Verifying signatures
Every delivery is signed with the endpoint's own secret using HMAC-SHA-256. Verify before trusting a request; anyone can POST JSON at a public URL.
Which scheme you have: endpoints created since July 2026 sign in the Standard Webhooks format (webhook-id / webhook-timestamp / webhook-signature headers). Integrations set up before then receive both header sets on every delivery, the standard ones and the original X-SumGenius-* ones, so existing verifiers keep working and you can migrate to the standard check whenever you like.
Standard scheme
-
signed string
webhook-id + "." + webhook-timestamp + "." + raw body
-
signature
"v1," + base64(HMAC-SHA-256(signed string, secret)). The header can hold several space-separated signatures; the delivery is valid if any matches. Your secret is the 64-character string from the portal, used as-is.
-
timestamp
Unix seconds. Reject deliveries older than about 5 minutes to prevent replays.
Legacy scheme
-
headers
X-SumGenius-Signature, X-SumGenius-Timestamp, X-SumGenius-Event, X-SumGenius-Delivery-Id
-
signature
"sha256=" + hex(HMAC-SHA-256(timestamp + "." + raw body, secret))
Rotation grace. When you rotate an endpoint's secret, the previous secret stays valid for 24 hours. Standard-signed deliveries carry signatures from both secrets during that window, which is exactly why you verify against "any signature matches". Update your stored secret within the day and nothing ever fails.
Always verify against the raw request body, byte for byte, before any JSON parsing, and compare with a constant-time function.
$secret = getenv('SUMGENIUS_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$id = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$ts = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
// reject stale deliveries (~5 min)
if (abs(time() - (int)$ts) > 300) { http_response_code(400); exit; }
$expected = 'v1,' . base64_encode(hash_hmac(
'sha256', $id . '.' . $ts . '.' . $rawBody, $secret, true
));
$valid = false;
foreach (explode(' ', $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '') as $sig) {
if (hash_equals($expected, $sig)) { $valid = true; }
}
if (!$valid) { http_response_code(401); exit; }
http_response_code(200); // ack, then process async
const crypto = require('crypto');
app.post('/webhooks',
express.raw({ type: 'application/json' }),
(req, res) => {
const id = req.headers['webhook-id'];
const ts = req.headers['webhook-timestamp'];
if (Math.abs(Date.now() / 1000 - ts) > 300) return res.sendStatus(400);
const expected = 'v1,' + crypto
.createHmac('sha256', process.env.SUMGENIUS_WEBHOOK_SECRET)
.update(`${id}.${ts}.${req.body}`)
.digest('base64');
const valid = (req.headers['webhook-signature'] || '')
.split(' ')
.some(sig => sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
if (!valid) return res.sendStatus(401);
res.sendStatus(200); // ack, then process async
});
$expected = 'sha256=' . hash_hmac(
'sha256',
$_SERVER['HTTP_X_SUMGENIUS_TIMESTAMP'] . '.' . $rawBody,
$secret
);
$valid = hash_equals($expected, $_SERVER['HTTP_X_SUMGENIUS_SIGNATURE'] ?? '');
Data
Payload profiles
Each endpoint chooses how much customer data its events carry. The active profile is stamped on every event as meta.payload_profile.
-
fulldefault
Everything: customer display name and username, phone and profile name on SMS and WhatsApp, CRM enrichment (contact_id, tags, stage), conversation assignment, attachment URLs and captions.
-
minimal
Keeps ids and message text for routing; drops the customer fields above (everything except platform_user_id), conversation assignment, and per-attachment url, title, and caption (type and media ids remain).
Not profile-gated, deliberately: message.text_raw, message and delivery ids, the channel sub-blocks, and the lead and appointment blocks. A lead event exists to hand you contact info, so lead payloads are always complete.
{
"customer": {
"platform_user_id": "17845629031224067"
},
"message": {
"id": "m_mid.4823",
"text_raw": "Do you ship to Kyiv?",
"attachments": [
{
"type": "image",
"media_type": "image/jpeg",
"media_id": "18043762291",
"received_at": "2026-07-30T04:00:00+00:00"
}
]
},
"meta": {
"version": "v1",
"payload_profile": "minimal",
"generated_at": "2026-07-30T04:00:01+00:00"
}
}
Relay modes
Two optional modes for integrations where your system, not ChatGenius, is in charge of replies.
Global External Reply Mode
By default, ChatGenius AI and automation can still respond to incoming messages even when webhook forwarding is active. Global External Reply Mode applies a runtime override across all connected platforms, Facebook DM, Instagram DM, SMS, WhatsApp, and Telegram, so your external system becomes the sole responder.
When to use it
- You have your own AI or support system and want full control over every response on every platform
- You need to route different conversation types to different handlers
- You want ChatGenius purely as a message delivery and forwarding layer
What it does
- Pauses ChatGenius AI and automation replies on Facebook DM, Instagram DM, SMS, WhatsApp, and Telegram, while keeping all inbound forwarding, signing, retries, conversation storage, and the Send API fully working
- Auto-claims new incoming conversations to your account (across all platforms) so they appear as customer-managed in the portal
- Does not change your saved AI Assistant or Automation toggles in Settings. It is a runtime override that flips off when you disable External Reply Mode
How to enable
Toggle Global External Reply Mode in your portal under Integrations → Webhook & Send API. The change takes effect on the very next inbound message.
Per-platform fine-grained control
If you want AI on for some platforms but off for others (for example, AI for DMs but pure-relay for SMS and WhatsApp), leave Global External Reply Mode off and use each platform's individual AI toggle:
- Facebook / Instagram DMs: AI Assistant toggle in Settings → AI Configuration
- SMS: AI Auto-Responses in Settings → SMS
- WhatsApp: WhatsApp AI Assistant toggle in Settings → WhatsApp
- Telegram: Telegram AI Assistant toggle in Settings → Telegram
Any individual toggle being off pauses AI for that platform; Global External Reply Mode does the same thing for every platform at once: pause AI, keep forwarding.
When Global External Reply Mode is on, ChatGenius will not auto-respond to any inbound message. Make sure your webhook endpoint is live and your external system is ready to handle every conversation before flipping this on.
Inbound Body Purge After Forward
An optional per-account toggle for compliance scenarios where your server is the system of record and ChatGenius should not retain message bodies after forwarding. Designed for healthcare-adjacent and privacy-strict deployments.
Pure-relay only, incompatible with ChatGenius AI. This toggle clears the conversation context that AI uses to compose replies (recent message history, batched user inputs, follow-up scheduling state, active flow variables). With AI Assistant on, enabling this would cause incomplete or broken replies. Required configuration: SMS AI off, OR Global External Reply Mode on (which pauses AI across every connected platform). Use this only when your server handles every reply.
What it does
SMS only. Automatic body purge applies to inbound sms.message_received events only. Other channels, WhatsApp, Facebook DM, Instagram DM, and Telegram, are never auto-purged, regardless of this toggle.
When enabled, after we forward an inbound SMS message to your endpoint, we scrub:
- The message body text from stored conversation history
- The forwarded event's message.text_raw and message.attachments fields
- The staging queue entries holding the raw inbound SMS
What we retain
For audit and routing purposes:
- Message ID (Twilio MessageSid)
- Timestamp
- Sender identifier (E.164 phone number)
- Direction (inbound)
- Delivery status (delivered, failed, etc.)
- Ticket reference: if your endpoint returns a top-level ticket_ref string in its 2xx response body, we capture it. If missing or malformed, we still purge; the 2xx status is the only purge trigger.
One-shot delivery semantics
When the toggle is on, the forwarder uses max_attempts = 1 for SMS inbound events. We make exactly one HTTP attempt to your endpoint (with your endpoint's timeout) and then purge the body regardless of outcome, success or failure. This keeps the in-database lifetime tight for compliance.
Inbox impact
SMS threads will appear in the ChatGenius unified inbox as metadata-only entries: message ID, timestamp, sender, direction, delivery status, ticket reference. The message body is not displayed (because it is not stored). The inbox itself remains active so you can connect other channels (WhatsApp, Facebook DM, Instagram DM, Telegram) without reconfiguration.
Outbound message retention
Outbound message bodies (messages your server sends through us) are retained for 90 days for delivery troubleshooting, then automatically cleared for accounts using this compliance mode.
Backups
The nightly backup pipeline uses sanitized shadow tables for the body-bearing tables before the dump is compressed and replicated. For opted-in accounts, inbound SMS bodies are removed from backup data even if the live forward attempt is still pending when the snapshot starts. Metadata remains backup-recoverable, including message IDs, timestamps, sender identifiers, delivery state, and ticket references. Backup retention is 14 days local plus 14 days on Cloudflare R2.
Enabling it
Before flipping the toggle on, disable SMS AI, either:
- Per platform: turn off AI Auto-Responses in Settings → SMS.
- Or globally: enable Global External Reply Mode.
Then in Integrations → Webhook & Send API, flip Purge inbound message body after successful forward (SMS only). Owner-only. Default off. The change applies on the next inbound message.
Why these dependencies
The purger scrubs inbound bodies from conversation history, the webhook event queue, and the SMS staging table. ChatGenius AI normally pulls conversation history from those exact stores to compose replies, batch rapid-fire user inputs, schedule follow-ups, and drive flow logic. If the purger fires while AI is also running on the same conversation, AI's working memory disappears mid-stream and replies become incomplete or wrong. Disabling AI on SMS (or globally) ensures AI never tries to use what is about to be purged.
Once a body has been purged from a delivered or dead-letter event, you cannot redeliver that event from ChatGenius. The Retry buttons in the Delivery Log are disabled for purged events. Make sure your endpoint persists what it needs before responding 2xx.