Agent skill
Langfuse Webhooks Skill
Receive and verify Langfuse webhooks. Use when setting up a Langfuse prompt-management webhook endpoint, verifying the x-langfuse-signature header (t=,v1= HMAC-SHA256), debugging signature failures, or handling prompt version created, updated and deleted events.
Install this skill
npx skills add hookdeck/webhook-skills --skill langfuse-webhooks
Langfuse (the open-source LLM engineering platform) can run an Automation on prompt management: when a prompt version is created, updated (labels or tags) or deleted, a "Webhook Call" action sends a JSON notification to your URL. Each request is signed with the x-langfuse-signature header, so your endpoint can verify the sender and the exact body.
When to Use This Skill
- How do I receive Langfuse webhooks?
- How do I verify the
x-langfuse-signatureheader (t=<timestamp>,v1=<signature>)? - How do I handle Langfuse prompt version
created,updatedanddeletedevents? - Why does my Langfuse webhook signature verification fail (raw body, timestamp prefix)?
- Why does an
updatedLangfuse webhook arrive twice?
Verification (core)
The header is x-langfuse-signature: t=<timestamp>,v1=<hex>. The signature is the lowercase hex HMAC-SHA256 of <t>.<raw body>, keyed with the webhook's signing secret as UTF-8 bytes.
const crypto = require('crypto');
function verifyLangfuseSignature(rawBody /* Buffer */, header, secret) {
let t;
const sigs = [];
for (const part of String(header || '').split(',')) {
const i = part.indexOf('=');
if (i === -1) continue;
const k = part.slice(0, i).trim(), v = part.slice(i + 1).trim();
if (k === 't' && t === undefined) t = v; // keep t as the exact string sent
else if (k === 'v1') sigs.push(v); // may repeat (rotation); ignore v0 etc.
}
if (!secret || !t || sigs.length === 0) return false;
const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8'))
.update(Buffer.concat([Buffer.from(`${t}.`), rawBody])).digest();
return sigs.some((s) => // validate before decoding, then constant-time
/^[0-9a-fA-F]{64}$/.test(s) &&
crypto.timingSafeEqual(Buffer.from(s.toLowerCase(), 'hex'), expected));
}
- Sign bytes: the raw body exactly as received (
express.raw({ type: '*/*' }), Next.jsawait request.arrayBuffer(), FastAPIawait request.body()). NeverJSON.parse/stringifyfirst. - Use
texactly as it appears in the header; never parse it to a number and re-format it. - The secret is used as-is (UTF-8), not hex- or base64-decoded. Env var:
LANGFUSE_WEBHOOK_SECRET. - Accept the delivery if any
v1=entry matches. A header with not=or nov1=fails. - Hex is case-insensitive: require exactly 64 hex characters, then compare bytes in constant time.
- Python:
hmac.compare_digeston bytes (bytes.fromhexafter a[0-9a-fA-F]{64}check). - Missing header, malformed value or mismatch:
401. - No timestamp window by default. Hookdeck retries and replays resend the original headers and timestamp, so a fixed window would reject legitimate retries. To opt in, set
LANGFUSE_(unset orWEBHOOK_ TOLERANCE_ SECONDS 0= off);tis Unix seconds.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
Dispatch on the body's action field. type is always "prompt-version" today.
action | Sent When |
|---|---|
created | A prompt version is created |
updated | A prompt version's labels or tags change (two deliveries: the version gaining and the version losing the label/tag) |
deleted | A prompt version is deleted |
Only created is shown in Langfuse's documented payload; the lowercase spelling of updated and deleted follows the automation UI (Created / Updated / Deleted). Log and acknowledge anything else. The payload carries id, timestamp, type, apiVersion, action and a prompt object (name, version, labels, tags, prompt, config, commitMessage, ...). See references/overview.md.
Delivery
POST with a JSON body. A 2xx confirms receipt; otherwise Langfuse may retry with exponential back-off until it succeeds. No timeout or attempt limit is documented, so make the handler idempotent (key on the payload id plus action) and respond quickly.
Environment Variables
LANGFUSE_WEBHOOK_SECRET=your_langfuse_webhook_secret # shown when the webhook is created; regenerable
# LANGFUSE_WEBHOOK_TOLERANCE_SECONDS=300 # optional opt-in replay window; off by default
Local Development
Langfuse can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 langfuse --path /webhooks/langfuse
Resources
- references/overview.md - What Langfuse webhooks are, events and payload
- references/setup.md - Create the automation and webhook action, get the signing secret
- references/verification.md - Signature details, gotchas, debugging
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests