---
title: Queries API
description: Submit SQL over HTTPS, poll until it succeeds, and download results as Parquet or read them inline as JSON.
species: reference
---

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

```bash fragment
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) until
`status` becomes `succeeded` or `failed`.

## Submit a query

```endpoint
POST /queries
```

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

```bash
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"}'
```

```json
{
  "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 an
`Idempotent-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

```endpoint
GET /queries/{id}
```

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

```bash
curl https://api.supernova.ai/queries/qr_... \
  -H "Authorization: Bearer pat_..."
```

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```sql fragment
-- DuckDB, in your own environment
select * from read_parquet('https://storage.googleapis.com/...')
```

```text
# 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:

```json
{
  "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 a
`Retry-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.
