Event subscriptions
An event subscription has Missived call a URL you choose when something happens: a message is posted, someone joins, a channel is made, a meeting is scheduled, a schedule is published. This is how n8n, Zapier, Make and your own server hear about changes without polling. Every delivery is signed with Standard Webhooks.
A bot that can hold a WebSocket can get the same events over the bot gateway instead, with no public URL.
Subscribe
In the app: Org Settings → Integrations → Event subscriptions. You need the Manage integrations permission (Owner, Admin, or a custom role that grants it). Pick the URL, which events, which channels and how much of a message goes along. The signing secret is shown once.
Over the API: use a personal token whose owner has Manage integrations, or an app token, with the events:subscribe scope. This is a REST hook: Zapier's and n8n's instant triggers subscribe and unsubscribe this way.
curl -X POST https://api.missived.com/v1/orgs/<orgId>/event-subscriptions \
-H "Authorization: Bearer $MISSIVED_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "n8n", "url": "https://n8n.example.com/webhook/abc", "event_types": ["message.created", "member.*"]}'
The answer is 201 { "data": { …subscription… }, "secret": "whsec_…" }. The secret appears only here; keep it to verify deliveries.
event_typestakes exact names or globs (message.*,*). Empty means every type, including ones added later.channel_idsnarrows the subscription to some channels. Leave it out for every channel the subscriber can see. An app token may only list channels its app was added to, and stops receiving a channel's events as soon as the app is removed from it.DELETE /v1/orgs/{orgId}/event-subscriptions/{id}unsubscribes.PATCHchanges the name, URL, filters orpayload_mode, pauses (enabled: false) or rotates the secret (rotate_secret: true).- Each plan allows a number of subscriptions; past it you get
403 cap_reached.
What a delivery looks like
POST /your/url
content-type: application/json
webhook-id: 019281f0-7a2e-7cc3-9f1e-6d2b3c4a5e6f
webhook-timestamp: 1760440000
webhook-signature: v1,<base64 HMAC-SHA256>
{
"id": "019281f0-7a2e-7cc3-9f1e-6d2b3c4a5e6f",
"type": "message.created",
"timestamp": "2026-10-14T10:26:40.000Z",
"org_id": "…",
"payload": "full",
"data": { "message": { … }, "channel": { … } }
}
webhook-id is the event's id. It is the same on every retry and replay, so use it as your idempotency key. Answer with any 2xx within 10 seconds.
Verify the signature
Compute HMAC-SHA256 over "{webhook-id}.{webhook-timestamp}.{raw body}" with the secret (the base64 part after whsec_), and compare it with each v1, entry in webhook-signature. Reject timestamps more than 5 minutes from now. Always verify the raw bytes you received, before parsing the JSON.
import crypto from "node:crypto";
export function verify(secret, headers, rawBody) {
const id = headers["webhook-id"], ts = headers["webhook-timestamp"], sigs = headers["webhook-signature"] ?? "";
if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest();
return sigs.split(" ").some((s) => {
const [version, value] = s.split(",");
if (version !== "v1" || !value) return false;
const given = Buffer.from(value, "base64");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
Any Standard Webhooks library works too. While you rotate the secret, deliveries carry a signature for the new secret and one for the old, so a receiver that still has the old secret keeps working until you update it.
Retries, pauses and 410
Delivery is at least once, in the order events happened in the org.
- A 5xx, 408, 429 or network error is retried with exponential backoff for up to 24 hours.
Retry-Afteris honoured. - Any other 4xx stops that delivery: it was a configuration problem. After 20 failed deliveries in a row, the subscription pauses itself.
- 410 Gone switches the subscription off at once: the receiver doesn't want these events any more (Zapier does this when a Zap is turned off). Resuming it in the app clears that.
Replay
- A window:
POST /v1/orgs/{orgId}/event-subscriptions/{id}/replaywith{ "since", "until" }(ISO 8601, default the last 24 hours) queues the stored events in that window again, at most 1 000 per call (truncated: truesays to narrow it). The app's Replay 24h button does this. - One delivery:
POST /v1/orgs/{orgId}/event-subscriptions/{id}/deliveries/{deliveryId}/redeliversends one delivered or failed delivery again (202). The delivery log's Replay button does this. A delivery still being sent answers409 in_flight; a paused subscription answers409 paused, so resume it first.
Both keep the original webhook-id, so a receiver that remembers ids skips what it already has. GET …/{id}/deliveries is the delivery log: status, attempts, the last HTTP status and error.
How much of a message goes along
payload is "full" or "thin". A full message.created carries the text, the cards and the attachments' names; a thin one carries only ids and metadata, and your receiver fetches the message with GET /v1/channels/{channel_id}/messages/{id}.
Every workspace's messages are encrypted at rest. A workspace whose Owner switched that on before it was on everywhere sends thin payloads unless its Owner opts in; every other workspace sends full payloads unless its Owner opts out (GET / PATCH /v1/orgs/{orgId}/events/settings). A subscription can ask for payload_mode: "thin", never for more than the org allows; effective_payload_mode says what it gets.
What is never sent
- Direct messages, group chats and people's personal channels: no event is recorded for them.
- A private channel's events go only to a subscription that lists that channel.
- Meeting guests' details, sign-in and security data, and the audit log (it has its own webhooks).
The catalogue
| Type | When |
|---|---|
message.created, message.updated, message.deleted |
A message is posted, edited or removed. |
reaction.added, reaction.removed |
A reaction changes. |
member.joined, member.left, member.role_changed |
Someone joins, is removed, or changes org role. |
member.role_assigned, member.role_removed |
Someone gains or loses a custom role — given by an admin, an app (PUT /v1/orgs/{orgId}/members/{userId}/roles/{roleId}) or a linked-role rule (source). |
channel.created, channel.updated, channel.deleted |
A channel is made, renamed or removed. |
meeting.scheduled, meeting.updated, meeting.cancelled |
A meeting is created, changed or cancelled. |
shift.published |
A schedule's shifts go live. |
portal.request_completed, portal.file_uploaded, portal.stage_changed |
A client finishes a request, a file goes into a portal's library, or a portal moves stage. Only a subscription naming the portal's Messages channel receives them; names are content. See Client portals API. |
stage.scheduled, stage.updated, stage.cancelled, stage.started, stage.ended |
A scheduled Stage is published, changed, called off, goes live or ends. stage.ended carries how long it ran and how many listened (never who). Scoped to the Stage's voice channel; the title is content. |
ticket.created, ticket.status_changed, ticket.assigned, ticket.replied, ticket.customer_replied |
Tickets T7: thin data.ticket contains id, number, queue id, status and assignee id. Subject and requester name are the sealed full-content part. Never notes, message bodies or addresses. Person-only tickets:read, with the subscription maker’s current queue access at delivery and polling; apps receive none. |
booking.created, booking.rescheduled, booking.cancelled |
Someone booked a time on a booking page, or a booking moved or was cancelled (by: booker or host). data.booking is ids, the event type, start / end and the location kind; the booker (name, time zone, answers) is the content part, and their email only when the workspace's Owner turns on "Include bookers' email addresses in events and the API". Never for an app's subscription: bookers are people outside the workspace, so only a subscription made by a person (or their personal token) receives these. |
sheet.row_created, sheet.row_updated, sheet.row_deleted |
A Table row is added, changed or deleted — in the app, by an import or through the Tables API. A shared view's events go only to a subscription naming the view's channel. |
GET /v1/events/catalog lists them with the fields each carries. New types are added without a version change; ignore types you don't know.
Polling instead
GET /v1/orgs/{orgId}/events returns the stored events in order, with cursors, for up to 7 days, filtered by types, channel_id, since and until. It needs the same permission as subscribing, and works once the org has at least one subscription. Message text is included only for a token that can read messages (a Table row's values, only for one with tables:read). GET /v1/orgs/{orgId}/events/{eventId} fetches one stored event by its id (the webhook-id), with the same rules: a receiver that can't check signatures over the raw body can confirm a delivery this way.
Limits
One subscription receives at most 600 events a minute; the rest are dropped and counted (dropped_count). Events are kept for 7 days.