> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bitfab.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTP Endpoints

> SDK-facing HTTP endpoints. Every SDK speaks this wire protocol.

All SDKs POST to the same set of endpoints under `{serviceUrl}/api/sdk/`. This page documents the wire protocol so you can integrate directly without using an SDK, or debug SDK behavior.

Base URL defaults to `https://bitfab.ai`.

## Authentication

All endpoints require the API key in the `Authorization` header:

```
Authorization: Bearer {apiKey}
```

Missing, empty, or invalid keys return `401 Unauthorized`.

## Common Request Fields

The SDKs append these fields to most request bodies:

| Field        | Type     | Description                                           |
| ------------ | -------- | ----------------------------------------------------- |
| `sdkVersion` | `string` | The SDK package version                               |
| `source`     | `string` | Origin tag - `"typescript-sdk"`, `"python-sdk"`, etc. |

## Endpoints

### `POST /api/sdk/functions/lookup`

Look up a function by name. Blocking.

**Request:**

```json theme={null}
{ "name": "my-function-key" }
```

**Response 200:**

```json theme={null}
{
  "id": "uuid",
  "name": "my-function-key",
  "versionId": "uuid",
  "versionNumber": 3,
  "prompt": "...",
  "providers": [ /* ProviderDefinition[] */ ]
}
```

**Response (not found):** `{ "id": null }` - SDKs raise a typed error pointing users to `/functions`.

### `POST /api/sdk/functions/{functionId}/traces`

Record a trace produced by a server-side function execution (e.g. `client.call(...)`). Fire-and-forget.

**Request:**

```json theme={null}
{
  "result": "...",
  "source": "typescript-sdk",
  "inputs": { "...": "..." },
  "rawCollector": { /* BAML collector snapshot, optional */ },
  "sdkVersion": "..."
}
```

**Response:** `200 OK`.

### `POST /api/sdk/externalSpans`

Record a single span produced by `withSpan` / `@span` / `Span` / `bitfab_span`. Fire-and-forget.

**Request:**

```json theme={null}
{
  "id": "canonical Bitfab span UUID",
  "traceId": "canonical Bitfab trace UUID",
  "type": "sdk-function",
  "source": "typescript-sdk-function",
  "sourceTraceId": "uuid",
  "traceFunctionKey": "my-function-key",
  "rawSpan": {
    "id": "uuid",
    "trace_id": "uuid",
    "parent_id": "uuid | null",
    "started_at": "ISO8601",
    "ended_at": "ISO8601",
    "input_source_span_id": "uuid | null",
    "span_data": {
      "name": "...",
      "type": "llm|agent|function|guardrail|handoff|custom",
      "input": {/* json */},
      "output": {/* json */},
      "input_meta": {/* superjson meta */},
      "output_meta": {/* superjson meta */},
      "function_name": "...",
      "error": "...",
      "contexts": [{}],
      "prompt": "..."
    }
  },
  "testRunId": "uuid (optional, set during replay)",
  "sdkVersion": "..."
}
```

**Response:** `200 OK`.

### `POST /api/sdk/externalTraces`

Record the completion of a root span's trace. Sent once per trace when the outermost span ends. Fire-and-forget.

The request's top-level `id` is the canonical Bitfab trace UUID. The nested raw trace ID remains an ingestion correlation value.

### `GET /api/sdk/traces/{traceId}/span`

Fetch one persisted span without loading the full trace. `traceId` and the optional exact `id` selector are canonical Bitfab UUIDs. Select exactly one of:

* `id={canonicalSpanId}` for an exact match
* `name={spanName}` with optional `occurrence=first|last|{zeroBasedIndex}`; the default is `last`

The response includes the span's canonical `id`, `traceId`, and `parentSpanId`, plus its name, type, input, output, contexts, prompt, metadata, metrics, errors, and timestamps. It never exposes ingestion source IDs.

### `PATCH /api/sdk/traces/{traceId}`

Update a detached current trace by its canonical Bitfab ID. The body accepts `appendContexts`, `mergeMetadata`, and `setSessionId`.

**Request:**

```json theme={null}
{
  "type": "sdk-function",
  "source": "typescript-sdk-function",
  "traceFunctionKey": "my-function-key",
  "externalTrace": {
    "id": "uuid",
    "started_at": "ISO8601",
    "ended_at": "ISO8601",
    "workflow_name": "my-function-key",
    "metadata": {/* optional */},
    "contexts": [{/* optional */}]
  },
  "completed": true,
  "sessionId": "... (optional)",
  "sdkVersion": "..."
}
```

