Skip to main content
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:
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.
The client libraries 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:

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:
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.