Appearance
The minutes are ready
POST to your own endpoint meeting.ready
Protoki calls this address as soon as the minutes and tasks of a meeting exist. You set it up in the admin area under "Webhooks".
Verify the signature — this way and no other:
- Read
Protoki-Signature, shapedt=<unix seconds>,v1=<hex>. - If
tis more than 300 seconds old, or in the future, throw the call away. Without this step a captured call can be replayed as often as you like. - Take HMAC-SHA256 over the string
<t>.<raw body>with your secret and compare it againstv1in constant time.
⚠️ The raw body is meant. Run it through JSON.parse and back through JSON.stringify and you change key order and whitespace — the check then fails every single time.
Answer with any 2xx, as fast as you can; the limit is 10 seconds. Anything else counts as a failure and is retried after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h and 12 h — just over 22 hours in total. 410 Gone ends the attempts at once and for good.
Double delivery is possible, for instance when your 2xx is lost on the way. So deduplicate on delivery_id: it stays the same across every attempt and also sits in the Protoki-Delivery header.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Protoki-Signature | string | yes | Timestamp and signature: t=1770000000,v1=<hex>. |
Protoki-Event | string | yes | Values: meeting.ready. |
Protoki-Delivery | uuid | yes | Delivery id, for deduplication. |
Body
Example
json
{
"event": "meeting.ready",
"delivery_id": "e07b6d13-84af-4c92-b5d7-0a9e1f83c246",
"occurred_at": "2026-08-04T09:04:15Z",
"workspace_id": "3b1f7c02-5d64-4c1a-9f88-2ac0e6b41d77",
"data": {
"meeting": {
"id": "e07b6d13-84af-4c92-b5d7-0a9e1f83c246",
"title": "Quarterly review",
"platform": "string",
"source": "string",
"status": "string",
"starts_at": "2026-08-04T08:00:00Z",
"ends_at": "2026-08-04T09:00:00Z",
"organizer_email": "anna@company.example",
"join_url": "https://teams.microsoft.com/l/meetup-join/…"
},
"protocol": {
"id": "e07b6d13-84af-4c92-b5d7-0a9e1f83c246",
"tldr": "Quote goes out by Friday, engineering clarifies the interface.",
"generated_at": "2026-08-04T09:04:12Z"
},
"action_items": [
{
"id": "c4d9f0a1-72be-4e35-8f10-6d2a5b8c9e04",
"task": "Send the quote",
"owner_name": "Anna Weber",
"due": "2026-08-15",
"verdict": "open",
"status": "open"
}
],
"participants": [
{
"display_name": "Anna Weber",
"email": "anna@company.example",
"domain": "company.example",
"is_internal": true,
"attended": true
}
]
},
"links": {
"meeting": "https://protoki.com/api/v1/meetings/e07b6d13-84af-4c92-b5d7-0a9e1f83c246",
"transcript": "https://protoki.com/api/v1/meetings/e07b6d13-84af-4c92-b5d7-0a9e1f83c246",
"action_items": "https://protoki.com/api/v1/meetings/e07b6d13-84af-4c92-b5d7-0a9e1f83c246",
"participants": "https://protoki.com/api/v1/meetings/e07b6d13-84af-4c92-b5d7-0a9e1f83c246",
"app": "https://protoki.com/api/v1/meetings/e07b6d13-84af-4c92-b5d7-0a9e1f83c246"
}
}| Field | Type | Required | Description |
|---|---|---|---|
event | meeting.ready | yes | Which event this is. Branch on it, not on the shape of data. |
delivery_id | uuid | yes | Stays the same across every retry — this is what you deduplicate double deliveries on. |
occurred_at | date-time | yes | When the event happened on our side, not when this attempt was sent. |
workspace_id | uuid | yes | The workspace the meeting belongs to. |
data | object | yes | The payload of the event. |
data.meeting | object | — | The meeting this event is about. |
data.meeting.id | uuid | — | Identifies the meeting everywhere in this API. |
data.meeting.title | string | — | Subject, as the calendar or the upload spelled it. |
data.meeting.platform | string | — | Where the meeting took place. |
data.meeting.source | string | — | How the meeting reached Protoki. |
data.meeting.status | string | — | Where the meeting stood when this call went out. |
data.meeting.starts_at | date-time | null | — | Start of the appointment. null for an upload, which has none. |
data.meeting.ends_at | date-time | null | — | End of the appointment. null for an upload. |
data.meeting.organizer_email | string | null | — | Who invited. null when the calendar gave nothing. |
data.meeting.join_url | string | null | — | The link the recorder used to join. null for an upload. |
data.protocol | object | null | — | The short version and the id — the FULL text of the minutes is deliberately not in the body. |
data.protocol.id | uuid | — | Identifies this set of minutes. |
data.protocol.tldr | string | null | — | The short version. null when the model produced none. |
data.protocol.generated_at | date-time | — | When the model wrote these minutes. |
data.action_items | object[] | — | The tasks taken from these minutes. |
data.action_items[].id | uuid | yes | Identifies the task. |
data.action_items[].task | string | yes | What is to be done, in one sentence. |
data.action_items[].owner_name | string | null | — | Who is to do it. null when nobody was named in the meeting. |
data.action_items[].due | date | null | — | When it is due. null when no date was named. |
data.action_items[].verdict | string | yes | Outcome of the second pass over the task. resolved_in_meeting means it was already dealt with while the meeting was still running. Values: open, resolved_in_meeting, discarded, duplicate, unclear_owner. |
data.action_items[].status | string | yes | Where the task stands. This one is set by a person in the app, not by the model. Values: open, done, dismissed. |
data.participants | object[] | — | Everyone the meeting knows about — present or only invited. |
data.participants[].display_name | string | — | The name as the calendar or the meeting platform spelled it. |
data.participants[].email | string | null | — | Email address. null when neither source gave one. |
data.participants[].domain | string | null | — | Email domain — this is what ties a meeting to a company. |
data.participants[].is_internal | boolean | — | The email domain belongs to your own house. |
data.participants[].attended | boolean | — | false means: invited, did not turn up. Somebody who sat through the meeting without saying a word counts as present. |
links | object | yes | Ready-made addresses for the follow-up call — the text of the minutes and the transcript are behind these. |
links.meeting | uri | — | The meeting in this API. |
links.transcript | uri | — | The transcript of this meeting. |
links.action_items | uri | — | The tasks of this meeting. |
links.participants | uri | — | The participants of this meeting. |
links.app | uri | — | The meeting in the Protoki app, for a person to open. |
Response 200
Accepted.
Other answers
| Code | Meaning |
|---|---|
410 | This receiver is gone — Protoki stops sending. |