Webhook guide
Verify Cariosan webhook signatures and implement idempotency in Go, Node, and Python.
Every webhook delivery arrives at your endpoint as a signed HTTP POST. Verifying the signature and implementing idempotency is on you — this guide shows how in three languages.
Request shape
Events
Subscribe to any of these on CreateWebhook. The envelope is identical to the example above; only data differs per event.
| Event | Fires when | data |
|---|---|---|
message.created | A message is sent | { message: { … } } |
message.updated | A message is edited | { message: { id, channel_id, channel_external_id, content, edited_at } } |
message.deleted | A message is unsent | { message: { id, channel_id, channel_external_id, deleted_at } } |
message.reaction.added | A reaction is added | { reaction: { message_id, channel_id, channel_external_id, user_id, emoji } } |
message.reaction.removed | A reaction is removed | { reaction: { … same shape } } |
message.mentioned | A message @-mentions ≥1 member | { message_id, channel_id, channel_external_id, by_user: { … }, mentions: [{ user_id, external_id, name }] } |
message.moderated | Automatic moderation acts on a message | { channel_id, channel_external_id, user_id, message_id?, rule, action, excerpt } |
channel.created / channel.deleted | A channel is created / deleted | { channel: { … } } |
member.joined / member.left | Channel membership changes | { channel_id, user_id } |
user.created | A user is provisioned | { user: { … } } |
Quoted replies
When a message.created event is a reply, its message object also includes parent_id (the quoted message's id) and a quoted_message preview — { id, user_id, user_name, content, type, deleted }. Both fields are absent on non-reply messages, so branch on their presence.
Mentions
When a message.created event @-mentions members, its message object includes a mentions array ([{ user_id, external_id, name }]). A dedicated message.mentioned event also fires once per mentioning message — subscribe to it specifically if you only care about routing mention notifications.
Signature scheme
- Algorithm: HMAC-SHA256.
- Pre-image:
<timestamp>.<raw body>. The body is the exact bytes you received, not a re-serialised version. - Key: the
signing_secretyou got once onCreateWebhook. - Encoding: hex, prefixed with
sha256=in the header.
Always use constant-time comparison — hmac.Equal (Go), crypto.timingSafeEqual (Node), hmac.compare_digest (Python). Non-constant-time comparison leaks timing information that can recover the HMAC byte-by-byte from a remote attacker.
Replay protection
Reject deliveries whose timestamp is more than 5 minutes older than your server clock. Same window, same idea as Stripe and Slack — tight enough to stop replays, loose enough to tolerate NTP skew.
Idempotency
Cariosan retries on 5xx / timeout. Same event_id can arrive twice. Your handler must be safe to apply twice.
Pattern — INSERT ... ON CONFLICT (event_id) DO NOTHING in SQL, or make the downstream effect naturally idempotent (upserts, set assignments). Either approach makes retries free.
Examples
Retry and failure handling
- 2xx → success, no further attempts.
- 4xx → terminal failure. Cariosan assumes your handler has a bug and won't retry.
- 5xx / timeout / network → retried at 30s, 2m, 10m, 1h, 6h with ±20% jitter, then marked
permanently_failed. - 24 consecutive failures → the webhook is auto-disabled.
4xx is terminal — fix the handler bug, then manually retry via POST /v1/webhooks/:id/deliveries/:delivery_id/retry. Cariosan won't retry 4xx automatically because doing so would just spin against a broken endpoint.
Use the delivery history endpoint to debug:
Tip
Each delivery record shows the attempt count, the server's response status, the truncated response body, and any network error — handy for tracking down silent handler failures.
Was this page helpful?