Agent skill

Knock Webhooks Skill

Receive and verify Knock outbound webhooks. Use when setting up Knock webhook handlers, debugging x-knock-signature verification, or handling notification events like message.sent, message.delivered, message.bounced, message.read, workflow.committed, or message.link_clicked.

Install this skill

npx skills add hookdeck/webhook-skills --skill knock-webhooks


When to Use This Skill

  • Setting up Knock outbound webhook handlers
  • Debugging x-knock-signature verification failures
  • Handling Knock notification message lifecycle events (sent, delivered, bounced, read, link_clicked)
  • Reacting to Knock resource changes (workflow.committed, translation.committed, etc.)
  • Porting a Stripe-style verifier to Knock and discovering it silently fails (Knock uses milliseconds, Stripe uses seconds)

Verification (core)

Knock signs each webhook with HMAC-SHA256 (base64) and sends a single header:

x-knock-signature: t=<timestamp_ms>,s=<base64_signature>

The signed string is ${timestamp_ms}.${raw_body} (period separator). The timestamp is in milliseconds, not seconds — this is an explicit deviation from Stripe. There is no SDK helper (@knocklabs/node and knockapi do not expose an inbound verification method); verify with the standard library.

const crypto = require('crypto');

function verifyKnockSignature(rawBody, header, secret, toleranceMs = 5 * 60 * 1000) {
  if (!header) return false;
  const [tPart, sPart] = header.split(',');
  const timestampMs = tPart?.startsWith('t=') ? tPart.slice(2) : null;
  const signature = sPart?.startsWith('s=') ? sPart.slice(2) : null;
  if (!timestampMs || !signature) return false;

  if (Math.abs(Date.now() - parseInt(timestampMs, 10)) > toleranceMs) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestampMs}.${rawBody}`)
    .digest('base64');

  const a = Buffer.from(signature, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

For complete handlers with route wiring, event dispatch, and tests, see:

Common Event Types

EventDescription
message.sentMessage was sent through a channel
message.deliveredChannel confirmed delivery
message.delivery_attemptedDelivery attempt was made (success or failure)
message.undeliveredChannel failed to deliver after retries
message.bouncedRecipient address bounced
message.seenRecipient saw the message in feed/inbox
message.readRecipient marked the message as read
message.archivedRecipient archived the message
message.interactedRecipient interacted with the message
message.link_clickedRecipient clicked a tracked link
workflow.committedWorkflow committed to an environment
translation.committedTranslation committed to an environment

For full event reference (23 events across message, workflow, email_layout, translation, source_event_action, partial), see Knock Outbound Webhooks Event Types.

Environment Variables

KNOCK_WEBHOOK_SECRET=your_per_endpoint_signing_secret  # From Developers → Webhooks → endpoint detail

The signing secret is per webhook endpoint (visible on the endpoint detail page in the Knock dashboard) — it is not your Knock account API key.

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 knock --path /webhooks/knock

Use the printed Hookdeck URL as the destination URL when creating the webhook endpoint in the Knock dashboard.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Aug 2, 2026

View on GitHub →