Hookflo Tern
Use this skill for requests touching webhooks, event-driven infrastructure, or real-time HTTP callba
- Rating
- 4.9 (188 reviews)
- Downloads
- 507 downloads
- Version
- 1.0.0
Overview
Use this skill for requests touching webhooks, event-driven infrastructure, or real-time HTTP callbacks.
Complete Documentation
View Source →
Hookflo + Tern Webhook Skill
This skill covers two tightly related tools in the Hookflo ecosystem:
- Tern (
@hookflo/tern) — an open-source, zero-dependency TypeScript library for
- Hookflo — a hosted webhook event alerting and logging platform. Sends real-time
Mental Model
Incoming Webhook Request
│
▼
[Tern] verify signature ←── your server/edge function
│
isValid?
│
yes │ no
│──────► 400 / reject
│
▼
process payload
│
(optionally forward to)
▼
[Hookflo] alert + log
Slack / Email / Dashboard
Use Tern when you need programmatic signature verification in your own code. Use Hookflo when you want no-code / low-code alerting and centralized event logs. They can be used together or independently.
Part 1 — Tern (Webhook Verification Library)
Installation
npm install @hookflo/tern
No other dependencies required. Full TypeScript support.
Core API
#### WebhookVerificationService.verify(request, config)
The primary method. Returns a WebhookVerificationResult.
import { WebhookVerificationService } from '@hookflo/tern';
const result = await WebhookVerificationService.verify(request, {
platform: 'stripe',
secret: process.env.STRIPE_WEBHOOK_SECRET!,
toleranceInSeconds: 300, // replay attack protection window (optional, default 300)
});
if (result.isValid) {
console.log('Verified payload:', result.payload);
console.log('Metadata:', result.metadata); // timestamp, id, etc.
} else {
console.error('Rejected:', result.error);
// return 400
}
#### WebhookVerificationService.verifyWithPlatformConfig(request, platform, secret, tolerance?)
Shorthand that accepts just a platform name + secret.
const result = await WebhookVerificationService.verifyWithPlatformConfig(
request,
'github',
process.env.GITHUB_WEBHOOK_SECRET!
);
#### WebhookVerificationService.verifyTokenBased(request, webhookId, webhookToken)
For token-based platforms (Supabase, GitLab).
const result = await WebhookVerificationService.verifyTokenBased(
request,
process.env.SUPABASE_WEBHOOK_ID!,
process.env.SUPABASE_WEBHOOK_TOKEN!
);
WebhookVerificationResult type
interface WebhookVerificationResult {
isValid: boolean;
error?: string;
platform: WebhookPlatform;
payload?: any; // parsed JSON body
metadata?: {
timestamp?: string;
id?: string | null;
[key: string]: any;
};
}
Built-in Platform Configs
| Platform | Algorithm | Signature Header | Format |
|---|---|---|---|
| stripe | HMAC-SHA256 | stripe-signature | t={ts},v1={sig} |
| github | HMAC-SHA256 | x-hub-signature-256 | sha256={sig} |
| clerk | HMAC-SHA256 (base64) | svix-signature | v1,{sig} |
| supabase | Token-based | custom | — |
| gitlab | Token-based | x-gitlab-token | — |
| shopify | HMAC-SHA256 | x-shopify-hmac-sha256 | raw |
| vercel | HMAC-SHA256 | custom | — |
| polar | HMAC-SHA256 | custom | — |
| dodo | HMAC-SHA256 (svix) | webhook-signature | v1,{sig} |
'stripe', 'github').Custom Platform Configuration
For any provider not in the list, supply a full signatureConfig:
import { WebhookVerificationService } from '@hookflo/tern';
// Standard HMAC-SHA256 with prefix
const result = await WebhookVerificationService.verify(request, {
platform: 'acmepay',
secret: 'your_secret',
signatureConfig: {
algorithm: 'hmac-sha256',
headerName: 'x-acme-signature',
headerFormat: 'prefixed',
prefix: 'sha256=',
payloadFormat: 'raw',
},
});
// Timestamped payload (signs "{timestamp}.{body}")
const result2 = await WebhookVerificationService.verify(request, {
platform: 'mypay',
secret: 'your_secret',
signatureConfig: {
algorithm: 'hmac-sha256',
headerName: 'x-webhook-signature',
headerFormat: 'raw',
timestampHeader: 'x-webhook-timestamp',
timestampFormat: 'unix',
payloadFormat: 'timestamped',
},
});
// Svix/StandardWebhooks compatible (Clerk, Dodo, etc.)
const result3 = await WebhookVerificationService.verify(request, {
platform: 'my-svix-platform',
secret: 'whsec_abc123...',
signatureConfig: {
algorithm: 'hmac-sha256',
headerName: 'webhook-signature',
headerFormat: 'raw',
timestampHeader: 'webhook-timestamp',
timestampFormat: 'unix',
payloadFormat: 'custom',
customConfig: {
payloadFormat: '{id}.{timestamp}.{body}',
idHeader: 'webhook-id',
},
},
});
SignatureConfig fields:
algorithm:'hmac-sha256'|'hmac-sha1'|'hmac-sha512'| customheaderName: the HTTP header that carries the signatureheaderFormat:'raw'|'prefixed'|'comma-separated'|'space-separated'prefix: string prefix to strip before comparing (e.g.'sha256=')timestampHeader: header name for the timestamp (if any)timestampFormat:'unix'|'iso'|'ms'payloadFormat:'raw'|'timestamped'|'custom'customConfig.payloadFormat: template like'{id}.{timestamp}.{body}'customConfig.idHeader: header supplying the{id}valuecustomConfig.encoding:'base64'if the provider base64-encodes the key
Framework Integration
#### Express.js
import express from 'express';
import { WebhookVerificationService } from '@hookflo/tern';
const app = express();
// IMPORTANT: use raw body parser for webhook routes
app.post(
'/webhooks/stripe',
express.raw({ type: 'application/json' }),
async (req, res) => {
const result = await WebhookVerificationService.verifyWithPlatformConfig(
req,
'stripe',
process.env.STRIPE_WEBHOOK_SECRET!
);
if (!result.isValid) {
return res.status(400).json({ error: result.error });
}
const event = result.payload;
// handle event.type, e.g. 'payment_intent.succeeded'
res.json({ received: true });
}
);
Common mistake: Express's default json() middleware consumes and re-serializes
the body, breaking HMAC. Always use express.raw() on webhook endpoints.#### Next.js App Router (Route Handler)
// app/api/webhooks/github/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { WebhookVerificationService } from '@hookflo/tern';
export async function POST(req: NextRequest) {
const result = await WebhookVerificationService.verifyWithPlatformConfig(
req,
'github',
process.env.GITHUB_WEBHOOK_SECRET!
);
if (!result.isValid) {
return NextResponse.json({ error: result.error }, { status: 400 });
}
const event = req.headers.get('x-github-event');
// handle event
return NextResponse.json({ received: true });
}
// Disable body parsing so Tern gets the raw body
export const config = { api: { bodyParser: false } };
#### Cloudflare Workers
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request: Request): Promise<Response> {
if (request.method === 'POST' && new URL(request.url).pathname === '/webhooks/clerk') {
const result = await WebhookVerificationService.verifyWithPlatformConfig(
request,
'clerk',
CLERK_WEBHOOK_SECRET
);
if (!result.isValid) {
return new Response(JSON.stringify({ error: result.error }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response(JSON.stringify({ received: true }));
}
return new Response('Not Found', { status: 404 });
}
Platform Manager (Advanced)
import { platformManager } from '@hookflo/tern';
// Verify using the platform manager directly
const result = await platformManager.verify(request, 'stripe', 'whsec_...');
// Get the config for a platform (for inspection)
const config = platformManager.getConfig('stripe');
// Get docs/metadata for a platform
const docs = platformManager.getDocumentation('stripe');
// Run built-in tests for a platform
const passed = await platformManager.runPlatformTests('stripe');
Testing
npm test # run all tests
npm run test:platform stripe # test one platform
npm run test:all # test all platforms
Part 2 — Hookflo (Hosted Alerting Platform)
Hookflo requires no library installation. The integration is:
- Create a webhook endpoint in the Hookflo Dashboard → get a Webhook URL + Secret
- Point your provider (Stripe, Supabase, Clerk, GitHub, etc.) at that URL
- Configure Slack/email notifications in the dashboard
How to Set Up a Hookflo Integration
Step 1 — Go to hookflo.com/dashboard and create a new webhook. You'll receive:
- Webhook URL — paste into your provider's webhook settings
- Webhook ID — used for token-based platforms
- Secret Token — used by Hookflo to verify incoming events
- Notification channel settings — configure Slack or email
| Provider | Where to paste the URL |
|---|---|
| Stripe | Dashboard → Developers → Webhooks → Add endpoint |
| Supabase | Dashboard → Database → Webhooks → Create webhook |
| Clerk | Dashboard → Webhooks → Add endpoint |
| GitHub | Repo/Org Settings → Webhooks → Add webhook |
- Which event types to alert on (e.g.,
payment_intent.succeeded,user.created) - Notification channels (Slack workspace/channel, email addresses)
- Digest frequency if you want batched summaries instead of per-event alerts
Hookflo Platform Docs
- Stripe: docs.hookflo.com/webhook-platforms/stripe
- Supabase: docs.hookflo.com/webhook-platforms/supabase
- Clerk: docs.hookflo.com/webhook-platforms/clerk
- GitHub: docs.hookflo.com/webhook-platforms/github
- Slack notifications: docs.hookflo.com/notification-channels/slack
Hookflo + Tern Together
If you want both programmatic verification (Tern) AND logging/alerting (Hookflo), use a proxy pattern:
// Your server receives the webhook, verifies it with Tern, then forwards to Hookflo
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
// 1. Verify with Tern
const result = await WebhookVerificationService.verifyWithPlatformConfig(
req, 'stripe', process.env.STRIPE_WEBHOOK_SECRET!
);
if (!result.isValid) return res.status(400).json({ error: result.error });
// 2. Process locally
handleStripeEvent(result.payload);
// 3. Forward to Hookflo for alerting/logging (optional)
await fetch(process.env.HOOKFLO_WEBHOOK_URL!, {
method: 'POST',
headers: { ...req.headers, 'Content-Type': 'application/json' },
body: req.body,
});
res.json({ received: true });
});
Alternatively, point Stripe directly at your Hookflo URL and keep Tern for a different endpoint.
Common Pitfalls & Best Practices
Raw Body Requirement
HMAC signatures are computed over the exact raw bytes of the request body. Any re-serialization (e.g., by a JSON body parser) will break verification. Always ensure:- Express: use
express.raw({ type: 'application/json' })on webhook routes - Next.js Pages Router: set
export const config = { api: { bodyParser: false } } - Next.js App Router: Tern reads the body directly from the
Requestobject
Replay Attack Protection
Always passtoleranceInSeconds (default is 300 = 5 minutes). This rejects requests
with timestamps too far in the past, preventing replay attacks.Secrets Management
- Never hardcode secrets in source code
- Use environment variables:
process.env.STRIPE_WEBHOOK_SECRET - For Cloudflare Workers: use
wrangler secret put STRIPE_WEBHOOK_SECRET - For Vercel: add secrets in project settings
Error Responses
Always return HTTP 400 (not 500) for failed verification — this signals to the sender that the request was rejected (not that your server crashed).HTTPS Only
Webhook endpoints must use HTTPS in production. Never accept webhook traffic over HTTP.Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| isValid: false, error about signature | Body was parsed before Tern | Use raw body parser |
| isValid: false, error about timestamp | Clock skew or replay attack | Check server clock; increase tolerance if dev |
| isValid: false for Clerk | Missing svix headers | Ensure svix-id, svix-timestamp, svix-signature are forwarded |
| isValid: false for GitHub | Wrong secret | Re-copy secret from GitHub Webhooks settings |
| Tern not finding platform | Typo in platform name | Use lowercase: 'stripe', 'github', 'clerk' |
| Hookflo not receiving events | Wrong URL pasted | Re-copy URL from Hookflo dashboard |
Key Links
- Tern GitHub: https://github.com/Hookflo/tern
- Tern npm: https://www.npmjs.com/package/@hookflo/tern
- Tern docs: https://tern.hookflo.com
- Hookflo homepage: https://hookflo.com
- Hookflo dashboard: https://hookflo.com/dashboard
- Hookflo docs: https://docs.hookflo.com
- Hookflo Discord: https://discord.com/invite/SNmCjU97nr
Installation
openclaw install hookflo-tern
💻Code Examples
Slack / Email / Dashboard
Use **Tern** when you need programmatic signature verification in your own code.
Use **Hookflo** when you want no-code / low-code alerting and centralized event logs.
They can be used together or independently.
---
## Part 1 — Tern (Webhook Verification Library)
### Installationnpm install @hookflo/tern
No other dependencies required. Full TypeScript support.
### Core API
#### `WebhookVerificationService.verify(request, config)`
The primary method. Returns a `WebhookVerificationResult`.}
#### `WebhookVerificationService.verifyWithPlatformConfig(request, platform, secret, tolerance?)`
Shorthand that accepts just a platform name + secret.);
#### `WebhookVerificationService.verifyTokenBased(request, webhookId, webhookToken)`
For token-based platforms (Supabase, GitLab).}
---
### Built-in Platform Configs
| Platform | Algorithm | Signature Header | Format |
|---|---|---|---|
| `stripe` | HMAC-SHA256 | `stripe-signature` | `t={ts},v1={sig}` |
| `github` | HMAC-SHA256 | `x-hub-signature-256` | `sha256={sig}` |
| `clerk` | HMAC-SHA256 (base64) | `svix-signature` | `v1,{sig}` |
| `supabase` | Token-based | custom | — |
| `gitlab` | Token-based | `x-gitlab-token` | — |
| `shopify` | HMAC-SHA256 | `x-shopify-hmac-sha256` | raw |
| `vercel` | HMAC-SHA256 | custom | — |
| `polar` | HMAC-SHA256 | custom | — |
| `dodo` | HMAC-SHA256 (svix) | `webhook-signature` | `v1,{sig}` |
Always use the lowercase string name (e.g., `'stripe'`, `'github'`).
---
### Custom Platform Configuration
For any provider not in the list, supply a full `signatureConfig`:});
**`SignatureConfig` fields:**
- `algorithm`: `'hmac-sha256'` | `'hmac-sha1'` | `'hmac-sha512'` | custom
- `headerName`: the HTTP header that carries the signature
- `headerFormat`: `'raw'` | `'prefixed'` | `'comma-separated'` | `'space-separated'`
- `prefix`: string prefix to strip before comparing (e.g. `'sha256='`)
- `timestampHeader`: header name for the timestamp (if any)
- `timestampFormat`: `'unix'` | `'iso'` | `'ms'`
- `payloadFormat`: `'raw'` | `'timestamped'` | `'custom'`
- `customConfig.payloadFormat`: template like `'{id}.{timestamp}.{body}'`
- `customConfig.idHeader`: header supplying the `{id}` value
- `customConfig.encoding`: `'base64'` if the provider base64-encodes the key
---
### Framework Integration
#### Express.js);
> **Common mistake**: Express's default `json()` middleware consumes and re-serializes
> the body, breaking HMAC. Always use `express.raw()` on webhook endpoints.
#### Next.js App Router (Route Handler)}
---
### Platform Manager (Advanced)const passed = await platformManager.runPlatformTests('stripe');
---
### Testingnpm run test:all # test all platforms
---
## Part 2 — Hookflo (Hosted Alerting Platform)
Hookflo requires **no library installation**. The integration is:
1. Create a webhook endpoint in the Hookflo Dashboard → get a Webhook URL + Secret
2. Point your provider (Stripe, Supabase, Clerk, GitHub, etc.) at that URL
3. Configure Slack/email notifications in the dashboard
### How to Set Up a Hookflo Integration
**Step 1** — Go to [hookflo.com/dashboard](https://hookflo.com/dashboard/webhooks) and create a new webhook.
You'll receive:
- **Webhook URL** — paste into your provider's webhook settings
- **Webhook ID** — used for token-based platforms
- **Secret Token** — used by Hookflo to verify incoming events
- **Notification channel settings** — configure Slack or email
**Step 2** — Set up the provider to send to that Hookflo URL:
| Provider | Where to paste the URL |
|---|---|
| Stripe | Dashboard → Developers → Webhooks → Add endpoint |
| Supabase | Dashboard → Database → Webhooks → Create webhook |
| Clerk | Dashboard → Webhooks → Add endpoint |
| GitHub | Repo/Org Settings → Webhooks → Add webhook |
**Step 3** — In the Hookflo dashboard, configure:
- Which event types to alert on (e.g., `payment_intent.succeeded`, `user.created`)
- Notification channels (Slack workspace/channel, email addresses)
- Digest frequency if you want batched summaries instead of per-event alerts
### Hookflo Platform Docs
- **Stripe**: [docs.hookflo.com/webhook-platforms/stripe](https://docs.hookflo.com/webhook-platforms/stripe)
- **Supabase**: [docs.hookflo.com/webhook-platforms/supabase](https://docs.hookflo.com/webhook-platforms/supabase)
- **Clerk**: [docs.hookflo.com/webhook-platforms/clerk](https://docs.hookflo.com/webhook-platforms/clerk)
- **GitHub**: [docs.hookflo.com/webhook-platforms/github](https://docs.hookflo.com/webhook-platforms/github)
- **Slack notifications**: [docs.hookflo.com/notification-channels/slack](https://docs.hookflo.com/notification-channels/slack)
### Hookflo + Tern Together
If you want both programmatic verification (Tern) AND logging/alerting (Hookflo), use a proxy pattern:Tags
Quick Info
Ready to Install?
Get started with this skill in seconds
Related Skills
4claw
4claw — a moderated imageboard for AI agents.
Aap Passport
Agent Attestation Protocol - The Reverse Turing Test.
Acestep Lyrics Transcription
Transcribe audio to timestamped lyrics using OpenAI Whisper or ElevenLabs Scribe API.
Adaptive Suite
A continuously adaptive skill suite that empowers Clawdbot.