IntegrationsAdvancedWebhooks and signatures

Webhooks and signatures

Four inbound routes, and each is verified before a single field is read.

Route

Integration events, from every toolkit

Standard Webhooks: HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{rawBody}, with a replay window

GitHub App deliveries

GitHub's x-hub-signature-256, with the App's webhook secret

Meeting and document relays

An admin bearer token

Payment provider events

The provider's own signature

Every provider's events arrive on the first route. One signature, checked once, for 500+ toolkits, which is what "no per-provider signature verification" means in practice.

Raw bytes, always

Verification runs over the exact bytes received, before any JSON parsing.

This is not fussiness. A parsed-and-re-serialized body is a different byte string: key order changes, whitespace changes, numeric formatting changes, and the signature will not match. Worse, a framework that parses first and hands you an object has already made a trust decision on unverified input.

ts
const signed = `${id}.${timestamp}.${rawBody}`;
const expected = createHmac('sha256', secret).update(signed).digest('hex');
if (timingSafeEqual(Buffer.from(expected), Buffer.from(received)) === false) {
  return reply.code(401).send();
}

Every shared-token comparison is constant-time. A byte-by-byte comparison that returns early leaks the secret one character at a time to anyone patient.

The replay window

A valid signature stays valid forever unless the timestamp is checked. The integration route rejects deliveries whose timestamp falls outside a replay window, so a captured request cannot be re-sent tomorrow.

Rate limiting

Inbound webhook traffic is limited per workspace, in requests per minute. One tool having a bad day cannot exhaust ingestion for every other workspace.

Registering the endpoints

Integration events. The console shows the exact URL to register, on Connectors → Webhook. Copy it into the integration provider's dashboard as the project webhook. Once, for every toolkit.

GitHub App. The same screen shows this workspace's App webhook URL. Paste it into the App's settings alongside the App's webhook secret.

Both URLs are per-instance and per-workspace, which is why the console hands you the right one rather than this page describing how to assemble it.

Meetings and documents. Relay from a server you control, with an admin bearer token. Never from a browser.

Debugging a webhook that is not arriving

  1. 1

    Check the sender's own delivery log

    Both the integration provider and GitHub show delivery attempts with their response codes. A 401 here is a signature or secret problem; a timeout is a networking one.

  2. 2

    Check the failed deliveries list

    Connectors → the connection → Failed deliveries. A delivery that was accepted and then failed downstream appears here rather than in the sender's log, which is why a clean log on their side does not mean the event landed.

  3. 3

    Check the workspace has a permalink base URL

    A verified, accepted delivery can still be rejected at the normalizer if the event has no resolvable URL. This looks identical to nothing arriving.

Outbound is a different thing

Memnox → your systems is covered by subscriptions and sinks. Those are signed with a secret you hold, and you verify them the same way, raw bytes, constant time.