Submit a query
curl -X POST 'https://api.justcrawl.io/api/v1/bi/queries' \ -H 'Authorization: Bearer $JUSTCRAWL_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"sql":"SELECT status, count(*) FROM jobs GROUP BY 1","label":"status mix"}'import os, requestsr = requests.post( 'https://api.justcrawl.io/api/v1/bi/queries', headers={'Authorization': f'Bearer {os.environ["JUSTCRAWL_API_KEY"]}'}, json={'sql': 'SELECT status, count(*) FROM jobs GROUP BY 1'},)r.raise_for_status()body = r.json()# 200 != success — branch on status, then poll if it is still running.if body['status'] == 'success' and 'results' in body: print(body['results']['rows'])Submit either read-only SQL or one structured aggregate definition against your bi
schema. The two input forms are mutually exclusive. The call waits briefly for the query to
finish: if it completes inside that window the first page of rows comes back in the same
response under results; otherwise you get { jobId, status } and poll
GET /api/v1/bi/queries/{id}.
A 200 does not mean the query succeeded. A query that failed fast returns 200 with
status: failed and an error object describing the query’s failure — that error is
query detail, not an HTTP error envelope, and carries no docs_url or request_id.
Branch on status, not on the HTTP code.
dryRun is the pre-submit EXPLAIN estimate. It is informational: nothing is rejected
for size, and only the engine’s own time limit is enforced.
Send a stable UUID in Idempotency-Key when retrying an indeterminate submission. An
exact replay returns the original query and does not enqueue it again; reusing the key
for a different request returns idempotency_conflict.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”Stable UUID for replay-safe submission.
Request Bodyrequired
Section titled “Request Bodyrequired”object
The query. Read-only; max 100000 characters.
Optional human label shown in query history. Max 200 characters.
Owned history execution whose immutable scope this SQL edit retains.
object
Versioned aggregate intent. Column and table names are validated against the caller-authorized BI catalog.
object
object
Defaults to 100 when omitted.
Optional human label shown in query history. Max 200 characters.
Responses
Section titled “Responses”Query accepted. results is present only when the query completed inside the
sync-wait window; error is present only when it failed inside that window.
object
Informational EXPLAIN estimate from the Postgres dry-run planning pass. All three fields are present together when the estimate succeeded, and all three are absent (an empty object) when no estimate exists — the CH-force kill switch skips the dry-run entirely, or it timed out before producing a plan. There is no valid flag on the wire; the caller cannot distinguish “skipped” from “timed out” from this field alone.
object
Display-only rendered SQL for structured submissions. Never accepted as input.
One page of result rows. Returned bare by GET /api/v1/bi/queries/{id}/results, but nested under results by POST /api/v1/bi/queries when the sync-wait window catches a completed query. The two are not interchangeable.
object
object
Total rows materialized for this query. -1 when unknown.
Present only when a structured aggregate left out readings older than the typed-column history backfill. Those readings have no typed values, so measures and typed filters could not include them. Absent when the answer covers every matching reading.
object
Readings created before this instant may be missing from the result.
Matching source readings excluded for lack of typed history.
The query’s own failure detail — not an HTTP error envelope.
object
Example
{ "status": "queued", "scopeKind": "org", "error": { "code": "syntax_error" }}Body/header rejected (invalid_query_input, invalid_definition, invalid_sql, invalid_label, invalid_idempotency_key, invalid_execution_context) or SQL failed validation (syntax_error)
object
object
Stable, machine-readable error code. Match on this, not on message.
Customer-safe description. Never raw exception or driver text, with one deliberate exception: a syntax_error (SQLSTATE 42601) passes the database’s own message through verbatim, because it describes SQL the caller wrote themselves and names nothing they could not already see — see the root CLAUDE.md §Error reporting for the full carve-out.
Reference page for this code. Codes shared with the rest of the API have their own page; the BI-local vocabulary points at the single BI errors page that tabulates all of them.
Correlation id. Identical to the X-Request-ID response header — quote either one to support.
object
Example
{ "error": { "code": "invalid_query_input" }}Missing or invalid authentication token
object
Example
{ "error": "Missing or invalid authentication token"}Access to the BI surface itself is refused. no_org and feature_disabled fire on every BI route from the mount guard, before any handler runs — feature_disabled is the first response most new integrations see, because the SQL console is entitled per organization. Permission and verified-email gates use insufficient_permissions and email_not_verified; rls_denied and kill_switched come from the engine. This response never describes resource-level authorization: an id you do not own returns 404, not 403.
object
object
Stable, machine-readable error code. Match on this, not on message.
Customer-safe description. Never raw exception or driver text, with one deliberate exception: a syntax_error (SQLSTATE 42601) passes the database’s own message through verbatim, because it describes SQL the caller wrote themselves and names nothing they could not already see — see the root CLAUDE.md §Error reporting for the full carve-out.
Reference page for this code. Codes shared with the rest of the API have their own page; the BI-local vocabulary points at the single BI errors page that tabulates all of them.
Correlation id. Identical to the X-Request-ID response header — quote either one to support.
object
Example
{ "error": { "code": "feature_disabled", "message": "SQL console is not enabled for this organization", "docs_url": "https://docs.justcrawl.io/guides/errors/bi", "request_id": "req_9f3a1c7e2b40" }}Not found, or not owned by your organization. The two are deliberately indistinguishable so cross-tenant existence cannot be probed
Error envelope returned by every /api/v1/bi/* handler error. Unlike the rest of the API — where the structured shape is opt-in via the application/vnd.justcrawl.v1+json media type — a BI handler always emits it, and all four fields are always present. The one exception: 401s from missing or invalid auth are rejected by shared middleware before any BI handler runs, so they carry the flat { "error": "<message>" } shape (Error schema) instead — see the Unauthorized response on each operation.
object
object
Stable, machine-readable error code. Match on this, not on message.
Customer-safe description. Never raw exception or driver text, with one deliberate exception: a syntax_error (SQLSTATE 42601) passes the database’s own message through verbatim, because it describes SQL the caller wrote themselves and names nothing they could not already see — see the root CLAUDE.md §Error reporting for the full carve-out.
Reference page for this code. Codes shared with the rest of the API have their own page; the BI-local vocabulary points at the single BI errors page that tabulates all of them.
Correlation id. Identical to the X-Request-ID response header — quote either one to support.
Example
{ "error": { "code": "not_found", "message": "Query not found", "docs_url": "https://docs.justcrawl.io/guides/errors/not_found", "request_id": "req_9f3a1c7e2b40" }}The engine could not complete the submission and the failure is not attributable to
your SQL (transient, canceled), or the idempotency key was reused for another
request (idempotency_conflict).
object
object
Stable, machine-readable error code. Match on this, not on message.
Customer-safe description. Never raw exception or driver text, with one deliberate exception: a syntax_error (SQLSTATE 42601) passes the database’s own message through verbatim, because it describes SQL the caller wrote themselves and names nothing they could not already see — see the root CLAUDE.md §Error reporting for the full carve-out.
Reference page for this code. Codes shared with the rest of the API have their own page; the BI-local vocabulary points at the single BI errors page that tabulates all of them.
Correlation id. Identical to the X-Request-ID response header — quote either one to support.
object
Example
{ "error": { "code": "transient" }}Your organization’s concurrency or queue-depth cap is full (too_many_queries).
No Retry-After header is sent — this API emits none anywhere on the BI surface,
so do not read one. Back off with capped exponential retry instead.
object
object
Stable, machine-readable error code. Match on this, not on message.
Customer-safe description. Never raw exception or driver text, with one deliberate exception: a syntax_error (SQLSTATE 42601) passes the database’s own message through verbatim, because it describes SQL the caller wrote themselves and names nothing they could not already see — see the root CLAUDE.md §Error reporting for the full carve-out.
Reference page for this code. Codes shared with the rest of the API have their own page; the BI-local vocabulary points at the single BI errors page that tabulates all of them.
Correlation id. Identical to the X-Request-ID response header — quote either one to support.
object
Example
{ "error": { "code": "too_many_queries" }}Unexpected server error. Full detail goes to the logs and PostHog $exception capture, never the response
Error envelope returned by every /api/v1/bi/* handler error. Unlike the rest of the API — where the structured shape is opt-in via the application/vnd.justcrawl.v1+json media type — a BI handler always emits it, and all four fields are always present. The one exception: 401s from missing or invalid auth are rejected by shared middleware before any BI handler runs, so they carry the flat { "error": "<message>" } shape (Error schema) instead — see the Unauthorized response on each operation.
object
object
Stable, machine-readable error code. Match on this, not on message.
Customer-safe description. Never raw exception or driver text, with one deliberate exception: a syntax_error (SQLSTATE 42601) passes the database’s own message through verbatim, because it describes SQL the caller wrote themselves and names nothing they could not already see — see the root CLAUDE.md §Error reporting for the full carve-out.
Reference page for this code. Codes shared with the rest of the API have their own page; the BI-local vocabulary points at the single BI errors page that tabulates all of them.
Correlation id. Identical to the X-Request-ID response header — quote either one to support.
Example
{ "error": { "code": "internal_error", "message": "Failed to list queries", "docs_url": "https://docs.justcrawl.io/guides/errors/internal_error", "request_id": "req_9f3a1c7e2b40" }}The query exceeded the engine’s time limit (statement_timeout). The limit that
applied is reported as maxQuerySec by the result manifest.
object
object
Stable, machine-readable error code. Match on this, not on message.
Customer-safe description. Never raw exception or driver text, with one deliberate exception: a syntax_error (SQLSTATE 42601) passes the database’s own message through verbatim, because it describes SQL the caller wrote themselves and names nothing they could not already see — see the root CLAUDE.md §Error reporting for the full carve-out.
Reference page for this code. Codes shared with the rest of the API have their own page; the BI-local vocabulary points at the single BI errors page that tabulates all of them.
Correlation id. Identical to the X-Request-ID response header — quote either one to support.
object
Example
{ "error": { "code": "statement_timeout" }}