# Export a thread transcript

Download a portable transcript of one thread at a chosen detail level. `messages` returns the conversation only; `activity` adds one human-readable line per tool call (no raw payloads); `full` adds redacted tool inputs and outputs, nested subagent transcripts, compaction summaries, model switches, and failures. JSON responses follow the versioned `TranscriptExport` schema; Markdown responses render the same document for people. Responses stream and are served as attachments. This endpoint has a dedicated per-caller budget of 30 exports per minute on top of the ingress rate limit.

`GET /api/threads/{id}/export`

Required permission: `thread:read`.

## Parameters

| Name | In | Type | Required | Description |

| --- | --- | --- | --- | --- |

| `id` | path | `string` | Yes | The Hoplite thread ID. |

| `level` | query | `string` | No | Detail level. Defaults to `messages`. |

| `format` | query | `string` | No | Response format. Defaults to `markdown`. |

| `subagents` | query | `boolean` | No | Whether `full` exports nest each subagent's own transcript. Ignored at other levels. Defaults to `true`. |

## Example request

```bash
curl --request GET \
  --url https://api.hoplite.sh/api/threads/thr_01JQ7B/export?format=json \
  --header "X-Api-Key: $HOPLITE_API_KEY"
```

## 200 response

The transcript, as JSON (`format=json`) or Markdown (`format=markdown`), delivered with a `Content-Disposition: attachment` header.

Content type: `application/json`. Response type: `TranscriptExport`.

| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `schemaVersion` | `integer` | Yes | constant 1 | Stable document version. Incompatible changes increment it; additive fields do not. |
| `exportedAt` | `string<date-time>` | Yes | — | — |
| `level` | `string` | Yes | messages · activity · full | — |
| `includeSubagents` | `boolean` | Yes | — | — |
| `thread` | `object` | Yes | — | — |
| `thread.id` | `string` | Yes | — | — |
| `thread.title` | `string` | Yes | — | — |
| `thread.createdAt` | `string<date-time>` | No | — | — |
| `runs` | `TranscriptRun[]` | Yes | — | — |
| `runs.id` | `string` | Yes | — | Run id, or `unattached` for messages that never joined a run. |
| `runs.status` | `string` | Yes | — | — |
| `runs.createdAt` | `string<date-time>` | Yes | — | — |
| `runs.finishedAt` | `string<date-time>` | No | — | — |
| `runs.entries` | `TranscriptEntry[]` | Yes | — | — |
| `runs.entries.Option 1` | `TranscriptMessageEntry` | No | — | — |
| `runs.entries.Option 1.type` | `string` | Yes | constant "message" | — |
| `runs.entries.Option 1.id` | `string` | Yes | — | — |
| `runs.entries.Option 1.role` | `string` | Yes | user · assistant | — |
| `runs.entries.Option 1.author` | `string` | Yes | — | — |
| `runs.entries.Option 1.createdAt` | `string<date-time>` | Yes | — | — |
| `runs.entries.Option 1.content` | `string` | Yes | — | — |
| `runs.entries.Option 1.attachments` | `TranscriptAttachment[]` | No | — | — |
| `runs.entries.Option 1.attachments.name` | `string` | Yes | — | — |
| `runs.entries.Option 1.attachments.contentType` | `string` | Yes | — | — |
| `runs.entries.Option 1.attachments.url` | `string<uri>` | No | — | Signed public URL, present only when the organization allows public media. Expires at urlExpiresAt. |
| `runs.entries.Option 1.attachments.urlExpiresAt` | `string<date-time>` | No | — | — |
| `runs.entries.Option 2` | `TranscriptToolCallEntry` | No | — | — |
| `runs.entries.Option 2.type` | `string` | Yes | constant "tool_call" | — |
| `runs.entries.Option 2.id` | `string` | Yes | — | — |
| `runs.entries.Option 2.createdAt` | `string<date-time>` | Yes | — | — |
| `runs.entries.Option 2.completedAt` | `string<date-time>` | No | — | — |
| `runs.entries.Option 2.toolName` | `string` | Yes | — | — |
| `runs.entries.Option 2.label` | `string` | Yes | — | Human-readable one-liner matching the thread timeline, for example `Ran `pnpm test` (exit 0)`. |
| `runs.entries.Option 2.status` | `string` | Yes | running · completed · failed · rejected | — |
| `runs.entries.Option 2.input` | `string` | No | — | Redacted tool input; only present at the full level. |
| `runs.entries.Option 2.output` | `string` | No | — | Redacted tool output; only present at the full level. |
| `runs.entries.Option 2.error` | `string` | No | — | — |
| `runs.entries.Option 3` | `TranscriptThinkingEntry` | No | — | — |
| `runs.entries.Option 3.type` | `string` | Yes | constant "thinking" | — |
| `runs.entries.Option 3.id` | `string` | Yes | — | — |
| `runs.entries.Option 3.createdAt` | `string<date-time>` | Yes | — | — |
| `runs.entries.Option 3.content` | `string` | Yes | — | — |
| `runs.entries.Option 4` | `TranscriptCompactionEntry` | No | — | — |
| `runs.entries.Option 4.type` | `string` | Yes | constant "compaction" | — |
| `runs.entries.Option 4.id` | `string` | Yes | — | — |
| `runs.entries.Option 4.createdAt` | `string<date-time>` | Yes | — | — |
| `runs.entries.Option 4.status` | `string` | Yes | completed · failed | — |
| `runs.entries.Option 4.summary` | `string` | Yes | — | Model-context summary, explicitly marked so it is never mistaken for an answer. |
| `runs.entries.Option 5` | `TranscriptModelEntry` | No | — | — |
| `runs.entries.Option 5.type` | `string` | Yes | constant "model" | — |
| `runs.entries.Option 5.id` | `string` | Yes | — | — |
| `runs.entries.Option 5.createdAt` | `string<date-time>` | Yes | — | — |
| `runs.entries.Option 5.modelId` | `string` | Yes | — | — |
| `runs.entries.Option 5.provider` | `string` | No | — | — |
| `runs.entries.Option 5.requestedModelId` | `string` | No | — | Present when the run resolved a different model than requested. |
| `runs.entries.Option 6` | `TranscriptFailureEntry` | No | — | — |
| `runs.entries.Option 6.type` | `string` | Yes | constant "failure" | — |
| `runs.entries.Option 6.id` | `string` | Yes | — | — |
| `runs.entries.Option 6.createdAt` | `string<date-time>` | Yes | — | — |
| `runs.entries.Option 6.code` | `string` | Yes | — | — |
| `runs.entries.Option 6.message` | `string` | Yes | — | — |
| `runs.entries.Option 6.detail` | `string` | No | — | — |
| `runs.entries.Option 7` | `TranscriptSubagentEntry` | No | — | — |
| `runs.entries.Option 7.type` | `string` | Yes | constant "subagent" | — |
| `runs.entries.Option 7.id` | `string` | Yes | — | — |
| `runs.entries.Option 7.createdAt` | `string<date-time>` | Yes | — | — |
| `runs.entries.Option 7.completedAt` | `string<date-time>` | No | — | — |
| `runs.entries.Option 7.name` | `string` | Yes | — | — |
| `runs.entries.Option 7.status` | `string` | Yes | running · completed · failed · interrupted | — |
| `runs.entries.Option 7.prompt` | `string` | No | — | — |
| `runs.entries.Option 7.summary` | `string` | No | — | — |
| `runs.entries.Option 7.entries` | `TranscriptEntry[]` | Yes | — | Nested child transcript; populated only at the full level when subagents are included. |
| `runs.entries.Option 7.entries.Recursive value` | `TranscriptEntry` | No | — | Recursive schema; see /docs/openapi.json. |

