Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions content/docs/observability/features/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"comments",
"corrections",
"user-feedback",
"web-callbacks",
"log-levels",
"agent-graphs",
"masking",
Expand Down
165 changes: 165 additions & 0 deletions content/docs/observability/features/web-callbacks.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
---
title: Web callbacks
description: Trigger a browser-side HTTP callback from a trace or observation in Langfuse.
sidebarTitle: Web Callbacks
---

# Web callbacks
Comment thread
claude[bot] marked this conversation as resolved.

Web callbacks let project members trigger an HTTP `POST` request from a trace or observation in Langfuse. Use them to connect trace debugging to your own tools, for example opening an internal investigation workflow, notifying an external system, or sending a selected trace ID to a local helper service.

Unlike [prompt webhooks](/docs/prompt-management/features/webhooks-slack-integrations), web callbacks are not event-driven automations. A project member manually triggers them from the trace detail menu.

<Callout type="info">
Web callbacks send only identifiers. Trace input, output, metadata, scores,
and user data are not included in the callback payload.
</Callout>

## Configure a web callback

1. Open your project in Langfuse.
2. Go to `Project Settings` > `Integrations` > `Web Callbacks`.
3. Create or edit the callback endpoint.
4. Configure the endpoint name, URL, toast message, request timeout, and optional browser-visible headers.
5. Save the endpoint and make sure it is enabled.

Only one web callback endpoint can be configured per project.

## Trigger a callback

Open a trace or observation and click the three-dot actions menu in the detail header. If an enabled endpoint exists, the menu shows `Call <callback name>`.

When you click the action, Langfuse immediately shows the configured toast message and sends the request from your browser to the configured endpoint.

## Request payload

Langfuse sends a JSON `POST` request with the following payload:

```json filename="web-callback-payload.json"
{
"version": 1,
"items": [
{
"projectId": "project-id",
"traceId": "trace-id",
"observationId": null
}
]
}
```

Payload fields:

- `version`: Payload contract version. The current version is `1`.
- `items`: List of selected objects. V1 sends one item.
- `projectId`: Langfuse project ID.
- `traceId`: Trace ID for the selected trace or observation.
- `observationId`: `null` for trace-level callbacks, or the selected observation ID for observation-level callbacks.

If your receiver needs the full trace, observation, session, or score data, use the IDs from the payload to query the [Langfuse API](/docs/api-and-data-platform/features/public-api) from your own backend.

## Endpoint requirements

Your endpoint must:

- Accept `POST` requests with a JSON body.
- Return any HTTP `2xx` status for success.
- Respond before the configured timeout.
- Allow browser requests from your Langfuse origin via CORS.
- Respond to CORS preflight `OPTIONS` requests if you configure custom headers.

Check failure on line 69 in content/docs/observability/features/web-callbacks.mdx

View check run for this annotation

Claude / Claude Code Review

Misleading CORS preflight condition (JSON POSTs always trigger preflight)

The prose at lines 69 and 155 says CORS preflight is needed "if you configure custom headers", but a POST with `Content-Type: application/json` (which Langfuse sends, per line 36) **always** triggers a CORS preflight regardless of any custom headers — `application/json` is not a CORS-safelisted Content-Type per the Fetch spec. The example code on lines 112–116 correctly handles `OPTIONS` unconditionally, but a reader following the prose literally could omit `OPTIONS` handling assuming they have
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The prose at lines 69 and 155 says CORS preflight is needed "if you configure custom headers", but a POST with Content-Type: application/json (which Langfuse sends, per line 36) always triggers a CORS preflight regardless of any custom headers — application/json is not a CORS-safelisted Content-Type per the Fetch spec. The example code on lines 112–116 correctly handles OPTIONS unconditionally, but a reader following the prose literally could omit OPTIONS handling assuming they have no custom headers and then hit a preflight failure on the first callback. Suggested fix: reword both lines to attribute preflight to Content-Type: application/json rather than custom headers.

Extended reasoning...

