---
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/developers/conventions"
updated: "2026-11-08"
---

# 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.
- An id is a type prefix and twelve random lowercase letters or digits: `ast_k7m3x9p2w8qr` is an assistant, `res_h4n8c2v6y9tk` a resource. A rename does not change it. A widget has the same `wdg_…` id in the API, the snippets and the hosted address. Treat ids as opaque strings.
- Timestamps are ISO 8601 strings in UTC.
- A JSON body is at most 1 MB. Send a file with the upload call.
- A trailing slash is allowed: `/v1/account/` is `/v1/account`.
- Responses carry `Cache-Control: no-store`.

A success wraps the result:

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

A list also has `has_more`, `next_cursor` and `limit`. Each endpoint page shows the shape of `data`.

## Inputs: forgiving, then strict

- A missing optional field takes its documented default.
- A `limit` above the maximum becomes the maximum. The response gives the limit used.
- An unknown field in a JSON body fails with `unknown_field`, at any depth (for example `rules.exlude`). The message gives the closest valid name: for `followTags`, "Did you mean 'follow\_tags'?".
- Lookio ignores an unknown query parameter, so a tracking parameter from a proxy does not break a call. But a filter with a typo returns an unfiltered result.
- The error gives only the first problem.

## Pagination

Lists of resources use a cursor.

| Parameter | Meaning                                           |
| --------- | ------------------------------------------------- |
| `limit`   | 1 to 100, default 25.                             |
| `cursor`  | The `next_cursor` of the previous page, as it is. |

```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) come whole, in one response.

## Idempotency

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

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

```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. Use a UUID. Make a new key for each request, and use it again only for retries of that request.
- A key belongs to one credential and lives 24 hours.
- The same key on a **different** request (method, path, query or body) fails with `conflict`.
- A retry while the first request runs fails with `conflict`. Wait a few seconds and retry.
- Lookio replays a refusal (4xx) like any response. It does not keep a `5xx` or a `429`, so the next retry runs again.
- Lookio keeps a response only if it is JSON and under 256 KB.
- For a file upload, Lookio compares the name, size and query, not the bytes.

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

The CLI sends a new `Idempotency-Key` with each `POST`, and the same key on its automatic retries.

## Request IDs

Every response has an `X-Request-Id` header, and JSON responses a `request_id` field: `req_` and 32 hex characters. Lookio makes it. You cannot send one.

Log it. It identifies the request without showing your key or your content. Support asks for it.

## Read after write

Every response has an `X-D1-Bookmark` header. Send the last one you got as `X-D1-Bookmark` on your next request. Lookio then reads data at least as new as your last write. Without it, a read just after a write can sometimes come from a copy that is not up to date. Lookio ignores a bookmark that is not valid.

## Asynchronous work

Importing is asynchronous. A new resource is `queued`. Poll [`GET /v1/resources/{resource_id}`](/docs/api/resources/get-resource) until `status` is `ready` or `failed`, then stop. An assistant answers only from `ready` resources.

## Costs and limits

Only [asking an assistant](/docs/api/assistants/query-assistant) and [searching its passages](/docs/api/assistants/search-assistant) (1 credit) spend credits. The response gives its `credit_cost`.

Importing is free of credits, but it has limits. Lookio refuses a call over a limit before it stores anything.

| Limit                 | Value                                                                 | Code when passed                                                                |
| --------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Size of one file      | 5 to 90 MB, by format. See [File formats](/docs/guides/file-formats). | `request_too_large`                                                             |
| Text resource         | 100,000 characters                                                    | `invalid_parameter`                                                             |
| JSON body             | 1 MB                                                                  | `request_too_large`                                                             |
| Pages of one site     | 10,000                                                                | The site stops at the limit and says so. See [Websites](/docs/guides/websites). |
| Words stored          | 125,000 (Free) to 10,000,000 (Pro), per workspace                     | `request_too_large_for_plan`                                                    |
| Pages read each month | 200 (Free) to 15,000 (Pro): PDF pages, slides, images and web pages   | `request_too_large_for_plan`                                                    |

See [Word allowance](/docs/guides/resources#word-allowance) and [Pages read each month](/docs/guides/resources#pages-read-each-month). `GET /v1/account` returns the limits of your plan. The Resources page of the app shows your use.
