|
7 | 7 | [](LICENSE) |
8 | 8 | [](package.json) |
9 | 9 |
|
10 | | -Stop writing webhook verification from scratch. **Tern** handles signature verification for Stripe, GitHub, Clerk, Shopify, and 15+ more platforms — with one consistent API. |
| 10 | +Stop writing webhook verification from scratch. **Tern** handles signature verification for Stripe, GitHub, Clerk, Shopify, and 15+ more platforms — with one consistent API. It also verifies **Standard Webhooks** (including Svix-style `svix-*` and canonical `webhook-*` headers) through a single `standardwebhooks` platform config. |
11 | 11 |
|
12 | 12 | > Need reliable delivery too? Tern supports inbound webhook delivery via Upstash QStash — automatic retries, DLQ management, replay controls, and Slack/Discord alerting. Bring your own Upstash account (BYOK). |
13 | 13 |
|
@@ -79,27 +79,6 @@ const result = await WebhookVerificationService.verifyAny(request, { |
79 | 79 | console.log(`Verified ${result.platform} webhook`); |
80 | 80 | ``` |
81 | 81 |
|
82 | | -### Twilio example |
83 | | - |
84 | | -```typescript |
85 | | -import { WebhookVerificationService } from '@hookflo/tern'; |
86 | | - |
87 | | -export async function POST(request: Request) { |
88 | | - const result = await WebhookVerificationService.verify(request, { |
89 | | - platform: 'twilio', |
90 | | - secret: process.env.TWILIO_AUTH_TOKEN!, |
91 | | - // Optional when behind proxies/CDNs if request.url differs from the public Twilio URL: |
92 | | - twilioBaseUrl: 'https://yourdomain.com/api/webhooks/twilio', |
93 | | - }); |
94 | | - |
95 | | - if (!result.isValid) { |
96 | | - return Response.json({ error: result.error }, { status: 400 }); |
97 | | - } |
98 | | - |
99 | | - return Response.json({ ok: true }); |
100 | | -} |
101 | | -``` |
102 | | - |
103 | 82 | ### Core SDK (runtime-agnostic) |
104 | 83 |
|
105 | 84 | Use Tern without framework adapters in any runtime that supports the Web `Request` API. |
@@ -214,17 +193,16 @@ app.post('/webhooks/stripe', createWebhookHandler({ |
214 | 193 | | **Doppler** | HMAC-SHA256 | ✅ Tested | |
215 | 194 | | **Sanity** | HMAC-SHA256 | ✅ Tested | |
216 | 195 | | **Svix** | HMAC-SHA256 | ⚠️ Untested for now | |
| 196 | +| **Standard Webhooks** (`standardwebhooks`) | HMAC-SHA256 | ✅ Tested | |
217 | 197 | | **Linear** | HMAC-SHA256 | ⚠️ Untested for now | |
218 | | -| **PagerDuty** | HMAC-SHA256 | ⚠️ Untested for now | |
219 | | -| **Twilio** | HMAC-SHA1 | ⚠️ Untested for now | |
220 | 198 | | **Razorpay** | HMAC-SHA256 | 🔄 Pending | |
221 | 199 | | **Vercel** | HMAC-SHA256 | 🔄 Pending | |
222 | 200 |
|
223 | 201 | > Don't see your platform? [Use custom config](#custom-platform-configuration) or [open an issue](https://github.com/Hookflo/tern/issues). |
224 | 202 |
|
225 | 203 | ### Platform signature notes |
226 | 204 |
|
227 | | -- **Standard Webhooks style** platforms (Clerk, Dodo Payments, Polar, ReplicateAI) commonly use a secret that starts with `whsec_...`. |
| 205 | +- **Standard Webhooks style** providers are supported via the canonical `standardwebhooks` platform (with aliases for both `webhook-*` and `svix-*` headers). Clerk, Dodo Payments, Polar, and ReplicateAI all follow this pattern and commonly use a secret that starts with `whsec_...`. |
228 | 206 | - **ReplicateAI**: copy the webhook signing secret from your Replicate webhook settings and pass it directly as `secret`. |
229 | 207 | - **fal.ai**: supports JWKS key resolution out of the box — use `secret: ''` for auto key resolution, or pass a PEM public key explicitly. |
230 | 208 |
|
@@ -365,25 +343,31 @@ const result = await WebhookVerificationService.verify(request, { |
365 | 343 | }); |
366 | 344 | ``` |
367 | 345 |
|
368 | | -### Svix / Standard Webhooks format (Clerk, Dodo Payments, ReplicateAI, etc.) |
| 346 | +### Standard Webhooks config helpers (Svix-style and webhook-* headers) |
369 | 347 |
|
370 | 348 | ```typescript |
371 | | -const svixConfig = { |
372 | | - platform: 'my-svix-platform', |
373 | | - secret: 'whsec_abc123...', |
| 349 | +import { |
| 350 | + createStandardWebhooksConfig, |
| 351 | + STANDARD_WEBHOOKS_BASE, |
| 352 | +} from '@hookflo/tern'; |
| 353 | + |
| 354 | +const signatureConfig = createStandardWebhooksConfig({ |
| 355 | + id: 'webhook-id', |
| 356 | + timestamp: 'webhook-timestamp', |
| 357 | + signature: 'webhook-signature', |
| 358 | + idAliases: ['svix-id'], |
| 359 | + timestampAliases: ['svix-timestamp'], |
| 360 | + signatureAliases: ['svix-signature'], |
| 361 | +}); |
| 362 | + |
| 363 | +const result = await WebhookVerificationService.verify(request, { |
| 364 | + platform: 'standardwebhooks', |
| 365 | + secret: process.env.STANDARD_WEBHOOKS_SECRET!, |
374 | 366 | signatureConfig: { |
375 | | - algorithm: 'hmac-sha256', |
376 | | - headerName: 'webhook-signature', |
377 | | - headerFormat: 'raw', |
378 | | - timestampHeader: 'webhook-timestamp', |
379 | | - timestampFormat: 'unix', |
380 | | - payloadFormat: 'custom', |
381 | | - customConfig: { |
382 | | - payloadFormat: '{id}.{timestamp}.{body}', |
383 | | - idHeader: 'webhook-id', |
384 | | - }, |
| 367 | + ...STANDARD_WEBHOOKS_BASE, |
| 368 | + ...signatureConfig, |
385 | 369 | }, |
386 | | -}; |
| 370 | +}); |
387 | 371 | ``` |
388 | 372 |
|
389 | 373 | See the [SignatureConfig type](https://tern.hookflo.com) for all options. |
@@ -431,8 +415,6 @@ interface WebhookVerificationResult { |
431 | 415 |
|
432 | 416 | ## Troubleshooting |
433 | 417 |
|
434 | | -- **Twilio invalid signature behind proxies/CDNs**: if your runtime `request.url` differs from the public Twilio webhook URL, pass `twilioBaseUrl` in `WebhookVerificationService.verify(...)` for platform `twilio`. |
435 | | - |
436 | 418 |
|
437 | 419 | **`Module not found: Can't resolve "@hookflo/tern/nextjs"`** |
438 | 420 |
|
|
0 commit comments