### `POST /api/sdk/replay/start`

Begin a replay session. Blocking. Timeout: 30 s on the client side.

**Request:**

```json theme={null}
{
  "traceFunctionKey": "my-function-key",
  "name": "Prompt candidate",
  "limit": 10,
  "traceIds": ["uuid", "..."],
  "datasetId": "uuid",
  "graderIds": ["uuid", "..."]
}
```

`name` is an optional display name stored on the resulting experiment/test run. `limit` (1-5,000, default 5) caps how many recent traces are fetched. When `traceIds` is present (max 100 entries) the ID list determines the count and `limit` is ignored; the field stays accepted because older SDKs always send a defaulted value. When `datasetId` is present, the dataset's full trace list determines the count and `limit` is ignored. The dataset is validated against the organization; a foreign ID returns a 400. `graderIds` (max 100) attaches graders directly to this run, independent of the dataset's own graders; the resulting experiment is graded by the union of these and the dataset's runnable graders at completion. Each id must be an active grader in the same organization and trace function, or the replay returns a 400. A replay with no dataset can still carry graders this way.

**Response 200:**

```json theme={null}
{
  "testRunId": "uuid",
  "testRunUrl": "/trace-functions/my-function-key/traces?testRunId=uuid",
  "items": [
    {
      "originalTraceId": "uuid",
      "originalSpanId": "uuid",
      "sourceTraceId": "uuid (deprecated alias for originalTraceId)",
      "sourceSpanId": "uuid (deprecated alias for originalSpanId)",
      "durationMs": 1750,
      "tokens": {
        "input": 80,
        "output": 20,
        "cached": 5,
        "total": 100
      },
      "model": "claude-sonnet-4-5"
    }
  ]
}
```

Each `items[]` entry carries reference metrics from the original trace being replayed: `durationMs` is the end-to-end wall time (integer milliseconds), `tokens` is an object with `input`/`output`/`cached`/`total` counts, and `model` is the model id. Any field may be `null` when the underlying trace didn't capture it. The replayed run's own token usage is returned later by `/api/sdk/replay/complete` (see below), once its spans are persisted.

### `POST /api/sdk/replay/complete`

Signal that a replay session finished.

**Request:**

```json theme={null}
{ "testRunId": "uuid" }
```

**Response:**

```json theme={null}
{
  "id": "uuid",
  "status": "completed",
  "traceIds": { "<sdk-trace-id>": "<server-trace-id>" },
  "tokens": {
    "<server-trace-id>": { "input": 80, "output": 20, "cached": 5, "total": 100 }
  },
  "traceCount": 1
}
```

`traceIds` maps each SDK-generated replay trace id to the persisted server trace id. `tokens` is each replay trace's token usage, keyed by server trace id and aggregated from the freshly-uploaded replay spans (the same source the experiments view reads), so it's the replayed run's cost rather than the original's; a trace with no token data maps to `null`. `traceCount` is how many traces the server persisted for the run. The SDK maps `tokens` onto each item to populate its replay `tokens`.

### `GET /api/sdk/externalSpans/{id}`

Fetch a specific external span by ID. Used by replay internals. Returns the `rawSpan` object.

## Serialization

* Inputs and outputs in `span_data.input` / `span_data.output` are serialized using [superjson](https://github.com/flightcontrolhq/superjson) by the TypeScript SDK. The `input_meta` / `output_meta` fields are the superjson meta descriptors.
* Python, Ruby, and Go SDKs use JSON-compatible fallbacks. Objects that don't round-trip through JSON are coerced to string via the SDK's `serialize` helper.

## Error Responses

| Status | Meaning                                                   |
| ------ | --------------------------------------------------------- |
| `200`  | Success                                                   |
| `400`  | Malformed request body                                    |
| `401`  | Missing/invalid API key                                   |
| `403`  | API key valid but lacks access to the referenced resource |
| `404`  | Function / span / trace ID not found                      |
| `429`  | Rate limit exceeded                                       |
| `5xx`  | Server error                                              |

SDKs swallow all non-2xx responses on fire-and-forget endpoints and log to stderr. Blocking endpoints (`lookup`, `replay/start`) raise typed errors.
