Gareth Wilson Gareth Wilson

Guide to Langfuse Webhooks: Features and Best Practices

Published


Langfuse is an open-source LLM engineering platform for tracing, evaluating and managing the prompts behind AI applications. Its prompt management lets teams version prompts and promote a version to production with a label, without a code deploy. Webhooks close the loop: when someone publishes, relabels or deletes a prompt version, Langfuse can tell your application to refresh its prompt cache, run an evaluation suite or record the change.

This guide covers Langfuse's prompt-version Automations, why a single label move sends two webhooks, verifying x-langfuse-signature, and what Langfuse leaves undocumented about retries.

What are Langfuse webhooks?

Webhooks in Langfuse are one of two actions an Automation can run (the other posts to Slack). You create an Automation in a project, choose which prompt-version changes trigger it, optionally limit it to particular prompts, and pick Webhook Call with your URL and any static headers you want added.

When a matching change happens, Langfuse sends a JSON POST describing the prompt version and the action (created, updated or deleted). The payload includes the whole version, including the prompt text, labels, tags and config, so a receiver can update a cache without calling the Langfuse API.

Each request is signed. x-langfuse-signature uses the same t=...,v1=... layout as Stripe: a timestamp, and a hex HMAC-SHA256 of the timestamp and body.

Langfuse webhook features

FeatureDetails
ConfigurationProject Prompts > Automations, action Webhook Call
TriggersPrompt version Created, Updated (labels or tags), Deleted
Signaturehex HMAC-SHA256
Signature headerx-langfuse-signature (t=<timestamp>,v1=<hex>)
Signed content<t>.<raw body>
KeySigning secret per Automation, shown at creation and regenerable
BodyJSON
Event routingBody field action
Custom headersStatic headers configurable per Automation
RetriesExponential back-off until a 2xx; no limit or timeout documented

Events and payloads

actionTriggered WhenCommon Use Cases
createdA prompt version is createdSync prompts to a repo, notify a channel, trigger CI evals
updatedA version's labels or tags changeRedeploy when production moves, refresh a prompt cache
deletedA prompt version is deletedRemove cached copies, audit trail

When a label such as production moves from version 3 to version 4, Langfuse sends two updated deliveries: one for version 4, which gained the label, and one for version 3, which lost it. Process both, and work out the result from the labels array on each rather than from the order they arrive in.

Langfuse documents a body only for created. The lowercase updated and deleted values follow the UI labels (Created, Updated, Deleted), so log any action you don't expect and still return 2xx.

Published payload example

This created delivery comes from the webhook samples library. type is always prompt-version today, and apiVersion is v1, though Langfuse doesn't describe that field.

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2024-07-10T10:30:00Z",
  "type": "prompt-version",
  "apiVersion": "v1",
  "action": "created",
  "prompt": {
    "id": "prompt_abc123",
    "name": "movie-critic",
    "version": 3,
    "projectId": "xyz789",
    "labels": [
      "production",
      "latest"
    ],
    "prompt": "As a {{criticLevel}} movie critic, rate {{movie}} out of 10.",
    "type": "text",
    "config": {
      "key": "value"
    },
    "commitMessage": "Improved critic persona",
    "tags": [
      "entertainment"
    ],
    "createdAt": "2024-07-10T10:30:00Z",
    "updatedAt": "2024-07-10T10:30:00Z"
  }
}

Setting up Langfuse webhooks

Prerequisites

  • A Langfuse project (Langfuse Cloud or self-hosted) with permission to manage prompt automations
  • A public HTTPS endpoint for your handler

How Do I Register an Endpoint?

  1. In Langfuse, open your project and go to Prompts > Automations.
  2. Create an Automation and choose the prompt-version events to watch: Created, Updated and/or Deleted. Optionally restrict it to specific prompts.
  3. Choose the Webhook Call action and enter your endpoint URL. Add any static headers your receiver needs.
  4. Save.

How Do I Get the Signing Secret?

Langfuse shows the signing secret when you create the webhook Automation. Copy it into WEBHOOK_SECRET exactly as shown; it's used as UTF-8 bytes, not decoded. You can regenerate it later, which invalidates the old one, so update your receiver at the same time.

Langfuse's UI labels can vary between versions; the official guide has the current wording.

Test a delivery

Start a local receiver and forward requests with the Hookdeck CLI:

npx hookdeck-cli listen 3000 langfuse --path /webhooks/langfuse

Use the CLI's URL in the Automation, then save a new version of a test prompt. Moving a label afterwards shows the paired updated deliveries.

Securing Langfuse webhooks

