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-signature header (t=<timestamp>,v1=<signature>)?
  • How do I handle Langfuse prompt version created, updated and deleted events?
  • Why does my Langfuse webhook signature verification fail (raw body, timestamp prefix)?
  • Why does an updated Langfuse 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.js await request.arrayBuffer(), FastAPI await request.body()). Never JSON.parse/stringify first.
  • Use t exactly 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 no t= or no v1= fails.
  • Hex is case-insensitive: require exactly 64 hex characters, then compare bytes in constant time.
  • Python: hmac.compare_digest on bytes (bytes.fromhex after 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_WEBHOOK_TOLERANCE_SECONDS (unset or 0 = off); t is 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.

actionSent When
createdA prompt version is created
updatedA prompt version's labels or tags change (two deliveries: the version gaining and the version losing the label/tag)
deletedA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 12, 2026

View on GitHub →