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

# Live request-log tail

> Stream your tenant's API request log in real time over Server-Sent Events.

# Live request-log tail

`GET /api/v1/logs/tail` opens a Server-Sent Events (SSE) connection that streams every API request handled for your tenant, sub-second after each response completes. It is the data source behind the **Live tail** toggle on the developer Request Logs page, and you can consume it directly from your own tooling.

The stream excludes health, readiness, and metrics probes, as well as the tail endpoint itself.

**Base path:** `/api/v1/logs/tail`

**Authentication:** API key (`X-API-Key`) or session JWT. The caller must hold the `owner`, `admin`, or `developer` role.

## Connect

```bash theme={null}
curl -N "https://api.orbit.devotel.io/api/v1/logs/tail?token=dv_live_sk_your_key_here"
```

Browsers use `EventSource`, which cannot set request headers, so pass your credential as the `token` query parameter instead. A `dv_`-prefixed value is promoted to `X-API-Key`; any other value is treated as a bearer JWT. When you call from a server you can send the `X-API-Key` or `Authorization` header directly and omit `token`.

```js theme={null}
const source = new EventSource(
  `https://api.orbit.devotel.io/api/v1/logs/tail?token=${jwt}`,
  { withCredentials: true },
);

source.addEventListener("log", (event) => {
  const row = JSON.parse(event.data);
  console.log(row.method, row.path_pattern, row.status_code, row.duration_ms);
});
```

## Frames

After connecting, the server sends, in order:

1. An `event: connected` handshake frame with `data: {"type":"connected", ...}`.
2. A backfill of up to the most recent \~100 request rows.
3. New rows live, as each request completes.

Comment lines (`: heartbeat`) are sent every 30 seconds to keep the connection open. Each request row arrives as a named `log` frame — listen with `addEventListener("log", …)`, not `onmessage`, because `EventSource` only routes named frames to a matching listener.

Each `log` frame's `data:` line is one JSON object:

```json theme={null}
{
  "type": "log",
  "ts": 1718971200123,
  "request_id": "req_7f3c2a",
  "method": "POST",
  "path_pattern": "/api/v1/messages",
  "status_code": 202,
  "duration_ms": 41,
  "user_id": "user_abc",
  "api_key_id": null,
  "error_code": null,
  "error_message": null,
  "user_agent": "MyApp/1.4"
}
```

| Field           | Type           | Notes                                                                      |
| --------------- | -------------- | -------------------------------------------------------------------------- |
| `type`          | string         | Always `log`.                                                              |
| `ts`            | integer        | Unix epoch milliseconds when the response completed.                       |
| `request_id`    | string         | Per-request id, also returned as the `X-Request-Id` header.                |
| `method`        | string         | HTTP method.                                                               |
| `path_pattern`  | string         | Normalized route with ids collapsed to `:id`, e.g. `/api/v1/messages/:id`. |
| `status_code`   | integer        | HTTP status code.                                                          |
| `duration_ms`   | integer        | Server-side handling time in milliseconds.                                 |
| `user_id`       | string \| null | Authenticated user, or `null` for API-key requests.                        |
| `api_key_id`    | string \| null | API key that authenticated the request, or `null`.                         |
| `error_code`    | string \| null | Machine-readable error code for `>= 400` responses, otherwise `null`.      |
| `error_message` | string \| null | Error message for `>= 400` responses (truncated), otherwise `null`.        |
| `user_agent`    | string \| null | Request `User-Agent` (truncated), or `null`.                               |

Adding fields is backwards-compatible — ignore keys you don't recognize, and drop any row missing a required field.

## Resume after a disconnect

Every frame carries an `id` (a stream cursor). To resume without losing rows, reconnect with the `Last-Event-ID` request header set to the last id you received — browser `EventSource` does this automatically. From a client that can't set the header, pass the same value as the `lastEventId` query parameter:

```bash theme={null}
curl -N "https://api.orbit.devotel.io/api/v1/logs/tail?token=dv_live_sk_your_key_here&lastEventId=1718971200123-0"
```

On resume the server replays the rows newer than that cursor before continuing the live tail.

## Limits and errors

| Status | Meaning                                                                                                       |
| ------ | ------------------------------------------------------------------------------------------------------------- |
| `403`  | Caller lacks the `owner`/`admin`/`developer` role, or the request did not come from a trusted browser origin. |
| `429`  | Tenant has reached the maximum of 10 concurrent tail connections. Close an existing connection and retry.     |
| `503`  | The real-time backing store is temporarily unavailable. Retry with backoff.                                   |

This is a live tail, not durable history. It carries only recent rows and the live stream — use it for observability, not as a system of record.