What the bug is. Lines 69 and 155 of content/docs/observability/features/web-callbacks.mdx both condition CORS preflight handling on "custom headers":\n\n- Line 69 (Endpoint requirements): "Respond to CORS preflight OPTIONS requests if you configure custom headers."\n- Line 155 (Troubleshooting → CORS error): "If you use custom headers, the browser may send an OPTIONS preflight request before the POST; your server must handle that request too."\n\nBoth statements are incorrect. Per the Fetch spec, only three Content-Type values qualify a request as a CORS "simple request": application/x-www-form-urlencoded, multipart/form-data, and text/plain. application/json is not safelisted, so any POST with that Content-Type triggers a preflight unconditionally — custom headers or not.\n\nWhy this contradicts the doc itself. Three independent signals in the same file confirm preflight is always required:\n\n1. Line 36 explicitly says "Langfuse sends a JSON POST request", i.e. Content-Type: application/json.\n2. Line 110 of the example sets Access-Control-Allow-Headers: "content-type" — that header is only meaningful when Content-Type itself is a non-safelisted value and therefore subject to preflight checks.\n3. Lines 112–116 of the example handle OPTIONS unconditionally, with no "if custom headers" branch. The example correctly reflects that preflight is always needed; only the prose is wrong.\n\nImpact. This is a docs-accuracy bug, not a runtime bug — but it actively misleads implementers on their very first integration. A reader who configures no custom headers will follow line 69 literally, skip OPTIONS handling in their receiver, and the browser will reject the request before any POST body is sent. The error will surface as a CORS preflight failure, which is exactly the symptom described in the troubleshooting section the prose misexplains.\n\nStep-by-step proof. Suppose a user reads the doc and writes a minimal receiver that handles only POST (because they configured no custom headers):\n\n1. User triggers a web callback from a Langfuse trace.\n2. Browser sees outbound request: POST <endpoint> with Content-Type: application/json.\n3. Per Fetch spec §4.3, this is not a simple request (application/json is not in the safelisted Content-Type set), so the browser issues a CORS preflight: OPTIONS <endpoint> with Access-Control-Request-Method: POST and Access-Control-Request-Headers: content-type.\n4. The user's receiver returns 405 Method Not Allowed (or whatever its non-POST default is) — the preflight fails.\n5. The browser never sends the actual POST. Langfuse shows the "CORS error" toast.\n6. The user re-reads line 69, sees "if you configure custom headers", and is confused because they didn't configure any.\n\nHow to fix. Reword both lines to attribute preflight to the Content-Type, not custom headers. For example:\n\n- Line 69: "Respond to CORS preflight OPTIONS requests, because the callback uses Content-Type: application/json."\n- Line 155: "Because the request uses Content-Type: application/json, the browser sends an OPTIONS preflight before the POST; your server must handle that request too."


Langfuse treats non-2xx responses, timeouts, network errors, and CORS failures as failed callbacks and shows an error toast.

## Browser-side delivery

Web callbacks are delivered directly from the user's browser with `fetch`.

This has a few practical consequences:

- The receiver sees the user's browser as the client, not Langfuse servers.
- The receiver must allow CORS from the Langfuse origin.
- Custom headers are visible in browser developer tools.
- Headers are not suitable for secrets or private API keys.
- Langfuse does not retry failed callback requests in the background.

<Callout type="warn">
Do not put secrets into web callback headers. If the receiver needs privileged
access to Langfuse data, let the receiver call the Langfuse API with credentials
stored on the receiver side.
</Callout>

## Minimal receiver

The receiver can be any HTTP server. This example logs the payload and acknowledges the callback:

```ts filename="server.ts"
import http from "node:http";

type WebCallbackPayload = {
version: 1;
items: Array<{
projectId: string;
traceId: string;
observationId: string | null;
}>;
};

const server = http.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "https://cloud.langfuse.com");
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "content-type");

Check warning on line 110 in content/docs/observability/features/web-callbacks.mdx

View check run for this annotation

Claude / Claude Code Review

Example Access-Control-Allow-Headers omits custom headers configured in Langfuse

The example sets `Access-Control-Allow-Headers: "content-type"` but step 4 above invites users to configure "optional browser-visible headers" on the endpoint. Any custom header configured in the Langfuse UI (e.g. `X-Trace-Source`) will appear in the preflight's `Access-Control-Request-Headers` and be rejected by this static allowlist — causing a confusing CORS failure. Consider adding a comment noting that any custom headers configured in Langfuse must be appended to `Access-Control-Allow-Heade
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The example sets Access-Control-Allow-Headers: "content-type" but step 4 above invites users to configure "optional browser-visible headers" on the endpoint. Any custom header configured in the Langfuse UI (e.g. X-Trace-Source) will appear in the preflight's Access-Control-Request-Headers and be rejected by this static allowlist — causing a confusing CORS failure. Consider adding a comment noting that any custom headers configured in Langfuse must be appended to Access-Control-Allow-Headers, or calling this out in the troubleshooting CORS section (which currently mentions preflight but not the Allow-Headers requirement).

