Trigger.dev
Verify a Hypeline delivery with the standardwebhooks library in your own route, then run a Trigger.dev task with the event id as the idempotency key.
Trigger.dev runs background tasks in your own codebase. Trigger.dev v4 has no managed incoming-webhook trigger, so the pattern is a route you own that verifies the delivery and calls tasks.trigger(). Because that route is your code, you can verify with an off-the-shelf Standard Webhooks library, which proves the interop claim: Hypeline signs exactly the way OpenAI, Anthropic, Supabase, and Twilio webhooks do.
Step 1: register a destination pointing at your route
Deploy a route (for example /api/hypeline) and register its public HTTPS URL as a Hypeline destination:
curl -X POST https://api.hypeline.io/v1/alerts/{alert_id}/destinations \
-H "Authorization: Bearer $YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"kind":"webhook","url":"https://your-app.com/api/hypeline","delivery_mode":"instant"}'Store the whsec_ secret from the response as HYPELINE_WEBHOOK_SECRET.
Step 2: verify in the route with the standardwebhooks library
Install the library (npm i standardwebhooks). It expects the raw body bytes and the three webhook-* headers, and it does the whsec_ key derivation for you. Read the RAW body: verifying a re-serialized object breaks the HMAC.
// app/api/hypeline/route.ts (Next.js App Router)
import { Webhook } from "standardwebhooks";
import { tasks } from "@trigger.dev/sdk/v3";
import type { handlePageChange } from "@/trigger/handlePageChange";
const wh = new Webhook(process.env.HYPELINE_WEBHOOK_SECRET!);
export async function POST(req: Request) {
const body = await req.text(); // RAW body, not req.json()
const headers = {
"webhook-id": req.headers.get("webhook-id")!,
"webhook-timestamp": req.headers.get("webhook-timestamp")!,
"webhook-signature": req.headers.get("webhook-signature")!,
};
let event: any;
try {
// verify() checks the HMAC and the 5-minute timestamp freshness, then
// returns the parsed payload. It throws on a bad signature or a stale POST.
event = wh.verify(body, headers);
} catch {
return new Response("invalid signature", { status: 401 });
}
// Dedup anchor: the event UUIDv7 (== webhook-id). Trigger.dev drops a repeat.
await tasks.trigger<typeof handlePageChange>("handle-page-change", event, {
idempotencyKey: event.id,
});
return new Response("ok", { status: 200 });
}The Webhook constructor takes the whsec_ secret directly. If your version of the library wants the bare base64 without the prefix, strip it once with secret.replace(/^whsec_/, ""). Confirm against the library version you install.
Express or Hono follow the same shape: read the raw body (express.raw({ type: "*/*" }) or Hono's c.req.text()), pass the three headers to wh.verify, then trigger.
Step 3: define the task
// trigger/handlePageChange.ts
import { task } from "@trigger.dev/sdk/v3";
export const handlePageChange = task({
id: "handle-page-change",
run: async (event: {
id: string;
url: string;
title: string;
diff?: unknown;
tags: string[];
}) => {
// event is the StreamEvent Hypeline delivered.
return { handled: event.url };
},
});idempotencyKey: event.id is the teachable moment: delivery is at-least-once, and the event id is a UUIDv7 that never repeats for distinct events, so passing it as the idempotency key makes a redelivery a no-op.
Defense in depth: verify the content signature
The HMAC above proves the delivery reached your route intact. For end-to-end content authenticity that does not depend on the transport, also verify the event's Ed25519 signature against the published JWKS inside the task. It survives any re-serialization on the way in. The verifying events page has the Node recipe you can drop into the task before acting on event.
Notes
- This recipe pins
delivery_mode: "instant". A digest destination posts a batch wrapper with aneventsarray instead of a single event, so the route would iterate it; that shape is out of scope here. - The destination URL must be public HTTPS. A
localhostroute URL is rejected at registration by the SSRF guard, so deploy the route or tunnel to it.