> This page is for Gateway.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.merge.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.merge.dev/_mcp/server.

# Tracing

> Send trace headers on requests you already make to group an agent run into a span tree in Logs, and group runs into threads and turns.

Tracing groups the requests in one run (an agent loop, chain, or pipeline step) into one trace that Logs shows as a tree. It's header-based, so adding `X-Merge-Trace-Id` to requests you already send works on every endpoint and compatible SDK.

## Trace a run

Send one trace ID on every request in a run. Optional span names make the tree easier to read.

**`cURL`**

```bash title="cURL"
TRACE_ID="run-$(uuidgen)"

curl https://api-gateway.merge.dev/v1/responses \
  -H "Authorization: Bearer $MERGE_GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Merge-Trace-Id: $TRACE_ID" \
  -H "X-Merge-Span-Name: draft" \
  -d '{
    "model": "openai/gpt-5.5",
    "input": [
      {"type": "message", "role": "user", "content": "Draft a reply to a customer asking for a refund."}
    ]
  }'
```

**`OpenAI SDK`**

```python title="OpenAI SDK"
import os
import uuid
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["MERGE_GATEWAY_API_KEY"],
    base_url="https://api-gateway.merge.dev/v1/openai",
    default_headers={"X-Merge-Trace-Id": f"run-{uuid.uuid4()}"},
)

draft = client.chat.completions.create(
    model="openai/gpt-5.5",
    messages=[{"role": "user", "content": "Draft a reply to a customer asking for a refund."}],
    extra_headers={"X-Merge-Span-Name": "draft"},
)
```

**`Anthropic SDK`**

```python title="Anthropic SDK"
import os
import uuid
from anthropic import Anthropic

client = Anthropic(
    api_key=os.environ["MERGE_GATEWAY_API_KEY"],
    base_url="https://api-gateway.merge.dev/v1/anthropic",
    default_headers={"X-Merge-Trace-Id": f"run-{uuid.uuid4()}"},
)

draft = client.messages.create(
    model="anthropic/claude-sonnet-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Draft a reply to a customer asking for a refund."}],
    extra_headers={"X-Merge-Span-Name": "draft"},
)
```

A traced request with no parent is a root span. Mint a fresh trace ID per run, since reusing one merges unrelated runs. Untraced requests appear in Logs as usual.

## Nest spans under a parent

Pass a response's `X-Request-ID` header as `X-Merge-Parent-Span-Id` on a later request to nest it:

**`Python`**

```python title="Python"
draft = client.chat.completions.with_raw_response.create(
    model="openai/gpt-5.5",
    messages=[{"role": "user", "content": "Draft a reply to a customer asking for a refund."}],
    extra_headers={"X-Merge-Span-Name": "draft"},
)

critique = client.chat.completions.create(
    model="anthropic/claude-sonnet-5",
    messages=[{"role": "user", "content": f"Critique this draft: {draft.parse().choices[0].message.content}"}],
    extra_headers={
        "X-Merge-Span-Name": "critique",
        "X-Merge-Parent-Span-Id": draft.headers["x-request-id"],
    },
)
```

Or send your own UUID as the parent's `X-Request-ID` and reuse it on the children. Gateway doesn't check that a parent exists, so a span whose parent never arrives sits at the root.

A traced [Fusion](/merge-gateway/capabilities/fusion) request adds its panel calls as child spans `fusion-candidate-1`, `fusion-candidate-2`, and so on, with synthesis as your request's own span.

## Threads and turns

A thread is the conversation a run belongs to, such as a support ticket or chat session. Send one `X-Merge-Thread-Id` for its lifetime and a fresh trace ID per run, so filtering Logs by thread shows the whole conversation. `X-Merge-Turn-Id` groups an agent's requests for one user message. Neither adds tree structure.

## View traces in Logs

Filter [Logs](https://gateway.merge.dev/logs) by **Trace ID**, **Thread ID**, **Turn ID**, **Client trace ID**, or **Span name** (only names you sent). A traced request's detail shows its trace fields and, for multi-request traces, a tree of up to 100 requests with model, timeline bar, and status; click a row to open it. Detail also breaks down Gateway time (routing, security scanning, vendor time to first token) and shows payloads unless **Disable payload logs** is on under **Settings → Organization**. Viewing traces needs **View logs**.

## Tracing from coding agents

When no `X-Merge-Thread-Id` is sent, Gateway turns a coding agent's session header into a **Thread ID**:

| Tool          | Thread grouping                                                                         |
| ------------- | --------------------------------------------------------------------------------------- |
| Claude Code   | Automatic, from `x-claude-code-session-id`                                              |
| Codex         | Automatic, from `session-id`, plus a **Turn ID** per turn from `x-codex-turn-metadata`  |
| OpenCode      | Automatic, from `X-Session-Id`                                                          |
| Pi            | Set `"compat": { "sendSessionAffinityHeaders": true }` on the provider in `models.json` |
| Continue.dev  | Send `X-Merge-Thread-Id` in the model's `requestOptions.headers`                        |
| Factory Droid | Send `X-Merge-Thread-Id` in the custom model's `extraHeaders`                           |
| Cursor        | Not available, because requests come from Cursor's servers                              |

An explicit header always wins. This one groups a developer's Claude Code sessions into one thread:

```bash
export ANTHROPIC_CUSTOM_HEADERS="X-Merge-Thread-Id: $(whoami)-claude-code"
```

Headers set this way are fixed per session, so an `X-Merge-Trace-Id` set at launch makes the session one flat trace. A W3C `traceparent` trace ID is recorded as **Client trace ID**, matching the agent's own tool-call traces.

## Reference

| Header                   | Effect                                                                                                                           | Limit          |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| `X-Merge-Trace-Id`       | Adds the request to a trace. Without it, the request isn't traced.                                                               | 128 characters |
| `X-Merge-Parent-Span-Id` | Nests the request under an earlier request's `X-Request-ID`. Ignored without a trace ID.                                         | 128 characters |
| `X-Merge-Span-Name`      | Labels the span, such as `draft`. Ignored without a trace ID. In the trace panel, unnamed spans show the model name.             | 64 characters  |
| `X-Merge-Thread-Id`      | Groups requests into a conversation. Works without a trace ID.                                                                   | 128 characters |
| `X-Merge-Turn-Id`        | Groups the requests for one agent turn. Works without a trace ID.                                                                | 128 characters |
| `traceparent`            | Records the W3C trace ID as **Client trace ID**                                                                                  | W3C format     |
| `X-Request-ID`           | Sets the request's own ID, which children reference as their parent. A value that isn't a UUID is replaced with a generated one. | UUID           |

Merge header values may use `A-Z a-z 0-9 . _ : -`. An invalid or too-long value is dropped, never truncated, and the request still succeeds; only a dropped `X-Merge-Trace-Id` leaves it untraced.

## Next steps

#### [Telemetry export](/merge-gateway/observability/telemetry-export)

Export spans to Datadog, Grafana Cloud, or your own OTLP endpoint

#### [Fusion](/merge-gateway/capabilities/fusion)

See each panel call as a child span

#### [Roles and permissions](/merge-gateway/security/roles-and-permissions)

Control who can view logs and traces