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.

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.

Replay

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

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.