```json
{
  "schemaVersion": 1,
  "exportedAt": "2026-08-10T08:10:00.000Z",
  "level": "activity",
  "includeSubagents": true,
  "thread": {
    "id": "thr_01JQ7B",
    "title": "Add checkout analytics",
    "createdAt": "2026-08-10T08:00:00.000Z"
  },
  "runs": [
    {
      "id": "run_01JQ7C",
      "status": "completed",
      "createdAt": "2026-08-10T08:00:01.000Z",
      "finishedAt": "2026-08-10T08:04:00.000Z",
      "entries": [
        {
          "type": "message",
          "id": "msg_01JQ7C",
          "role": "user",
          "author": "Ada",
          "createdAt": "2026-08-10T08:00:01.000Z",
          "content": "Add a checkout analytics event."
        },
        {
          "type": "tool_call",
          "id": "call_01JQ7C",
          "createdAt": "2026-08-10T08:01:00.000Z",
          "completedAt": "2026-08-10T08:01:04.000Z",
          "toolName": "shell",
          "label": "Ran `pnpm test` (exit 0)",
          "status": "completed"
        },
        {
          "type": "message",
          "id": "msg_01JQ7D",
          "role": "assistant",
          "author": "Hoplite",
          "createdAt": "2026-08-10T08:04:00.000Z",
          "content": "Implemented the checkout analytics event and added tests."
        }
      ]
    }
  ]
}
```

## Response headers

| Header | Type | Statuses | Description |

| --- | --- | --- | --- |

| `RateLimit` | `string` | `200`, `400`, `401`, `402`, `403`, `404`, `429`, `500`, `501`, `503` | Current ingress policy, remaining requests, and seconds until reset, for example `"ingress";r=599;t=60`. |

| `RateLimit-Policy` | `string` | `200`, `400`, `401`, `402`, `403`, `404`, `429`, `500`, `501`, `503` | Ingress quota and window, for example `"ingress";q=600;qu="requests";w=60`. |

| `X-RateLimit-Limit` | `integer` | `200`, `400`, `401`, `402`, `403`, `404`, `429`, `500`, `501`, `503` | Maximum requests allowed in the current rolling window. |

| `X-RateLimit-Remaining` | `integer` | `200`, `400`, `401`, `402`, `403`, `404`, `429`, `500`, `501`, `503` | Requests remaining in the current rolling window. |

| `X-RateLimit-Reset` | `integer` | `200`, `400`, `401`, `402`, `403`, `404`, `429`, `500`, `501`, `503` | Unix timestamp when the current rolling window resets. |

| `x-request-id` | `string` | `200`, `400`, `401`, `402`, `403`, `404`, `429`, `500`, `501`, `503` | Request correlation ID. |

| `Content-Disposition` | `string` | `200` | Attachment filename derived from the thread title, for example `attachment; filename="add-checkout-analytics.md"`. |

| `Retry-After` | `integer` | `429`, `503` | Seconds until another request should be attempted. |

| `X-Retry-After` | `integer` | `429`, `503` | Legacy retry delay in seconds. |

## Status codes

- `200` — The transcript, as JSON (`format=json`) or Markdown (`format=markdown`), delivered with a `Content-Disposition: attachment` header.

- `400` — The level, format, or subagents option is not recognized.

- `401` — The API key is missing, invalid, or lacks the operation's permission.

- `402` — The organization does not have access to the requested product capability.

- `403` — The key's current user no longer has the required organization membership or role.

- `404` — The thread was not found.

- `429` — The API-key rate limit has been exceeded.

- `500` — The API encountered an unexpected failure.

- `501` — The deployment has not configured the requested capability.

- `503` — A required API dependency, including the rate limiter, is unavailable.