# Request Logs

Source: https://aiand-docs-staging.pages.dev/analytics/logs/

```http
GET /logs?range=<range>&after=<ts>&after_id=<id>&errors=<bool>&limit=<n>
```

Returns the full per-request log for your organization, ordered newest-first.

You can also retrieve and inspect the same logs in the
[console](https://console.aiand.com/analytics?tab=logs) under **Analytics → Logs**. Use the console
for interactive troubleshooting and the API for programmatic retrieval or export.

From a terminal, the [ai& CLI](/cli/) reads the same logs: `aiand logs --range 1h --errors` shows
recent failures, and `aiand logs --follow` tails new requests.

## Query parameters

| Parameter        | Type    | Required | Description                                                                                                 |
| ---------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `range`          | string  | No       | `15m`, `1h`, `6h`, `24h`, `7days`, `30days`. Default: `24h`.                                                |
| `after`          | string  | No       | Pagination cursor — `next_after` from the previous page. Requires `after_id`.                               |
| `after_id`       | string  | No       | Pagination cursor — `next_after_id` from the previous page. Requires `after`.                               |
| `errors`         | boolean | No       | If `true`, only non-2xx requests.                                                                           |
| `endpoint`       | string  | No       | Only requests to this `/v1` path, e.g. `/v1/responses`.                                                     |
| `content_logged` | boolean | No       | If `true`, only requests where capture was attempted (see below). The page is then ordered by `request_at`. |
| `limit`          | integer | No       | Page size. Capped at 100. Default: 50.                                                                      |

## Response

```json
{
  "data": [
    {
      "id": "...",
      "model": "...",
      "api_key": "sk-...abcd",
      "status_code": 200,
      "ttft_ms": 210,
      "latency_ms": 420,
      "input_tokens": 123,
      "output_tokens": 45,
      "cached_tokens": 64,
      "cost": "0.00420000",
      "currency": "usd",
      "created_at": "2026-05-18 12:34:56.123456+00",
      "request_at": "2026-05-18 12:34:55.901234+00",
      "endpoint": "/v1/chat/completions",
      "content_logged": true
    }
  ],
  "has_more": true,
  "next_after": "2026-05-18T12:34:55.654321Z",
  "next_after_id": "..."
}
```

`cost` is denominated in `currency` — the org's billing currency (`usd` or `jpy`); it is never a bare USD figure. When `has_more` is `false`, you've reached the end of the available range.

`ttft_ms` is the time to first token: milliseconds from receiving a streamed request to the first generated output you receive, whether text, reasoning or a tool call. A chunk that carries only metadata, such as the opening role announcement, does not count, and neither does output the endpoint does not send you, such as reasoning on `/v1/messages` without `thinking`. It is `null` for a non-streamed request and for a stream that ended before any output.

`cached_tokens` is the portion of `input_tokens` billed at the discounted [cached input rate](/models/pricing/#prompt-caching). It is `0` when there was no cache hit and `null` on models without a cached rate.

`request_at` is when the request was received; `created_at` is when the row was settled, a moment later. Rows are ordered and paged by `created_at`, or by `request_at` when `content_logged=true`; `next_after` always carries the matching value, so pass it back unchanged. `endpoint` is the `/v1` path, and `content_logged` says whether capture was attempted for this request — not that storage necessarily succeeded. A rare miss surfaces as `unavailable` in the detail response, never as `content_logged: false` (see [Content logging](#content-logging)).

## Pagination

Pass `next_after` and `next_after_id` from the previous page as `after` and `after_id`, and stop when `has_more` is `false`. Send both: `after_id` breaks ties between rows that share a timestamp, so either one alone is rejected with `400 invalid_request_error` naming the missing parameter.

`next_after` is an opaque cursor, not an RFC 3339 timestamp: pass it back unchanged and don't parse it.

## Auth

API key or JWT + `X-Org-ID`. See [Authentication](/authentication/).

<Aside type="note">
  Request-log metadata is stored for 365 days, but this endpoint lists at most the last 30 days.
  Rows already older than 30 days cannot be backfilled from this list. Exporting more often than every 30 days keeps the rows the list still returns.
</Aside>

## Retrieve one request (with content)

```http
GET /logs/{id}
```

Returns one request's metadata plus its captured **content** (the request body and the response), when content logging is enabled for your organization (see [Content logging](#content-logging)). Use the `id` from the list, or the `X-Request-ID` header returned on every `/v1` response.

```json
{
  "id": "...",
  "state": "ready",
  "metadata": {
    "model": "...",
    "endpoint": "/v1/chat/completions",
    "api_key": "sk-...abcd",
    "status_code": 200,
    "input_tokens": 123,
    "output_tokens": 45,
    "cost": "0.00420000",
    "currency": "usd",
    "request_at": "2026-05-18 12:34:56.123456+00",
    "created_at": "2026-05-18 12:34:58.000000+00"
  },
  "content": {
    "endpoint": "/v1/chat/completions",
    "streamed": false,
    "capturedAt": "2026-05-18T12:34:56.123Z",
    "responseId": "chatcmpl_...",
    "request": "{ ...the request you sent... }",
    "response": "{ ...the response... }",
    "error": null,
    "truncated": false
  }
}
```

`request` and `response` are stored as **text** — a JSON string for a non-streamed response, and the captured SSE event stream for a streamed one — so you parse them client-side. The stored values retain the endpoint's native shape (`/v1/messages` uses the Anthropic shape, for example), but they are processed for safe storage and are not guaranteed to be byte-for-byte identical to the original wire content. Inline-media placeholders and size truncation are described below.

For streamed responses, inline-media replacement is applied only to JSON payloads in the stored copy's `data:` events. The SSE framing and non-media events remain parseable in their original form, and the live stream delivered to your client is never modified by request logging.

As a general rule, each captured log record is limited to **10 MiB (approximately 10 MB)** across the request, response, and envelope metadata. Content beyond that limit is truncated rather than rejected: `truncated` is `true`, and the stored text includes a truncation marker. Inline base64 media is replaced with an `omitted:<media-type>;bytes=<n>` placeholder before the size limit is applied.

For a failed request, `response` is the error body you received and `error` carries its `message` plus `status` — the upstream status when there was one, otherwise the status you received. The upstream body itself is never stored.

Client-supplied request ids must match `[A-Za-z0-9._:@=+-]{1,36}`. If your `X-Request-ID` is outside that shape, the service replaces it with a generated id; use the `X-Request-ID` returned in the response to retrieve the metadata and content.

### `state`

`content` may be absent; `state` says why, so the response is never an error unless the id is unknown:

| State         | Meaning                                                 |
| ------------- | ------------------------------------------------------- |
| `ready`       | Metadata and content are both present.                  |
| `processing`  | The request just completed; metadata is still settling. |
| `not_logged`  | Content logging was off for this request.               |
| `expired`     | Older than 30 days — the content has been deleted.      |
| `unavailable` | Content was expected but is missing.                    |

A `404` is returned only when neither metadata nor content exists.

## Export stored content

```http
GET /logs/export
```

Organization admins only. Downloads the stored request/response bodies of the logged requests in a window as one gzip-compressed [JSON Lines](https://jsonlines.org/) file, `request-logs.jsonl.gz`, newest first: one line per request, each in the shape of `content` in [Retrieve one request](#retrieve-one-request-with-content). It takes the list's `range`, `errors` and `endpoint` parameters, with the same `24h` default.

One file holds at most the newest 5,000 requests in the window. For older ones, page through the list with `content_logged=true` (see [Pagination](#pagination)) and retrieve each request. A request whose content has already been deleted is left out of the file.

The console offers this export when an admin switches the organization to [zero data retention](/organizations/zero-data-retention/).

## Delete stored content

```http
DELETE /logs
```

Organization admins only. Starts deleting every stored request/response body for your organization and returns `202 { "accepted": true }`; the purge finishes in the background within a few minutes. Metadata rows are kept, and affected requests then open as `unavailable`. Deleting does not change the logging switch: while logging is on, requests made after the purge are captured again. Turning logging off first is not a clean cut either, since capture can continue for up to about a minute after the switch (see [Content logging](#content-logging)).

## Content logging

Capturing request/response bodies is **opt-in and off by default**. Metadata (the list above) is always recorded; request logs store **content** (the actual prompts and responses) only while content logging is on. Other features store content of their own whether or not it is on, such as inline PDFs and video jobs; [Zero data retention](/organizations/zero-data-retention/) lists them.

```http
GET   /logs/settings
PATCH /logs/settings   { "log_content_enabled": true }
```

`GET` returns `{ "log_content_enabled": <bool>, "available": <bool>, "data_retention": "standard" | "zero" }` and reflects a `PATCH` immediately. When `available` is `false`, hide the content-capture control; deletion remains available. `PATCH` (organization admins only) toggles logging when available. An organization on [zero data retention](/organizations/zero-data-retention/) reports `"data_retention": "zero"`, and where capture is available a `PATCH` that switches logging on returns `403 zero_data_retention`.

Organization admins can also turn content logging on or off in the
[console](https://console.aiand.com/settings/organizations) under **Settings → Organizations →
Request Logging**. The switch is shown only when content capture is available in the current
environment; when it is unavailable, the console still allows admins to delete previously stored
content.

<Aside type="caution">
  Changes can take up to about a minute (twice the auth-cache TTL) to affect capture. Turning
  content logging off does not delete existing content — use `DELETE /logs` to purge it, or let it
  expire after 30 days. Bodies are stored in-region and are never written to our internal logs.
</Aside>
