Skip to content

Webhooks

So that you never have to ask whether something is done: Protoki calls an address you own as soon as the minutes and tasks of a meeting exist. You set the address up in the admin area under Webhooks. The signing secret is shown exactly once.

There are two events. The field tables, the headers, the retry schedule and the rules for answering are on their own pages:

A newly registered endpoint gets meeting.ready only. meeting.failed is added by hand in the admin area, so that an automation that already runs does not suddenly receive calls without minutes.

What a body looks like

The meeting.ready body carries the key facts of the meeting, the participants, the short version and the tasks — deliberately without the text of the minutes and without the transcript. Both are one call away, under links. That keeps the body small and keeps what was said out of other people's log files.

json
{
  "event": "meeting.ready",
  "delivery_id": "8f14e45f-ceea-467a-9e2b-1d3f5c0a7b21",
  "occurred_at": "2026-08-08T10:12:03.441Z",
  "workspace_id": "…",
  "data": {
    "meeting": { "id": "…", "title": "Quarterly talk", "platform": "teams",
                 "starts_at": "2026-08-08T09:00:00Z", "ends_at": "2026-08-08T10:00:00Z" },
    "protocol": { "id": "…", "tldr": "Quote by Friday, engineering clarifies the interface.",
                  "generated_at": "2026-08-08T10:11:58Z" },
    "action_items": [ { "id": "…", "task": "Send the quote", "owner_name": "Anna",
                        "due": "2026-08-15", "verdict": "open", "status": "open" } ],
    "participants": [ { "display_name": "Anna", "email": "anna@company.example",
                        "is_internal": true, "attended": true } ]
  },
  "links": {
    "meeting":      "https://protoki.com/api/v1/meetings/…",
    "transcript":   "https://protoki.com/api/v1/meetings/…/transcript",
    "action_items": "https://protoki.com/api/v1/meetings/…/action-items",
    "app":          "https://protoki.com/meetings/…"
  }
}

A meeting.failed body carries the meeting and a failure block with the stage it broke off at.

json
{
  "event": "meeting.failed",
  "delivery_id": "3b1f7c02-5d64-4c1a-9f88-2ac0e6b41d77",
  "occurred_at": "2026-08-08T10:12:03.441Z",
  "workspace_id": "…",
  "data": {
    "meeting": { "id": "…", "title": "Quarterly talk", "platform": "teams",
                 "status": "failed" },
    "failure": { "stage": "transcribe", "retryable": true }
  },
  "links": {
    "meeting": "https://protoki.com/api/v1/meetings/…",
    "app":     "https://protoki.com/meetings/…"
  }
}

Trying it out

The admin area has a Send test call button. It posts a signed call with "event": "ping" to that one endpoint straight away and shows you the response — no retries, no real meeting data. So you do not have to wait for a meeting to find out whether your end is up.

Failed deliveries are listed with a reason in the admin area. Look there instead of guessing.

Verifying the signature

Every call carries Protoki-Signature: t=…,v1=…. What is signed is the timestamp and the body, not the body alone — otherwise a call captured once could be replayed at will. So check the age as well.

js
import { createHmac, timingSafeEqual } from 'node:crypto';

// IMPORTANT: the RAW body. If you run it through JSON.parse and back through
// JSON.stringify, key order and whitespace change — and the check then fails
// every time.
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.split('=').map((s) => s.trim())),
  );
  const t = Number(parts.t);
  if (!Number.isInteger(t)) return false;

  // 1. Check the age — this is the replay protection.
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;

  // 2. HMAC over "<t>.<body>".
  const expected = createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  // 3. Compare in constant time.
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1 ?? '', 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

Protoki — EU/GDPR-compliant meeting notetaker.