Skip to content
Browse the docs

Webhooks for calls

Updated

On this page

OneCamp can tell your own systems when a call begins and ends. An outgoing webhook set up for call.started and call.ended is sent an event when a call in a channel or a meeting starts, and another when it ends, with its times, how long it ran and how many people joined. Use them to log calls, count meeting time, or start your own follow-up when a meeting ends.

Set one up

Only admins can set up webhooks.

  1. Go to Admin → Webhooks and choose New webhook.
  2. Choose Outgoing, give it a name, and enter the address to send events to. It must start with https://.
  3. Under Events from, choose The whole workspace or One channel. A webhook set to one project is sent no calls.
  4. Under call, tick call.started, call.ended, or both. You can tick other kinds too: the dialog lists every kind of event a webhook can be sent.
  5. Choose Create webhook.

A webhook is sent only the kinds of event it names, so it needs at least one. Until one is ticked, Create webhook and Save changes wait, and OneCamp refuses a webhook with none: "Choose at least one event to send: a webhook with none is sent nothing."

Older webhooks could be made with no kinds ticked, and they were sent nothing. Admin → Webhooks lists such a webhook as Sends nothing: no kinds ticked. Choose Edit beside it and tick the kinds it should be sent.

Which calls are sent

  • Calls in channels and in meetings that the person who set up the webhook can open: a channel they can read, or a meeting they host or were let into. OneCamp checks this each time it sends, so calls stop reaching the webhook once that person loses access.
  • A webhook set to one channel is sent that channel's calls. A meeting's calls go only to webhooks set to the whole workspace.
  • A call in a direct message or a group chat is never sent.
  • A breakout room sends nothing of its own: it is part of its main call.

An event carries ids, times and counts. It never carries a name, a title or anything said in the call.

What an event looks like

Each event is a JSON POST to the webhook's address:

{
  "event": "call.ended",
  "timestamp": "2026-11-02T10:31:07Z",
  "data": {
    "session_id": "3f2b8c1e-5d4a-4f6b-9c2d-7e8f9a0b1c2d",
    "meeting_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "started_at": "2026-11-02T10:00:02.418329Z",
    "ended_at": "2026-11-02T10:31:06.902114Z",
    "duration_s": 1864,
    "people": 6,
    "reason": "last_left"
  }
}

event is the kind, timestamp is when OneCamp sent the event out (UTC, the same on every try), and data holds the call's fields:

Field Sent in What it is
session_id both The call. Its call.started and call.ended carry the same id; a call started again later has a new one.
channel_id both, for a call in a channel The channel the call is in.
meeting_id both, for a call in a meeting The meeting the call is in.
actor_uuid both, when someone did it Who started the call, or the host who ended it for everyone. Absent when a call ended by itself.
started_at both When the call began (RFC 3339, UTC).
ended_at call.ended When it ended (RFC 3339, UTC).
duration_s call.ended How long it ran, in whole seconds.
people call.ended How many people joined it.
reason call.ended Why it ended (below).

Why a call ended:

  • host_ended: a host ended it for everyone.
  • last_left: the last person in it left.
  • room_finished: the call's media room closed.
  • reconciled: OneCamp found the call had already ended, for example after a restart, and closed it.

A call starting in a channel:

{
  "event": "call.started",
  "timestamp": "2026-11-02T10:00:03Z",
  "data": {
    "session_id": "3f2b8c1e-5d4a-4f6b-9c2d-7e8f9a0b1c2d",
    "channel_id": "c9f0f895-fb98-4b91-8e1f-2d3c4b5a6978",
    "actor_uuid": "45c48cce-2e2d-4fbd-a2b1-6f7e8d9c0b1a",
    "started_at": "2026-11-02T10:00:02.418329Z"
  }
}

Receiving events

  • Answer with any 2xx within 10 seconds. Any other answer, or none, is a failed try.
  • A failed delivery is tried again, up to 10 tries in all over about a day: after 10 seconds, 1 minute, 5 minutes and 30 minutes, then 1, 2, 4, 6 and 8 hours, each a little later at random.
  • After 50 failed tries in a row, the webhook pauses itself and OneCamp tells the person who set it up. Admin → Webhooks says why, with Resume.

Each delivery carries these headers:

Header What it is
X-OneCamp-Event The kind, such as call.started.
X-OneCamp-Webhook-Id The webhook's id.
X-OneCamp-Delivery The event's id, the same on every try, so you can drop a repeat.
X-OneCamp-Retry-Num 0 on the first try, then 1, 2 and so on.
X-OneCamp-Timestamp When this try was signed, in Unix seconds.
X-OneCamp-Signature v1= and the signature (below).

Check the signature

Each outgoing webhook gets its own signing secret when it is made. Admin → Webhooks shows it, and can regenerate it.

The signature is the HMAC-SHA256, in lowercase hex, of v1:<timestamp>:<body>, keyed with the secret, where <timestamp> is the X-OneCamp-Timestamp header and <body> is the raw request body. Compute it over the body's bytes exactly as they arrived, compare in constant time, and refuse an old timestamp (for example, one more than five minutes old) so a captured request can't be replayed.

Python:

import hashlib
import hmac


def verify(body: bytes, signature: str, timestamp: str, secret: str) -> bool:
    version, _, received = signature.partition("=")
    if version != "v1":
        return False
    expected = hmac.new(secret.encode(), f"v1:{timestamp}:".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received)

Node.js:

const crypto = require("crypto")

// body is the raw request body, as a Buffer.
function verify(body, signature, timestamp, secret) {
  const [version, received] = signature.split("=")
  if (version !== "v1" || !received) return false
  const expected = crypto.createHmac("sha256", secret).update(`v1:${timestamp}:`).update(body).digest("hex")
  return received.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
}

Something missing or wrong in this guide? Write to support@onemana.dev.