Queries API#
Submit a read-only SQL query, poll it by id, and collect the results — as signed
Parquet download URLs by default, or inline JSON rows when that's more
convenient. Everything on this page works with curl.
All requests are authenticated with a bearer credential — a personal access
token (pat_…), created in your dashboard and shown once at creation:
Authorization: Bearer pat_...Keep tokens server-side; they grant access to your data.
The lifecycle#
Queries run asynchronously. You submit SQL and immediately get back a query
object with status: "running" and an id. Poll it every 1–2 seconds (backing
off for long-running queries — polls count against your rate limit) untilstatus becomes succeeded or failed.
Submit a query#
Returns 202 with the running query; the Location header points at the
query's URL.
| Body field | Type | |
|---|---|---|
sql | string | Required. One read-only statement — see restrictions below. |
curl https://api.supernova.ai/queries \
-H "Authorization: Bearer pat_..." \
-H "Content-Type: application/json" \
-d '{"sql": "select currency, count(*) as n from stripe.customers group by currency"}'{
"object": "query",
"id": "qr_...",
"status": "running",
"created": 1751731200,
"sql": "select currency, count(*) as n ...",
"livemode": true
}Retries are safe with an Idempotency-Key header: replaying the same key
returns the original query object instead of running it again, marked with anIdempotent-Replayed: true header. A key is bound to the exact sql it first
ran — reusing it with different SQL is rejected with `409
idempotency_key_reused`, so an edited retry can never silently return the old
query's results. Keys expire after 24 hours.
Fetch status and results#
The query's status and, once succeeded, its results.
| Query param | Type | |
|---|---|---|
format | string | files (default) returns signed Parquet download URLs; inline returns rows in the response. |
limit | integer | inline only. Rows per page — default 100, max 1000. |
starting_after | string | inline only. The next_cursor from the previous page. |
curl https://api.supernova.ai/queries/qr_... \
-H "Authorization: Bearer pat_..."{
"object": "query",
"id": "qr_...",
"status": "succeeded",
"created": 1751731200,
"duration_ms": 812,
"row_count": null,
"files": [
{ "url": "https://storage.googleapis.com/...", "bytes": 18734, "expires": 1751734800 }
]
}duration_ms is execution time, excluding time before execution. With format=inline,
rows come back paginated in the response — row_count is the total across all
pages:
{
"object": "query",
"id": "qr_...",
"status": "succeeded",
"created": 1751731200,
"duration_ms": 812,
"columns": ["currency", "n"],
"row_count": 3,
"data": [
{ "currency": "usd", "n": 1230 },
{ "currency": "eur", "n": 88 },
{ "currency": "gbp", "n": 41 }
],
"has_more": false,
"next_cursor": null
}A failed query carries a structured error instead of results:
{
"object": "query",
"id": "qr_...",
"status": "failed",
"created": 1751731200,
"duration_ms": 64,
"error": { "message": "Binder Error: Table \"custmers\" does not exist" }
}Result files#
The signed URLs need no authentication — anyone holding one can download the
file until it expires, so treat them like the data itself. URLs are valid for 1
hour; every poll mints fresh ones. Read files as a list even though results
are a single file today — large results may split in the future. The URLs
support HTTP range requests, so Parquet readers fetch selectively:
-- DuckDB, in your own environment
select * from read_parquet('https://storage.googleapis.com/...')# pandas
df = pd.read_parquet("https://storage.googleapis.com/...")File-only responses don't scan the Parquet, so row_count is null there;
the exact count is in the Parquet metadata, or in any inline page.
Fetch results promptly: queries and their results are retained for at least 24
hours after submission.
SQL restrictions#
A query is rejected up front (HTTP 400) if it has more than one statement
(multiple_statements), isn't read-only — write, DDL, and session keywords are
rejected, though they're fine inside string literals, so `where type =
'DELETE' works (write_not_allowed`) — calls a file or storage function such
as read_parquet or read_csv, even inside string literals
(forbidden_function), or exceeds 100,000 characters (sql_too_long).
Storage URIs (gs://, s3://, file://) are rejected anywhere in the query
text; to filter a column holding them, match a substring without the scheme:where path like '%bucket/x%'.
Errors#
Errors return the matching HTTP status and a JSON body; every response carries
a Request-Id header — include it when contacting support. Errors tied to a
specific input name it in param, and doc_url links the code's explanation:
{
"error": {
"type": "invalid_request_error",
"code": "write_not_allowed",
"message": "Only read-only queries are allowed. ...",
"doc_url": "https://api.supernova.ai/docs#write_not_allowed",
"request_id": "req_..."
}
}The codes this resource returns:
| Status | Code | Meaning |
|---|---|---|
| 400 | parameter_missing | A required parameter (e.g. sql) was absent |
| 400 | write_not_allowed | The query contained a write, DDL, or session keyword |
| 400 | multiple_statements | More than one ;-separated statement |
| 400 | forbidden_function | A file or storage function was referenced |
| 400 | sql_too_long | sql exceeded 100,000 characters |
| 400 | invalid_format | format was not files or inline |
| 400 | invalid_cursor | starting_after was not a cursor from a previous page |
| 404 | query_not_found | No query with that id under this account |
| 409 | idempotency_key_reused | The Idempotency-Key was already used with different sql |
| 429 | rate_limited | Request rate limit exceeded — honor Retry-After |
| 429 | too_many_queries | Too many queries in flight for the account |
Rate limits#
Each caller may make up to 120 requests per minute, and each account up to 600
per minute across all its callers; over either, requests return 429 with aRetry-After header in seconds. There's also a ceiling on concurrently
in-flight queries per account (too_many_queries). At the recommended 1–2
second poll cadence, one caller comfortably polls a handful of queries at once
— back off if you run many in parallel.