Extended reasoning...

What the bug is. In content/docs/observability/features/web-callbacks.mdx, the "Configure a web callback" section (step 4, line 23) tells the reader they can configure "optional browser-visible headers" on the endpoint. The "Browser-side delivery" section (line 81) reiterates that custom headers are visible in browser dev tools. But the minimal receiver example (line 110) hardcodes:

res.setHeader("Access-Control-Allow-Headers", "content-type");

A reader who follows step 4 and adds any custom header on the Langfuse side will hit a CORS preflight failure the moment they wire up this example.

The code path that triggers it. Per the CORS spec, when the browser issues a non-simple request (here a JSON POST with a custom header), it first sends an OPTIONS preflight whose Access-Control-Request-Headers lists every non-CORS-safelisted header the actual request will send. The server's Access-Control-Allow-Headers response must be a superset of that list, or the browser rejects the preflight and never issues the POST.

Why existing code/docs don't prevent it. The "Endpoint requirements" section (line 69) acknowledges that custom headers trigger preflight. The "Troubleshooting > CORS error" section (line 155) repeats that the server must handle preflight — but neither section actually names Access-Control-Allow-Headers or tells the reader to extend it. The "For production" guidance (line 145) calls out tightening Access-Control-Allow-Origin, validating the body, and keeping credentials on the server, but never mentions Allow-Headers. So a user who diligently reads the whole page can still miss this.

Impact. Pure documentation completeness. The example works fine for the default no-custom-headers case (which is why this is a nit, not a normal-severity bug). But the doc itself invites the very configuration that breaks the example, and the failure mode — a CORS preflight error toast on first click — is exactly the kind of friction users blame the example for.

Step-by-step proof.

  1. User follows step 4 and adds a custom header X-Trace-Source: ui on the Langfuse endpoint config.
  2. User copies the minimal receiver example verbatim and runs it on http://127.0.0.1:4047.
  3. User clicks "Call ..." on a trace. The browser prepares a POST with headers content-type: application/json and x-trace-source: ui.
  4. Because x-trace-source is not on the CORS-safelisted-request-header list, the browser first sends OPTIONS / with Access-Control-Request-Headers: content-type, x-trace-source.
  5. The example server responds 204 with Access-Control-Allow-Headers: content-type.
  6. The browser sees x-trace-source is missing from the allowlist and aborts the preflight — the POST is never sent.
  7. Langfuse surfaces a CORS-error toast; nothing in the troubleshooting section points the reader back to the example's hardcoded Allow-Headers value.

How to fix. The lowest-friction fix is a one-line code comment in the example, e.g. // Append any custom headers configured in Langfuse (e.g. "content-type, x-trace-source"). Alternatively, extend the "CORS error" troubleshooting section to explicitly name Access-Control-Allow-Headers and tell readers to list every custom header configured in the Langfuse UI.


if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}

if (req.method !== "POST") {
res.writeHead(405);
res.end("Method not allowed");
return;
}

let body = "";

req.on("data", (chunk: Buffer) => {
body += chunk.toString("utf8");
});

req.on("end", () => {
const payload = JSON.parse(body) as WebCallbackPayload;

console.log("Received Langfuse web callback", payload);

res.writeHead(202, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
});
});

server.listen(4047, "127.0.0.1", () => {
console.log("Listening on http://127.0.0.1:4047");
});
```

For production, set `Access-Control-Allow-Origin` to your Langfuse deployment URL, validate the request body, and keep any Langfuse API credentials on the server.

## Troubleshooting

### Callback endpoint returned HTTP 404

The request reached your endpoint, but the path does not exist. Check the configured URL path and make sure the receiver handles `POST` requests at that route.

### CORS error

Because the request is sent from the browser, the receiver must allow the Langfuse origin. If you use custom headers, the browser may send an `OPTIONS` preflight request before the `POST`; your server must handle that request too.

### Callback request timed out

Increase the timeout in the endpoint settings or make the receiver return a `2xx` response faster. For longer jobs, acknowledge the callback quickly and process the work asynchronously.

## Related

- [Trace URLs](/docs/observability/features/url)
- [Sessions](/docs/observability/features/sessions)
- [Prompt webhooks](/docs/prompt-management/features/webhooks-slack-integrations)
2 changes: 1 addition & 1 deletion src/github-stars.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const GITHUB_STARS = 23619;
export const GITHUB_STARS = 27501;
Loading