Split x-langfuse-signature on commas into t and v1. Build the message as t, a dot and the raw body, compute HMAC-SHA256 with the signing secret, and compare the hex result with v1 in constant time. Langfuse's Python and TypeScript samples follow the same steps; see the webhook documentation.

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-langfuse-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const parts = signature.split(',').map((part) => part.trim());
  const times = parts.filter((part) => part.startsWith('t=')).map((part) => part.slice(2));
  if (times.length !== 1 || !/^[0-9]+$/.test(times[0])) return false;
  const message = Buffer.concat([Buffer.from(`${times[0]}.`, 'utf8'), rawBody]);
  const candidates = parts.filter((part) => part.startsWith('v1=')).map((part) => part.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

The handler deduplicates on the event id and keeps a cache of whichever version currently holds the production label.

const express = require('express');

function createLangfuseApp({ seen, promptCache, audit }) {
  const app = express();
  app.post('/webhooks/langfuse', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
    let parsed;
    try {
      parsed = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
    const { id, action, prompt } = parsed;
    if (!id || typeof action !== 'string' || !prompt || typeof prompt !== 'object') {
      return res.sendStatus(400);
    }
    try {
      if (await seen(id)) return res.sendStatus(200);
      await audit(action, prompt.name, prompt.version);
      if (action === 'deleted') {
        await promptCache.removeVersion(prompt.name, prompt.version);
      } else if (Array.isArray(prompt.labels) && prompt.labels.includes('production')) {
        await promptCache.setProduction(prompt.name, prompt);
      } else {
        await promptCache.clearIfProduction(prompt.name, prompt.version);
      }
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

The timestamp is signed, but Langfuse documents no tolerance for it, and its retries can run for an unknown time. If you add an age check in a direct receiver, keep it loose; the event id is the dependable guard against repeats.

Hookdeck's Langfuse source type reads t and v1, signs <t>.<raw body> with your secret and compares hex digests. It returns 200 to Langfuse in every case, so a bad signature doesn't trigger Langfuse's back-off; the request is logged as failed verification and kept from your destination. Hookdeck doesn't check the age of t.

Make Langfuse webhooks production-ready. Hookdeck Event Gateway verifies each Langfuse delivery, stores it, and fans prompt changes out to every service that caches prompts.

Langfuse webhook limitations and pain points

Two deliveries for one label move

The Problem: A cache briefly has no production prompt, or the wrong one, after a promotion.

Why It Happens: Moving a label fires one updated delivery for the version gaining it and another for the version losing it, and nothing guarantees their order.

Workarounds:

  • Act on the labels array in each delivery, never on the assumption that the second event undoes the first.
  • Only clear a cached production version if the delivery's version number matches the cached one.

How Hookdeck Can Help: Hookdeck's event log shows both deliveries side by side with their bodies, which makes ordering problems easy to see.

Undocumented retry limits

The Problem: You can't tell how long Langfuse keeps retrying, or when a failed delivery is abandoned.

Why It Happens: Langfuse says it retries with exponential back-off until it gets a 2xx, but gives no timeout, attempt count or spacing.

Workarounds:

  • Return 2xx quickly and do the work afterwards.
  • Re-read prompts through the Langfuse API on startup so a missed event can't leave a cache stale for long.

How Hookdeck Can Help: Hookdeck accepts each delivery straight away and applies its own documented retry policy to your service.

Partial payload documentation

The Problem: Parsers built for created may break on updated or deleted bodies.

Why It Happens: Only a created body is published, and apiVersion isn't described.

Workarounds:

  • Capture real updated and deleted deliveries in a test project and save them as fixtures.
  • Read only the fields you need and tolerate the rest.

How Hookdeck Can Help: Hookdeck keeps the full body of every delivery, so the undocumented shapes are visible from the first event.

Prompt events only

The Problem: You can't use Langfuse webhooks to react to traces, scores or evaluations.

Why It Happens: Automations currently trigger on prompt versions only.

Workarounds:

  • Use Langfuse's API or exports for trace and score data.

How Hookdeck Can Help: Hookdeck can still route prompt events alongside webhooks from your other AI tooling through one gateway.

Best practices

Use one Automation per consumer

Separate Automations, each with its own URL and secret, let a CI pipeline and a production cache subscribe to different prompts without sharing a key.

Keep the timestamp text as received

Build the signed message from the t string exactly as sent. Formatting it through a number can change it.

Mind what the payload contains

The body includes full prompt text and config. If prompts contain anything sensitive, keep the bodies out of general logs.

Acknowledge, then act

Langfuse expects a 2xx. Return it once the event is stored, then refresh caches or start evaluations, as described in the asynchronous processing guide. Keep a record of handled id values, following the idempotency guide.

Conclusion

Langfuse webhooks give prompt management a proper release signal: each version change arrives with the full prompt and a Stripe-style signature. The details to handle are the paired updated events on a label move, the thin documentation of updated and deleted bodies, and a retry policy with no stated limits.

Hookdeck Event Gateway stores and verifies those prompt events and delivers them to as many consumers as you need.

Get started with Hookdeck for free and handle Langfuse webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.