Reference

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 #

EventFires when
message.createdA message is committed. The workhorse event.
message.updatedA message is edited.
message.deletedA message is soft-deleted.
receipt.delivered / receipt.readA receipt is recorded.
reaction.added / reaction.removedA reaction changes.
attachment.ready / attachment.failedUpload verification finished.
member.added / member.removedRoom membership changes.
call.started / call.endedA 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;
}
Always verify, and always check freshness. An unverified webhook endpoint is an unauthenticated write path into your system. Compare in constant time and reject stale timestamps so a captured delivery cannot be replayed against you later.

Retries and failure #

  • Respond 2xx quickly — 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.created and call.started and send FCM/APNs from your app-server instead of storing credentials with us.
  • Ringing. call.started is 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.