Skip to content

Developer docs

Outgoing webhooks

Send signed events from a hub or a whole organization to your own service: the event catalog, payload schemas, signature checks in JavaScript, Python and Go, retries and redelivery.

An outgoing webhook sends a signed HTTPS request to your service when something happens. Hubs and organizations use the same system, the same signatures and the same retry rules.

ScopeWhere it is managedWhoEvents
HubManage, Developer toolsThe hub ownerMessages posted, edited and deleted in 1 to 20 chosen channels
OrganizationOrganization admin, IntegrationsThe owner and managersEvery event in the catalog below, across every hub of the organization or the ones you pick

Adding a webhook

  • Choose a name, a public HTTPS address, the events and, for an organization, which hubs. Adding one asks for your password and second factor, because events leave Hubs.
  • The signing secret (whsec_...) is shown once. Hubs keeps the address and the secret sealed and shows only the host and the secret's last four characters.
  • Addresses must be public HTTPS on port 443, with no user name or password and no literal IP address. Redirects are never followed and each request has 10 seconds.

What a webhook sees

A webhook sees only what the person who made it can see, checked when the event happens and again right before every attempt. Direct messages are never sent. Automated and imported posts are left out, so an incoming webhook's post never loops back out. Message text is read when the event is sent, so an edit or delete before then cancels the stale event.

Event catalog

Organization webhooks send every event in an envelope, version 1. Each event's data follows the schema below.

message.createdMessage posted

Someone posts in a channel. Includes the text and the author.

Sent to organization and hub webhooks.

data

FieldTypeRules
messagerequiredobject
message.idrequiredstring1 to 200 characters
message.contentrequiredstring
message.createdAtrequiredtimestamp
message.editedAtrequiredtimestamp or null
message.authorrequiredobject
message.author.idrequiredstring1 to 200 characters
message.author.handlerequiredstring
message.author.displayNamerequiredstring
message.author.botrequiredboolean

message.editedMessage edited

Someone changes a message. Includes the new text.

Sent to organization and hub webhooks.

data

FieldTypeRules
messagerequiredobject
message.idrequiredstring1 to 200 characters
message.contentrequiredstring
message.createdAtrequiredtimestamp
message.editedAtrequiredtimestamp or null
message.authorrequiredobject
message.author.idrequiredstring1 to 200 characters
message.author.handlerequiredstring
message.author.displayNamerequiredstring
message.author.botrequiredboolean

message.deletedMessage deleted

A message is deleted. Only ids, never the text.

Sent to organization and hub webhooks.

data

FieldTypeRules
messagerequiredobject
message.idrequiredstring1 to 200 characters

reaction.addedReaction added

Someone reacts to a message.

Sent to organization webhooks.

data

FieldTypeRules
reactionrequiredobject
reaction.messageIdrequiredstring1 to 200 characters
reaction.emojirequiredstringat least 1 character
reaction.userrequiredobject
reaction.user.idrequiredstring1 to 200 characters
reaction.user.handlerequiredstring
reaction.user.displayNamerequiredstring
reaction.user.botrequiredboolean

reaction.removedReaction removed

Someone takes a reaction back.

Sent to organization webhooks.

data

FieldTypeRules
reactionrequiredobject
reaction.messageIdrequiredstring1 to 200 characters
reaction.emojirequiredstringat least 1 character
reaction.userrequiredobject
reaction.user.idrequiredstring1 to 200 characters
reaction.user.handlerequiredstring
reaction.user.displayNamerequiredstring
reaction.user.botrequiredboolean

member.joinedMember joined

Someone joins one of the organization's hubs.

Sent to organization webhooks.

data

FieldTypeRules
memberrequiredobject
member.idrequiredstring1 to 200 characters
member.handlerequiredstring
member.displayNamerequiredstring
member.botrequiredboolean

member.leftMember left

Someone leaves or is removed from a hub.

Sent to organization webhooks.

data

FieldTypeRules
memberrequiredobject
member.idrequiredstring1 to 200 characters
member.handlerequiredstring
member.displayNamerequiredstring
member.botrequiredboolean

channel.createdChannel created

A new channel is made.

Sent to organization webhooks.

data

FieldTypeRules
channelrequiredobject
channel.idrequiredstring1 to 200 characters
channel.namerequiredstring
channel.kindrequiredstring

channel.archivedChannel archived

A channel is archived.

Sent to organization webhooks.

data

FieldTypeRules
channelrequiredobject
channel.idrequiredstring1 to 200 characters
channel.namerequiredstring
channel.kindrequiredstring

canvas.updatedCanvas updated

A canvas is saved, renamed or restored. Never the drawing itself.

Sent to organization webhooks.

data

