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

# Prepared queries

> Send a filter once, then re-run it by id over different ranges.

Every filter you send is registered on the server as a **template** with a content-addressed
id, and every response returns that id:

* Arrow: `bl.query_id_template` in the schema metadata
* JSON: `query_id_template`

To run the same filter again over another range (the next page of a backfill, the next poll of
the head), send the id instead of the whole body:

```json theme={null}
{"queryId": "0x22df2a6e1f32612c0db47f8e1e893ba9592a529613d0ced777513bb3d1d0dc4a", "fromBlock": 19426300, "toBlock": 19427499}
```

Only `fromBlock`, `toBlock`, `maxObjects` and `strictFields` may accompany a `queryId`. The filters and fields
are exactly what was registered. Mixing `queryId` with `logs`, `transactions` or `fields` is a
`400`.

> [!NOTE]
>
> The [client libraries](https://docs.bloklake.com/clients.md) do this automatically: `paginate` sends the full query once,
> then only its id, and re-sends the query if the id expired. You only need the rest of this page
> if you call the API directly.

## Why bother

* **Smaller requests.** A filter with 10,000 addresses is about 450 KB of JSON. Its id is 66
  characters.
* **Faster pages.** The server caches which rows match a template, per part of the chain, so
  each page of a sliding loop reuses work from the previous one.

## Registering without running

`POST /v1/queries` registers a template and returns its id without reading any data:

```bash theme={null}
curl https://api.bloklake.com/v1/chains/eth/v1/queries \
  -H "Authorization: Bearer $BLOKLAKE_API_KEY" -H 'content-type: application/json' \
  -d '{"logs": [{"topic0": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]}], "fields": {"log": {"address": true}}}'
```

```json theme={null}
{"query_id": "0xfee8ff0ddc7103d2f35bbc5bc6c6c90253ed493b1247c12fc47b425163b6a0a0", "expires_in": 1800}
```

## Expiry: always handle `unknown_query_id`

Templates expire **30 minutes after their last use** (`expires_in` is the current value) and
live on one chain's server. When the id is unknown (it expired, or the request went to another
chain), the server answers `400 unknown_query_id`. **Resend the full filter.** That registers it
again under the same id, and the loop continues:

```python theme={null}
template_id = None
frm = start
while frm <= end:
    page = {"queryId": template_id} if template_id else dict(body)
    page.update(fromBlock=frm, toBlock=end)
    try:
        tables, meta = query(page)
    except QueryError as e:
        if e.code == "unknown_query_id":   # expired: send the full filter again
            template_id = None
            continue
        raise
    template_id = meta["bl.query_id_template"]
    frm = int(meta["bl.next_block"])
```

> [!NOTE]
>
> Because the id is a hash of the filter, the same filter always gets the same id, on every chain
> and in every session. You can cache ids, but never rely on one still being registered.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.