Cariosan

Webhooks

Overview of Cariosan's event delivery system and when to use it.

5 min readUpdated Apr 18, 2026

Webhooks are Cariosan's extension point for anything the core server doesn't do itself — push notifications, audit logs, moderation, CRM sync, analytics. Instead of baking integrations into the server, Cariosan emits signed HTTP POSTs to URLs you register, and you do whatever you want on the other end.

The events

Each event fires exactly when the source-of-truth state changes — committed to Postgres — never on the optimistic write that hasn't reached durable storage yet. See the webhook guide for the full data shape of each.

EventFires when
message.createdA user posts a message (REST or WebSocket path)
message.updatedA message is edited
message.deletedA message is unsent
message.reaction.added / message.reaction.removedA reaction changes on a message
message.mentionedA message @-mentions one or more members
message.moderatedAutomatic moderation flags/masks/rejects a message
channel.createdA new channel is created
channel.deletedA channel is deleted
member.joinedA user is added to a channel
member.leftA user is removed from a channel
user.createdA new user is inserted (upsert on an existing external_id does not fire this)

Why so few?

High-frequency ephemeral events — typing.*, presence.*, and message.read — are deliberately not webhook-delivered. They'd overwhelm any consumer. Subscribe to the WebSocket gateway directly if you need them.

Registering a webhook

Webhook subscriptions are workspace-scoped. The signing secret is returned once at creation; store it securely.

register.go
wh, _ := client.CreateWebhook(ctx, cariosan.CreateWebhookRequest{
    URL: "https://api.example.com/cariosan/events",
    Events: []cariosan.EventType{
        cariosan.EventMessageCreated,
        cariosan.EventChannelCreated,
    },
})
// Store wh.SigningSecret somewhere secure — it's only returned once.

Reliability model

  • At-least-once delivery. Consumers must be idempotent, keyed by event_id in the payload.
  • Retries on 5xx / timeout / network error with exponential backoff at 30s, 2m, 10m, 1h, 6h (five attempts total).
  • 4xx is terminal. The server assumes a 4xx means your endpoint has a bug and won't retry.
  • Auto-disable. After 24 consecutive failures (roughly a day of every-delivery-fails), the webhook is disabled and subsequent events skip it. Re-enable with PATCH /v1/webhooks/:id.

Watch your tail latencies

Slow webhook responses block the worker pool. If your handler can't return in under 5 seconds, queue the work asynchronously and respond 200 immediately.

De-dup by event_id

At-least-once means you'll occasionally see the same event twice (during retries or partial failures). Store the last 1000 event_ids your endpoint has processed and short-circuit on hit.

Re-enabling a disabled webhook

Once auto-disabled, re-enable with:

reenable.go
client.UpdateWebhook(ctx, webhookID, cariosan.UpdateWebhookRequest{
    Enabled: cariosan.Bool(true),
})

Tip

Pair re-enable with a deploy-time hook so a hot-fixed bug doesn't leave subscribers stuck disabled. The admin UI lists every disabled webhook and the failure that caused it.

What's in the payload

Every delivery is a JSON POST with the same envelope: a stable event_id, the event type, an ISO-8601 timestamp, the workspace_id, and a data object whose shape depends on the event type. Two HTTP headers — X-Cariosan-Signature and X-Cariosan-Timestamp — let you verify authenticity with HMAC-SHA256.

json
{
  "event_id": "evt_2a9f...",
  "type": "message.created",
  "timestamp": "2026-04-18T09:30:00Z",
  "workspace_id": "ws_1b2c...",
  "data": {
    "message": {
      "id": "msg_7d8e...",
      "channel_id": "chn_3f4a...",
      "user_id": "usr_9c0d...",
      "type": "text",
      "content": "Halo!",
      "created_at": "2026-04-18T09:30:00Z"
    }
  }
}

Tip

Reject any request whose X-Cariosan-Timestamp is older than 5 minutes. This blocks replay attacks even if an attacker captures a valid signature.

See the webhook guide for the full payload shape per event type, HMAC verification code in Go / Node / Python, and a replay-protection checklist.

Was this page helpful?

On this page