FieldTypeRules
canvasrequiredobject
canvas.idrequiredstring1 to 200 characters
canvas.titlerequiredstring
canvas.revisionrequiredintegerat least 1
canvas.channelIdrequiredstring or null
canvas.updatedAtrequiredtimestamp
actorrequiredobject or null

meeting.startedMeeting started

Not sent yet: meetings belong to their host, not to a hub or organization.

Not sent yet. The schema is fixed so you can prepare.

data

FieldTypeRules
meetingrequiredobject
meeting.idrequiredstring1 to 200 characters
meeting.titlerequiredstring
meeting.startedAtrequiredtimestamp

meeting.endedMeeting ended

Not sent yet: meetings belong to their host, not to a hub or organization.

Not sent yet. The schema is fixed so you can prepare.

data

FieldTypeRules
meetingrequiredobject
meeting.idrequiredstring1 to 200 characters
meeting.titlerequiredstring
meeting.startedAtrequiredtimestamp
meeting.endedAtrequiredtimestamp

webhook.testTest event

Sent when you choose Send test event.

Sent to organization and hub webhooks.

data

FieldTypeRules
messagerequiredstring
webhookIdrequiredstring1 to 200 characters

The envelope

message.created
{
  "id": "8c5b1f9e-2f0e-4d0a-9d55-1f0c8b1c2a10",
  "type": "message.created",
  "version": 1,
  "createdAt": "2026-10-11T16:00:00.000Z",
  "organizationId": "a1b2c3d4-0000-4000-8000-000000000001",
  "hubId": "a1b2c3d4-0000-4000-8000-000000000002",
  "channelId": "a1b2c3d4-0000-4000-8000-000000000003",
  "data": {
    "message": {
      "id": "a1b2c3d4-0000-4000-8000-000000000004",
      "content": "Deploy finished.",
      "createdAt": "2026-10-11T16:00:00.000Z",
      "editedAt": null,
      "author": {
        "id": "a1b2c3d4-0000-4000-8000-000000000005",
        "handle": "river",
        "displayName": "River",
        "bot": false
      }
    }
  }
}

version changes only when a field is removed or changes meaning. New fields can arrive in the same version, so ignore fields you do not know.

Hub webhooks keep their original body for the three message events, because existing receivers read it: { id, type, createdAt, hubId, channelId, message }, where message is { id, content, createdAt, editedAt, author: { id, handle, displayName } } (only id for message.deleted). Their test event uses the envelope above with organizationId: null.

Headers

HeaderValue
X-Hubs-Event-IdThe event id. The same on retries and redeliveries
Idempotency-KeyThe same event id
X-Hubs-TimestampUnix seconds when this attempt was signed
X-Hubs-Signaturev1= and the hex HMAC-SHA256 of "<timestamp>.<raw body>"
X-Hubs-Delivery-IdOrganization webhooks: this attempt's delivery id

Verify the signature

Check the signature on the raw bytes before you parse them, compare in constant time, refuse timestamps more than five minutes away, and remember each event id so you act on it once. These samples are tested against the signing code Hubs runs.

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody is the exact bytes Hubs sent, before any JSON parsing.
export function verifyHubsSignature(secret, headers, rawBody, now = Date.now()) {
  const timestamp = headers["x-hubs-timestamp"] ?? "";
  const signature = headers["x-hubs-signature"] ?? "";
  if (!/^\d+$/.test(timestamp)) return false;
  // Refuse deliveries signed more than five minutes ago (or ahead).
  if (Math.abs(now / 1000 - Number(timestamp)) > 300) return false;
  const expected =
    "v1=" +
    createHmac("sha256", secret)
      .update(timestamp + ".")
      .update(rawBody)
      .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && timingSafeEqual(a, b);
}

Delivery and retries

  • Delivery is at least once. Answer with any 2xx after you have stored the event.
  • A network failure, 408, 429 or 5xx is tried again with backoff starting at 15 seconds and growing to at most an hour, 5 attempts in all. Any other answer, and any redirect, fails at once.
  • A webhook with 500 events waiting is paused and its waiting events cancelled; add it again once your receiver is back.
  • The delivery log shows the latest 50 deliveries with their state, attempts, the last status and a short reason. Your service's answers are never stored.

Test events, redelivery and new secrets

  • Send test event queues a signed webhook.test event right away.
  • Redeliver sends a delivered, failed or cancelled event again with the same event id, after checking what the webhook may see now.
  • New secret shows a new secret once; the old one stops working at once and waiting events are signed with the new one.
  • Remove stops the webhook and cancels what is waiting.

Every add, new secret, redelivery, pause and removal is written to the organization audit log, or for hubs to the hub audit trail.