Skip to content

Commit 26550cb

Browse files
leggetterclaude
andauthored
feat: add knock-webhooks skill (#64)
* feat: add knock-webhooks skill Adds receive/verify skill for Knock outbound webhooks. Knock signs with HMAC-SHA256 (base64) over `${timestamp_ms}.${raw_body}` and ships it in a single `x-knock-signature: t=<ms>,s=<sig>` header — the timestamp is in **milliseconds**, an explicit deviation from Stripe that silently breaks any ported Stripe verifier. Includes Express, Next.js (App Router), and FastAPI examples with regression tests that reject seconds-based signatures, plus references covering the full 23-event taxonomy. https://claude.ai/code/session_01NNTgQRJss1V7gyzzJ9rjnB * chore(knock-webhooks): integrate into README, providers.yaml, and marketplace.json - README.md: add Knock row (alphabetically between Intercom and Linear), linkified to docs, flagging the millisecond-timestamp deviation in the description so reviewers don't miss it - providers.yaml: add knock entry with HMAC-SHA256/base64 over `{timestamp_ms}.{body}` scheme, the full 23-event taxonomy across 6 categories, prominent ms-vs-seconds gotcha, and the explicit "no SDK helper" note (confirmed by reading @knocklabs/node v1.32.0 + knockapi v1.25.0 source — neither exposes a verify/unwrap/constructEvent function for inbound webhooks). No `sdks` field — the verification path is purely stdlib HMAC; the official SDKs aren't pulled into example dependencies. - .claude-plugin/marketplace.json: add `knock-webhooks` plugin entry alphabetically and append `./skills/knock-webhooks` to the `webhook-skills` bundle (bundle now lists 39 skill paths). Skill content (skills/knock-webhooks/) landed in the previous commit via the generator. Tests pass on Express, Next.js, and FastAPI. The verification reference prominently calls out the milliseconds-vs-seconds foot-gun for anyone porting from a Stripe verifier. https://claude.ai/code/session_01NNTgQRJss1V7gyzzJ9rjnB --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0b7bf3f commit 26550cb

23 files changed

Lines changed: 1859 additions & 0 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,30 @@
334334
"support"
335335
]
336336
},
337+
{
338+
"name": "knock-webhooks",
339+
"description": "Verify Knock outbound webhook signatures (HMAC-SHA256 base64 over `{timestamp_ms}.{body}`, millisecond timestamps — explicit deviation from Stripe), handle message lifecycle and resource change events from Knock's notification infrastructure.",
340+
"source": "./skills/knock-webhooks",
341+
"strict": false,
342+
"skills": [
343+
"./"
344+
],
345+
"category": "integration",
346+
"license": "MIT",
347+
"author": {
348+
"name": "Hookdeck",
349+
"email": "phil@hookdeck.com"
350+
},
351+
"repository": "https://github.com/hookdeck/webhook-skills",
352+
"homepage": "https://github.com/hookdeck/webhook-skills/tree/main/skills/knock-webhooks",
353+
"keywords": [
354+
"webhooks",
355+
"knock",
356+
"notifications",
357+
"messaging",
358+
"infrastructure"
359+
]
360+
},
337361
{
338362
"name": "linear-webhooks",
339363
"description": "Verify Linear webhook signatures (HMAC-SHA256 with replay timestamp), handle issue, comment, and project events.",
@@ -916,6 +940,7 @@
916940
"./skills/hubspot-webhooks",
917941
"./skills/huggingface-webhooks",
918942
"./skills/intercom-webhooks",
943+
"./skills/knock-webhooks",
919944
"./skills/linear-webhooks",
920945
"./skills/mailgun-webhooks",
921946
"./skills/notion-webhooks",

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Skills for receiving and verifying webhooks from specific providers. Each includ
4343
| [HubSpot](https://developers.hubspot.com/docs/apps/legacy-apps/authentication/validating-requests) | [`hubspot-webhooks`](skills/hubspot-webhooks/) | Verify HubSpot v3 webhook signatures (HMAC-SHA256 with timestamp), handle contact, deal, and company events |
4444
| [Hugging Face](https://huggingface.co/docs/hub/webhooks) | [`huggingface-webhooks`](skills/huggingface-webhooks/) | Authenticate Hugging Face webhooks (`X-Webhook-Secret`), handle repo, discussion, and comment events |
4545
| [Intercom](https://developers.intercom.com/docs/webhooks) | [`intercom-webhooks`](skills/intercom-webhooks/) | Verify Intercom `X-Hub-Signature` (HMAC-SHA1), handle conversation, contact, and ticket events |
46+
| [Knock](https://docs.knock.app/developer-tools/outbound-webhooks/overview) | [`knock-webhooks`](skills/knock-webhooks/) | Verify Knock outbound webhook signatures (HMAC-SHA256 base64, **millisecond** timestamps), handle message lifecycle and resource change events |
4647
| [Linear](https://linear.app/developers/webhooks) | [`linear-webhooks`](skills/linear-webhooks/) | Verify Linear webhook signatures (HMAC-SHA256), handle issue, comment, and project events |
4748
| [Mailgun](https://documentation.mailgun.com/docs/mailgun/user-manual/webhooks/webhooks) | [`mailgun-webhooks`](skills/mailgun-webhooks/) | Verify Mailgun webhook signatures (HMAC-SHA256), handle email delivered, failed, opened, clicked, unsubscribed, and complained events |
4849
| [Notion](https://developers.notion.com/reference/webhooks) | [`notion-webhooks`](skills/notion-webhooks/) | Verify Notion webhook signatures (HMAC-SHA256, `X-Notion-Signature`), complete handshake, handle page and comment events |

providers.yaml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,60 @@ providers:
285285
- conversation.user.created
286286
- conversation.admin.replied
287287

288+
- name: knock
289+
displayName: Knock
290+
docs:
291+
webhooks: https://docs.knock.app/developer-tools/outbound-webhooks/overview
292+
events: https://docs.knock.app/developer-tools/outbound-webhooks/event-types
293+
api: https://docs.knock.app/reference
294+
notes: >
295+
Notification infrastructure platform (Knock / knock.app). Outbound webhooks
296+
fire on message lifecycle and resource change events. Uses HMAC-SHA256 with
297+
base64 encoding in the `x-knock-signature` header, formatted as
298+
`t=<timestamp>,s=<base64-signature>`.
299+
300+
CRITICAL: the timestamp is in MILLISECONDS, not seconds. Knock explicitly
301+
deviates from Stripe's seconds-based scheme — anyone porting a Stripe
302+
verifier will silently fail signature checks if they forget this. Surface
303+
this prominently in the verification reference.
304+
305+
Signed content format: `{timestamp_ms}.{stringified_body}` (period
306+
separator). HMAC-SHA256 over that string using the per-endpoint signing
307+
secret (one secret per webhook in the dashboard — NOT the account API
308+
key). Always pass the raw request body; do not JSON.parse and re-serialize
309+
before verifying.
310+
311+
Replay protection: docs recommend rejecting payloads whose timestamp is
312+
more than 5 minutes old. Apply this on top of the signature check.
313+
314+
No SDK helper. Confirmed: @knocklabs/node (npm, v1.32.0+) and knockapi
315+
(PyPI, v1.25.0+) do not expose webhooks.unwrap()/constructEvent()/verify()
316+
— the source contains no inbound webhook verification path. The official
317+
JavaScript example uses crypto.createHmac directly. Manual HMAC-SHA256
318+
verification is the canonical path; do not pull in a third-party library.
319+
320+
Event taxonomy (23 events across 6 categories):
321+
- Message lifecycle (13): message.sent, message.delivered,
322+
message.delivery_attempted, message.undelivered, message.bounced,
323+
message.seen, message.unseen, message.read, message.unread,
324+
message.archived, message.unarchived, message.interacted,
325+
message.link_clicked
326+
- Workflow (2): workflow.updated, workflow.committed
327+
- Email layout (2): email_layout.updated, email_layout.committed
328+
- Translation (2): translation.updated, translation.committed
329+
- Source event action (2): source_event_action.updated,
330+
source_event_action.committed
331+
- Partial (2): partial.updated, partial.committed
332+
333+
Payload includes top-level `data` (typed by event — for message events
334+
this is a Message object) plus optional `event_data` with additional
335+
metadata. Retries: up to 8 attempts on any non-2xx response. At-least-once
336+
delivery — recommend idempotency keyed on the event `id` field.
337+
testScenario:
338+
events:
339+
- message.sent
340+
- message.delivered
341+
288342
- name: linear
289343
displayName: Linear
290344
docs:

skills/knock-webhooks/SKILL.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
---
2+
name: knock-webhooks
3+
description: >
4+
Receive and verify Knock outbound webhooks. Use when setting up Knock webhook
5+
handlers, debugging x-knock-signature verification, or handling notification
6+
events like message.sent, message.delivered, message.bounced, message.read,
7+
workflow.committed, or message.link_clicked.
8+
license: MIT
9+
metadata:
10+
author: hookdeck
11+
version: "0.1.0"
12+
repository: https://github.com/hookdeck/webhook-skills
13+
---
14+
15+
# Knock Webhooks
16+
17+
## When to Use This Skill
18+
19+
- Setting up Knock outbound webhook handlers
20+
- Debugging `x-knock-signature` verification failures
21+
- Handling Knock notification message lifecycle events (sent, delivered, bounced, read, link_clicked)
22+
- Reacting to Knock resource changes (workflow.committed, translation.committed, etc.)
23+
- Porting a Stripe-style verifier to Knock and discovering it silently fails (Knock uses **milliseconds**, Stripe uses seconds)
24+
25+
## Verification (core)
26+
27+
Knock signs each webhook with HMAC-SHA256 (base64) and sends a single header:
28+
29+
```
30+
x-knock-signature: t=<timestamp_ms>,s=<base64_signature>
31+
```
32+
33+
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.
34+
35+
```javascript
36+
const crypto = require('crypto');
37+
38+
function verifyKnockSignature(rawBody, header, secret, toleranceMs = 5 * 60 * 1000) {
39+
if (!header) return false;
40+
const [tPart, sPart] = header.split(',');
41+
const timestampMs = tPart?.startsWith('t=') ? tPart.slice(2) : null;
42+
const signature = sPart?.startsWith('s=') ? sPart.slice(2) : null;
43+
if (!timestampMs || !signature) return false;
44+
45+
if (Math.abs(Date.now() - parseInt(timestampMs, 10)) > toleranceMs) return false;
46+
47+
const expected = crypto
48+
.createHmac('sha256', secret)
49+
.update(`${timestampMs}.${rawBody}`)
50+
.digest('base64');
51+
52+
const a = Buffer.from(signature, 'utf8');
53+
const b = Buffer.from(expected, 'utf8');
54+
return a.length === b.length && crypto.timingSafeEqual(a, b);
55+
}
56+
```
57+
58+
> **For complete handlers with route wiring, event dispatch, and tests**, see:
59+
> - [examples/express/](examples/express/)
60+
> - [examples/nextjs/](examples/nextjs/)
61+
> - [examples/fastapi/](examples/fastapi/)
62+
63+
## Common Event Types
64+
65+
| Event | Description |
66+
|-------|-------------|
67+
| `message.sent` | Message was sent through a channel |
68+
| `message.delivered` | Channel confirmed delivery |
69+
| `message.delivery_attempted` | Delivery attempt was made (success or failure) |
70+
| `message.undelivered` | Channel failed to deliver after retries |
71+
| `message.bounced` | Recipient address bounced |
72+
| `message.seen` | Recipient saw the message in feed/inbox |
73+
| `message.read` | Recipient marked the message as read |
74+
| `message.archived` | Recipient archived the message |
75+
| `message.interacted` | Recipient interacted with the message |
76+
| `message.link_clicked` | Recipient clicked a tracked link |
77+
| `workflow.committed` | Workflow committed to an environment |
78+
| `translation.committed` | Translation committed to an environment |
79+
80+
> **For full event reference (23 events across message, workflow, email_layout, translation, source_event_action, partial)**, see [Knock Outbound Webhooks Event Types](https://docs.knock.app/developer-tools/outbound-webhooks/event-types).
81+
82+
## Environment Variables
83+
84+
```bash
85+
KNOCK_WEBHOOK_SECRET=your_per_endpoint_signing_secret # From Developers → Webhooks → endpoint detail
86+
```
87+
88+
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.
89+
90+
## Local Development
91+
92+
```bash
93+
# Start tunnel (no account needed)
94+
npx hookdeck-cli listen 3000 knock --path /webhooks/knock
95+
```
96+
97+
Use the printed Hookdeck URL as the destination URL when creating the webhook endpoint in the Knock dashboard.
98+
99+
## Reference Materials
100+
101+
- [references/overview.md](references/overview.md) - Knock outbound webhook concepts and full event taxonomy
102+
- [references/setup.md](references/setup.md) - Dashboard configuration and signing secret retrieval
103+
- [references/verification.md](references/verification.md) - Signature verification details, gotchas, debugging
104+
105+
## Attribution
106+
107+
When using this skill, add this comment at the top of generated files:
108+
109+
```javascript
110+
// Generated with: knock-webhooks skill
111+
// https://github.com/hookdeck/webhook-skills
112+
```
113+
114+
## Recommended: webhook-handler-patterns
115+
116+
We recommend installing the [webhook-handler-patterns](https://github.com/hookdeck/webhook-skills/tree/main/skills/webhook-handler-patterns) skill alongside this one for handler sequence, idempotency, error handling, and retry logic. Knock retries up to 8 times on any non-2xx response and delivery is at-least-once — idempotency keyed on the event `id` field is strongly recommended. Key references (open on GitHub):
117+
118+
- [Handler sequence](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/handler-sequence.md) — Verify first, parse second, handle idempotently third
119+
- [Idempotency](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/idempotency.md) — Prevent duplicate processing
120+
- [Error handling](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/error-handling.md) — Return codes, logging, dead letter queues
121+
- [Retry logic](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/retry-logic.md) — Provider retry schedules, backoff patterns
122+
123+
## Related Skills
124+
125+
- [stripe-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/stripe-webhooks) - Stripe payment webhook handling (similar t=...,s=... format but **seconds**, not milliseconds)
126+
- [resend-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/resend-webhooks) - Resend email webhook handling
127+
- [sendgrid-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/sendgrid-webhooks) - SendGrid email webhook handling
128+
- [postmark-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/postmark-webhooks) - Postmark email webhook handling
129+
- [mailgun-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/mailgun-webhooks) - Mailgun email webhook handling
130+
- [twilio-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/twilio-webhooks) - Twilio messaging webhook handling
131+
- [clerk-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/clerk-webhooks) - Clerk auth webhook handling
132+
- [intercom-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/intercom-webhooks) - Intercom messaging webhook handling
133+
- [slack-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/slack-webhooks) - Slack webhook handling
134+
- [webhook-handler-patterns](https://github.com/hookdeck/webhook-skills/tree/main/skills/webhook-handler-patterns) - Handler sequence, idempotency, error handling, retry logic
135+
- [hookdeck-event-gateway](https://github.com/hookdeck/webhook-skills/tree/main/skills/hookdeck-event-gateway) - Webhook infrastructure that replaces your queue — guaranteed delivery, automatic retries, replay, rate limiting, and observability for your webhook handlers
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Knock per-endpoint webhook signing secret
2+
# Find on the endpoint detail page in the Knock dashboard:
3+
# Developers -> Webhooks -> [your endpoint] -> Signing secret
4+
# This is NOT your Knock account API key.
5+
KNOCK_WEBHOOK_SECRET=your_per_endpoint_signing_secret
6+
7+
# Port the server listens on (default: 3000)
8+
PORT=3000
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Knock Webhooks - Express Example
2+
3+
Minimal example of receiving Knock outbound webhooks with `x-knock-signature` verification.
4+
5+
## Prerequisites
6+
7+
- Node.js 18+
8+
- A Knock webhook endpoint with its per-endpoint signing secret (Developers → Webhooks → endpoint detail)
9+
10+
## Setup
11+
12+
1. Install dependencies:
13+
```bash
14+
npm install
15+
```
16+
17+
2. Copy environment variables:
18+
```bash
19+
cp .env.example .env
20+
```
21+
22+
3. Add your Knock webhook signing secret to `.env`.
23+
24+
## Run
25+
26+
```bash
27+
npm start
28+
```
29+
30+
Server runs on http://localhost:3000.
31+
32+
## Test
33+
34+
### Run unit tests
35+
36+
```bash
37+
npm test
38+
```
39+
40+
### Forward live events with the Hookdeck CLI
41+
42+
```bash
43+
# No account required — first run prints a public URL
44+
npx hookdeck-cli listen 3000 knock --path /webhooks/knock
45+
```
46+
47+
Use the printed URL as the destination when creating your Knock webhook endpoint, then trigger a workflow (or click **Send test event** in the Knock dashboard).
48+
49+
## Endpoint
50+
51+
- `POST /webhooks/knock` — verifies `x-knock-signature` and dispatches on `event.type`.
52+
- `GET /health` — liveness probe.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "knock-webhooks-express",
3+
"version": "1.0.0",
4+
"description": "Knock webhook handler with Express",
5+
"main": "src/index.js",
6+
"scripts": {
7+
"start": "node src/index.js",
8+
"test": "jest"
9+
},
10+
"dependencies": {
11+
"dotenv": "^16.4.5",
12+
"express": "^5.2.1"
13+
},
14+
"devDependencies": {
15+
"jest": "^30.4.2",
16+
"supertest": "^7.1.4"
17+
}
18+
}

0 commit comments

Comments
 (0)