Webhooks & events
Server-to-server delivery of the same durable events your clients see on the socket. This is how your backend reacts to activity — moderation, analytics, your own push, CRM sync.
Subscribing #
curl -X POST https://api.example.com/v1/server/webhooks \
-H "Authorization: Bearer ak_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.example.com/hooks/ollacore",
"event_types": ["message.created","call.started","call.ended"]
}'
# → { "webhook_id": "…", "secret": "whsec_…" } ← shown once The secret is shown once. Store it — it is what proves a delivery came from us.
Event types #
| Event | Fires when |
|---|---|
message.created | A message is committed. The workhorse event. |
message.updated | A message is edited. |
message.deleted | A message is soft-deleted. |
receipt.delivered / receipt.read | A receipt is recorded. |
reaction.added / reaction.removed | A reaction changes. |
attachment.ready / attachment.failed | Upload verification finished. |
member.added / member.removed | Room membership changes. |
call.started / call.ended | A room's call begins / ends. Both carry the same call_id. |
Delivery shape #
POST /hooks/ollacore
x-ollacore-signature: t=1757500000,v1=9f86d081884c7d65…
{
"id": "evt_7f3a…",
"type": "message.created",
"tenant_id": "acct_…",
"room_id": "0f5c…",
"occurred_at": "2026-09-10T11:04:22Z",
"data": { … }
}
The id is an opaque event id — use it to deduplicate, because at-least-once delivery
means you can legitimately see the same event twice.
Verifying the signature #
HMAC-SHA256 over {timestamp}.{raw body} with your endpoint secret.
Verify against the raw body — parsing and re-serialising will change the bytes and
break the comparison.
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(parts.t + "." + rawBody)
.digest("hex");
// constant time, and reject anything older than ~5 minutes
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return ok && fresh;
} Retries and failure #
- Respond
2xxquickly — ideally enqueue and return. Slow endpoints are treated as failures. - Non-2xx or timeout is retried with exponential backoff over several hours, then given up.
- Every attempt is recorded:
GET /v1/server/webhooks/{id}/deliveries. - Replay any delivery by hand after you fix your side:
POST /v1/server/webhooks/{id}/deliveries/{delivery_id}/replay.
Endpoint rules #
Endpoints must be HTTPS and publicly resolvable. Private, loopback and link-local addresses are refused at creation and re-checked at delivery time, so a webhook cannot be used to probe the inside of the deployment's network. Redirects are not followed.
What people use this for #
- Your own push. Subscribe to
message.createdandcall.startedand send FCM/APNs from your app-server instead of storing credentials with us. - Ringing.
call.startedis the signal that a device which is not in the call should be rung. - Moderation and archiving. Mirror content into your own store or review queue.
- Sync. Keep a CRM, ticketing system or search index current.