---
title: "Conventions"
description: "How requests and responses are shaped in the Lookio API, with pagination, idempotency keys, request IDs, read-after-write and the forgiving and strict input rules."
canonical: "https://lookio.app/docs/api/conventions"
updated: "2026-11-03"
---

# Conventions

## Requests and responses

- Send JSON with `Content-Type: application/json`. A body without the header is accepted if it parses.
- Fields are `snake_case`, in requests and responses.
- Object IDs have a type prefix and twelve random lowercase letters or digits, such as `ast_k7m3x9p2w8qr` for an assistant or `res_h4n8c2v6y9tk` for a resource. They stay unchanged when renamed. Widgets use the same `wdg_…` id in the API, embed snippets and hosted URLs. Treat IDs as opaque strings. Timestamps are ISO 8601 strings in UTC.
- A JSON body is limited to 1 MB. A file goes in a separate upload call.
- A trailing slash is tolerated: `/v1/account/` and `/v1/account` are the same endpoint.
- Responses carry `Cache-Control: no-store`.

A success wraps the result:

```json
{
  "data": { },
  "request_id": "req_0123456789abcdef0123456789abcdef"
}
```

A list adds `has_more`, `next_cursor` and `limit` next to `data`. Each endpoint page shows the exact shape of `data`.

## Inputs: forgiving, then strict

- A missing optional field takes its documented default.
- A `limit` above the maximum is clamped, and the response reports the limit actually applied.
- An unknown field in a JSON body fails with `unknown_field`, at any depth (a field inside `rules` is named `rules.exlude`). The message suggests the closest valid name: sending `followTags` answers "Did you mean 'follow\_tags'?".
- An unknown query parameter is ignored, so a tracking parameter appended by a proxy does not break a call. A mistyped filter therefore returns an unfiltered result.
- Only the first problem is reported, so you fix one thing at a time.

## Pagination

Lists of resources use a cursor.

| Parameter | Meaning                                                        |
| --------- | -------------------------------------------------------------- |
| `limit`   | 1 to 100, default 25.                                          |
| `cursor`  | The `next_cursor` of the previous page, passed back unchanged. |

```bash
curl "https://api.lookio.app/v1/resources?limit=50" \
  -H "Authorization: Bearer $LOOKIO_API_KEY"
```

Results come newest first. While `has_more` is `true`, call again with `cursor` set to `next_cursor`. A cursor that matches nothing fails with `invalid_cursor`.

Other lists (assistants, tags, members) are returned whole, in one response.

## Idempotency

Networks fail, and automation tools retry. Without protection, a retried import creates a duplicate resource and a retried question is charged twice.

Add an `Idempotency-Key` header to a `POST`. Lookio runs the request once, and a repeat with the same key returns the first response with an `Idempotent-Replayed: true` header.

```bash
curl https://api.lookio.app/v1/assistants/ASSISTANT_ID/query \
  -H "Authorization: Bearer $LOOKIO_API_KEY" \
  -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
  -H "Content-Type: application/json" \
  -d '{ "question": "How long do I have to return an item?" }'
```

The rules:

- The key is 1 to 255 visible ASCII characters. A UUID is a good choice. Generate a new one for each distinct request and reuse it only for retries of that request.
- Keys belong to one credential and live 24 hours.
- The same key on a **different** request (method, path, query or body) fails with `conflict`.
- While the first request is still running, a retry fails with `conflict`. Wait a few seconds and retry.
- A refusal (4xx) is an answer and is replayed as such. A `5xx` or a `429` is not kept and frees the key, so a retry really retries.
- A response is kept only if it is JSON and under 256 KB.
- A file upload is fingerprinted by its name, size and query, not by its bytes.

> **Warning: Streamed answers are not replayed**
>
> A streamed answer (`stream: true`) is not kept, so a retry does not get the first answer back. Do not retry a stream blindly. To retry safely, use a JSON answer.

The CLI sends a fresh `Idempotency-Key` on every `POST` and reuses it across its automatic retries.

## Request IDs

Every response has an `X-Request-Id` header and, for JSON, a `request_id` field. It looks like `req_` followed by 32 hex characters. Lookio generates it. You cannot supply one.

Log it. It identifies the request on Lookio's side without exposing your key or your content, and it is what support asks for.

## Read after write

An import returns before it is read, and a resource you create can be asked for right away. Every response carries an `X-D1-Bookmark` header. Send the last one you received back as `X-D1-Bookmark` on your next request, and Lookio reads at least as fresh as your last write. Without it, a read made immediately after a write can occasionally be served from a replica that has not caught up. A malformed value is ignored.

## Asynchronous work

Importing is asynchronous. A resource comes back as `queued`, and you poll [`GET /v1/resources/{resource_id}`](/docs/api/resources/get-resource) until `status` is `ready` or `failed`. Both are final: do not poll past them. An assistant cannot answer from a resource that is not `ready`.

## Costs

Only [asking an assistant](/docs/api/assistants/query-assistant) spends credits. Every endpoint page says so when it matters, and the answer reports `credit_cost`.
