ReferenceAPI Reference

API Reference

Powerful data platform API for managed databases, queries, and analytics. Hotdata exposes a /v1/* HTTP API at api.hotdata.dev.

Authentication

Most /v1/* endpoints require two headers:

Authorization: Bearer <api_token>
X-Workspace-Id: <workspace_public_id>

Workspaces

Workspace management

List workspaces

Endpoint: GET /v1/workspaces

Query parameters:

  • organization_public_id string — Filter by organization. Defaults to the user's current organization.

Response: 200 Successful response

{
  "ok": true,
  "workspaces": [
    {
      "public_id": "workm4lz2mp899l2i7h9lk9u84azg3",
      "name": "production-analytics",
      "active": true,
      "favorite": true,
      "provision_status": "provisioned"
    }
  ]
}

Errors: 401 Missing or invalid authorization, 403 Forbidden — not a member of the organization or workspace token used, 404 Organization not found

Create a workspace

Endpoint: POST /v1/workspaces

Request body:

  • name stringrequired. Name for the new workspace.
  • organization_public_id string — Target organization. Defaults to the user's current organization.
{
  "name": "production-analytics",
  "organization_public_id": "string"
}

Response: 201 Workspace created

{
  "ok": true,
  "workspace": {
    "public_id": "workm4lz2mp899l2i7h9lk9u84azg3",
    "name": "production-analytics",
    "provision_status": "pending"
  }
}

Errors: 400 Invalid JSON body, 401 Missing or invalid authorization, 403 Forbidden, 404 Organization not found, 422 Validation error (e.g. name required)

Delete a workspace

Endpoint: DELETE /v1/workspaces/{public_id}

Response: 204 Workspace deleted

Errors: 401 Missing or invalid authorization, 403 Workspace-scoped tokens are not allowed, 404 Workspace not found, or caller is not a member of its organization

Query

Execute SQL queries against connected data sources. Use standard Postgres-compatible SQL syntax to query and join across multiple connections. Results are returned inline and also persisted asynchronously for later retrieval.

Execute SQL query

Endpoint: POST /v1/query

Request body:

  • async boolean — When true, execute the query asynchronously and return a query run ID for polling via GET /query-runs/. The query results can be retrieved via GET /results/ once the query run status is "succeeded".
  • async_after_ms integer,null — If set (requires async = true), first attempt the query synchronously and wait up to this many milliseconds: if it finishes in time the full result is returned, otherwise an async response (a run id to poll) is returned. Must be at least 1000 and at most the server's configured maximum; a value out of that range, or set without async = true, is rejected with 400.. Min: 1000
  • database_id string,null — Database to scope the query to (its id). Alternative to the X-Database-Id header — exactly one source must be provided. If both this field and the header are set and they disagree, the request is rejected with a 400.
  • default_catalog string,null — Catalog that unqualified table references resolve against within the query's database scope. Must name a catalog visible in the database (default, an attached catalog alias, or a system catalog). Defaults to default when omitted.
  • default_schema string,null — Schema that unqualified table references resolve against within the query's database scope. Defaults to main when omitted. Existence is not validated up front — an unknown schema surfaces as a "table not found" error at planning time.
  • sql stringrequired
{
  "async": true,
  "async_after_ms": null,
  "database_id": null,
  "default_catalog": null,
  "default_schema": null,
  "sql": "string"
}

Response: 200 Query executed successfully

{
  "columns": [
    "string"
  ],
  "execution_time_ms": 0,
  "nullable": [
    true
  ],
  "preview_row_count": 0,
  "query_run_id": "string",
  "result_id": null,
  "row_count": 0,
  "rows": [
    [
      null
    ]
  ],
  "total_row_count": null,
  "truncated": true,
  "warning": null
}

Response: 202 Query submitted asynchronously

{
  "query_run_id": "string",
  "reason": null,
  "status": "string",
  "status_url": "string"
}

Errors: 400 Invalid request (no database specified, or header/body database_id conflict), 404 Database not found, 429 The engine was too busy to run this query right now — too many concurrent queries, or not enough memory available (often because of other queries running at the same time). Retry after the Retry-After delay; if it persists, narrowing the query (add a filter or LIMIT) may help., 500 Internal server error, 503 Result store temporarily unavailable (a truncated result could not be persisted); retry after the Retry-After delay

Connections

Manage connections to remote databases (Postgres, MySQL, Snowflake, BigQuery, DuckLake, etc.). Creating a connection registers the source and triggers automatic schema discovery. Each connection's tables are cached locally for fast query performance.

List connections

Endpoint: GET /v1/connections

Response: 200 List of connections

{
  "connections": [
    {
      "id": "string",
      "name": "string",
      "source_type": "string"
    }
  ]
}

Create connection

Endpoint: POST /v1/connections

Request body:

  • config objectrequired. Connection configuration object. Fields vary by source type (host, port, database, etc.).
  • name stringrequired
  • secret_id string,null — Optional reference to a secret by ID (e.g., "secr_abc123"). If provided, this secret will be used for authentication. Mutually exclusive with secret_name.
  • secret_name string,null — Optional reference to a secret by name. If provided, this secret will be used for authentication. Mutually exclusive with secret_id.
  • skip_discovery boolean — If true, skip automatic schema discovery after registering the connection. The connection will be created but no tables will be discovered. You can run discovery later via the refresh endpoint.
  • source_type stringrequired
{
  "config": {},
  "name": "string",
  "secret_id": null,
  "secret_name": null,
  "skip_discovery": true,
  "source_type": "string"
}

Response: 201 Connection created

{
  "discovery_error": null,
  "discovery_status": "success",
  "id": "string",
  "name": "string",
  "source_type": "string",
  "tables_discovered": 0
}

Errors: 400 Invalid request, 409 Connection already exists

Get connection

Endpoint: GET /v1/connections/{connection_id}

Path parameters:

  • connection_id string — Connection ID

Response: 200 Connection details

{
  "id": "string",
  "name": "string",
  "source_type": "string",
  "synced_table_count": 0,
  "table_count": 0
}

Errors: 404 Connection not found

Delete connection

Endpoint: DELETE /v1/connections/{connection_id}

Path parameters:

  • connection_id string — Connection ID

Response: 204 Connection deleted

Errors: 404 Connection not found, 409 Connection backs a database's default catalog, or is attached to one or more databases as a non-default catalog; detach via DELETE /v1/databases//catalogs/ first

Purge connection cache

Endpoint: DELETE /v1/connections/{connection_id}/cache

Path parameters:

  • connection_id string — Connection ID

Response: 204 Cache purged

Errors: 400 Managed catalogs own their data and cannot be cache-purged, 404 Connection not found, 409 Connection backs a database's default catalog and cannot be purged directly

Check connection health

Endpoint: GET /v1/connections/{connection_id}/health

Path parameters:

  • connection_id string — Connection ID

Response: 200 Connection health status

{
  "connection_id": "string",
  "error": null,
  "healthy": true,
  "latency_ms": 0
}

Errors: 404 Connection not found

Add managed schema

Endpoint: POST /v1/connections/{connection_id}/schemas

Path parameters:

  • connection_id string — Connection ID

Request body:

  • name stringrequired
  • tables AddManagedTableDecl[]
    • key string[] — Columns that uniquely identify a row, enabling the key-based load modes (delete, update, upsert) on this table: those loads match rows by these columns' values. Omit (the default) to declare no key; the table can still be loaded with replace and append, but key-based modes are then rejected.
    • name stringrequired
{
  "name": "string",
  "tables": [
    {
      "key": [
        "string"
      ],
      "name": "string"
    }
  ]
}

Response: 201 Schema added

{
  "connection_id": "string",
  "schema": "string",
  "tables": [
    "string"
  ]
}

Errors: 400 Connection is not a managed catalog or identifier is invalid, 404 Connection not found, 409 Schema already exists

Add managed table

Endpoint: POST /v1/connections/{connection_id}/schemas/{schema}/tables

Path parameters:

  • connection_id string — Connection ID
  • schema string — Schema name

Request body:

  • key string[] — Columns that uniquely identify a row, enabling the key-based load modes (delete, update, upsert) on this table: those loads match rows by these columns' values. Omit (the default) to declare no key; the table can still be loaded with replace and append, but key-based modes are then rejected.
  • name stringrequired
{
  "key": [
    "string"
  ],
  "name": "string"
}

Response: 201 Table added

{
  "connection_id": "string",
  "schema": "string",
  "table": "string"
}

Errors: 400 Connection is not a managed catalog or identifier is invalid, 404 Connection or schema not found, 409 Table already exists

Delete managed table

Endpoint: DELETE /v1/connections/{connection_id}/schemas/{schema}/tables/{table}

Path parameters:

  • connection_id string — Connection ID
  • schema string — Schema name
  • table string — Table name

Response: 204 Managed table deleted

Errors: 400 Connection is not a managed catalog, 404 Connection or table not found

Load managed table from upload or query result

Endpoint: POST /v1/connections/{connection_id}/schemas/{schema}/tables/{table}/loads

Path parameters:

  • connection_id string — Connection ID
  • schema string — Schema name
  • table string — Table name

Request body:

  • async boolean — When true, run the load as a background job and return a job ID to poll instead of blocking until it finishes. Recommended for large uploads, which can take longer than an HTTP request should stay open.
  • async_after_ms integer,null — If set (requires async = true), wait up to this many milliseconds for the load to finish: if it completes in time the full result is returned (200), otherwise a 202 with a job ID to poll. Must be between 1000 and the server maximum; a value out of that range, or set without async = true, is rejected with 400.. Min: 1000
  • format string,null — File format of the upload: "csv", "json", or "parquet". Optional — when omitted, the format is auto-detected from the upload's Content-Type and, failing that, from the file contents. Provide it explicitly to override detection or when the contents are ambiguous. "json" expects newline-delimited JSON (one object per line), not a JSON array. Only applies to upload_id; query results are always parquet.
  • key array,null — Key columns identifying rows for "delete", "update", and "upsert" loads — the columns whose values decide which existing row an incoming row removes, updates, or replaces. Omit to use the key the table was created with. Keep the key consistent across loads of the same table: changing it re-targets which rows are matched. Ignored for "replace" and "append".
  • mode stringrequired. How the data is applied: "replace" overwrites the table's contents, "append" inserts the new rows on top of the existing data.
  • result_id string,null — ID of a persisted query result (see GET /v1/results/{result_id}) to publish as the table's contents. The result is copied into the table, so the table keeps its data even after the result expires. A result can be loaded into any number of tables. Provide either this or upload_id, not both.
  • upload_id string,null — ID of a previously-staged upload (see POST /v1/files). The upload is claimed atomically; concurrent loads against the same upload_id return 409. Provide either this or result_id, not both.
{
  "async": true,
  "async_after_ms": null,
  "format": null,
  "key": null,
  "mode": "string",
  "result_id": null,
  "upload_id": null
}

Response: 200 Managed table loaded

{
  "arrow_schema_json": "string",
  "connection_id": "string",
  "row_count": 0,
  "schema_name": "string",
  "table_name": "string"
}

Response: 202 Upload load accepted and running in the background; poll the returned job for status and result

{
  "id": "string",
  "status": "pending",
  "status_url": "string"
}

Errors: 400 Invalid request (bad mode, both or neither of upload_id/result_id, format combined with result_id, non-managed connection, invalid identifier, bad parquet, or the result failed to compute), 404 Connection, upload, or result not found, or the table was deleted, 409 Upload already consumed or in flight, the result is still being computed, or the incoming data changes a column's type incompatibly (only widening to a larger compatible type can be applied automatically); the existing data is unchanged and remains queryable

Purge table cache

Endpoint: DELETE /v1/connections/{connection_id}/tables/{schema}/{table}/cache

Path parameters:

  • connection_id string — Connection ID
  • schema string — Schema name
  • table string — Table name

Response: 204 Table cache purged

Errors: 404 Not found

Get table profile

Endpoint: GET /v1/connections/{connection_id}/tables/{schema}/{table}/profile

Path parameters:

  • connection_id string — Connection ID
  • schema string — Schema name
  • table string — Table name

Response: 200 Column profile statistics

{
  "columns": [
    {
      "cardinality": 0,
      "data_type": "string",
      "name": "string",
      "null_count": 0,
      "null_percentage": 0,
      "profile": null
    }
  ],
  "connection": "string",
  "row_count": 0,
  "schema": "string",
  "synced_at": null,
  "table": "string"
}

Errors: 404 Table or profile not found

Connection Types

Discover available connection types and their configuration requirements. Each type describes the config parameters and authentication needed to create a connection.

List connection types

Endpoint: GET /v1/connection-types

Response: 200 Available connection types

{
  "connection_types": [
    {
      "label": "string",
      "name": "string"
    }
  ]
}

Get connection type details

Endpoint: GET /v1/connection-types/{name}

Path parameters:

  • name string — Connection type name (e.g. postgres, mysql, snowflake)

Response: 200 Connection type details

{
  "auth": null,
  "config_schema": null,
  "label": "string",
  "name": "string"
}

Errors: 404 Unknown connection type

Refresh

Refresh schema metadata and table data for connections. Schema refresh re-discovers tables and column definitions from the remote source. Data refresh re-syncs cached data with the latest from the remote tables.

Refresh connection data

Endpoint: POST /v1/refresh

Request body:

  • async boolean — When true, submit the refresh as a background job and return immediately with a job ID for status polling. Only supported for data refresh operations.

  • async_after_ms integer,null — If set (requires async = true), wait up to this many milliseconds for the refresh to finish: if it completes in time the full result is returned, otherwise a 202 with a job ID to poll. Must be between 1000 and the server maximum; a value out of that range, or set without async = true, is rejected with 400. Only applies to data refresh.. Min: 1000

  • connection_id string,null

  • data boolean

  • include_uncached boolean — Controls whether uncached tables are included in connection-wide data refresh.

  • false (default): Only refresh tables that already have cached data. This is the common case for keeping existing data up-to-date.

  • true: Also sync tables that haven't been cached yet, essentially performing an initial sync for any new tables discovered since the connection was created.

This field only applies to connection-wide data refresh (when data=true and table_name is not specified). It has no effect on single-table refresh or schema refresh operations.

  • schema_name string,null
  • table_name string,null
{
  "async": true,
  "async_after_ms": null,
  "connection_id": null,
  "data": true,
  "include_uncached": true,
  "schema_name": null,
  "table_name": null
}

Response: 200 Refresh completed

null

Response: 202 Refresh accepted and running in the background; poll the returned job for status

{
  "id": "string",
  "status": "pending",
  "status_url": "string"
}

Errors: 400 Invalid request, 404 Connection not found, 409 A column's type changed incompatibly and can't be applied automatically (only widening to a larger compatible type is). The existing data is unchanged and remains queryable.

Information Schema

Inspect table and column metadata across all connections. Returns schema information including column names, data types, and sync status for every discovered table.

List tables

Endpoint: GET /v1/information_schema

Query parameters:

  • connection_id string — Filter by connection ID
  • schema string — Filter by schema name (supports % wildcards)
  • table string — Filter by table name (supports % wildcards)
  • include_columns boolean — Include column definitions (default: false)
  • limit integer — Maximum number of tables per page
  • cursor string — Pagination cursor from a previous response

Response: 200 Table metadata

{
  "count": 0,
  "has_more": true,
  "limit": 0,
  "next_cursor": null,
  "tables": [
    {
      "columns": null,
      "connection": "string",
      "last_sync": null,
      "schema": "string",
      "synced": true,
      "table": "string"
    }
  ]
}

Errors: 404 Connection not found

Secrets

Store and manage credentials used by connections. Secrets are encrypted at rest and referenced by connections for authentication. Secret values are never returned by the API — only metadata (name, timestamps) is exposed.

List secrets

Endpoint: GET /v1/secrets

Response: 200 List of secrets

{
  "secrets": [
    {
      "created_at": "2026-01-01T00:00:00Z",
      "name": "string",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  ]
}

Create secret

Endpoint: POST /v1/secrets

Request body:

  • name stringrequired
  • value stringrequired
{
  "name": "string",
  "value": "string"
}

Response: 201 Secret created

{
  "created_at": "2026-01-01T00:00:00Z",
  "id": "string",
  "name": "string"
}

Errors: 409 Secret already exists

Get secret

Endpoint: GET /v1/secrets/{name}

Path parameters:

  • name string — Secret name

Response: 200 Secret metadata

{
  "created_at": "2026-01-01T00:00:00Z",
  "name": "string",
  "updated_at": "2026-01-01T00:00:00Z"
}

Errors: 404 Secret not found

Update secret

Endpoint: PUT /v1/secrets/{name}

Path parameters:

  • name string — Secret name

Request body:

  • value stringrequired
{
  "value": "string"
}

Response: 200 Secret updated

{
  "name": "string",
  "updated_at": "2026-01-01T00:00:00Z"
}

Errors: 404 Secret not found

Delete secret

Endpoint: DELETE /v1/secrets/{name}

Path parameters:

  • name string — Secret name

Response: 204 Secret deleted

Errors: 404 Secret not found

Results

Retrieve persisted query results. Every query execution persists its results asynchronously. Results transition through statuses: processing → ready (or failed). Once ready, the full result data can be retrieved by ID.

List results

Endpoint: GET /v1/results

Query parameters:

  • limit integer — Maximum number of results (default: 100, max: 1000)
  • offset integer — Pagination offset (default: 0)

Response: 200 List of results

{
  "count": 0,
  "has_more": true,
  "limit": 0,
  "offset": 0,
  "results": [
    {
      "created_at": "2026-01-01T00:00:00Z",
      "error_message": null,
      "id": "string",
      "status": "string"
    }
  ]
}

Errors: 400 Missing or malformed X-Database-Id header, 404 Database not found

Get result

Endpoint: GET /v1/results/{id}

Path parameters:

  • id string — Result ID

Query parameters:

  • offset integer — Rows to skip (default: 0)
  • limit integer — Maximum rows to return (default: unbounded)
  • format ResultsFormatQueryarrow, json, csv, md, or parquet — overrides the Accept header. markdown is also accepted at runtime as an alias for md.

Response: 200 Result data. The body depends on the negotiated format: JSON callers receive GetResultResponse; Arrow callers receive an Arrow IPC stream; CSV callers receive comma-separated text (LF-terminated, double-quote escaped, RFC 4180-style quoting but not RFC 4180-strict on line endings); Markdown callers receive a single GitHub-flavored pipe table; Parquet callers receive the raw parquet bytes, served as-is. Non-finite floats (±Inf, NaN) render as null (JSON) or empty cells (CSV, Markdown) for cross-format consistency. Accept is treated as a flat content-type list — q= quality values are ignored; use ?format= to disambiguate.

{
  "columns": null,
  "error_message": null,
  "nullable": null,
  "result_id": "string",
  "row_count": null,
  "rows": null,
  "status": "string"
}

Response: 202 Result is still being computed (pending or processing). Poll the same URL.

{
  "columns": null,
  "error_message": null,
  "nullable": null,
  "result_id": "string",
  "row_count": null,
  "rows": null,
  "status": "string"
}

Errors: 400 Invalid offset, limit, or format., 404 Result not found., 409 Result computation failed. Body carries error_message describing the failure.

Query Runs

View the history of executed queries. Each query run records the SQL text, execution time, result reference, and status. Useful for auditing and debugging query performance.

List query runs

Endpoint: GET /v1/query-runs

Query parameters:

  • limit integer — Maximum number of results
  • cursor string — Pagination cursor
  • status string — Filter by status (comma-separated, e.g. status=running,failed)
  • saved_query_id string — Filter by saved query ID

Response: 200 List of query runs

{
  "count": 0,
  "has_more": true,
  "limit": 0,
  "next_cursor": null,
  "query_runs": [
    {
      "bytes_scanned": null,
      "completed_at": null,
      "created_at": "2026-01-01T00:00:00Z",
      "error_message": null,
      "execution_time_ms": null,
      "id": "string",
      "result_id": null,
      "row_count": null,
      "rows_scanned": null,
      "saved_query_id": null,
      "saved_query_version": null,
      "server_processing_ms": null,
      "snapshot_id": "string",
      "sql_hash": "string",
      "sql_text": "string",
      "status": "string",
      "trace_id": null,
      "user_public_id": null,
      "warning_message": null
    }
  ]
}

Errors: 400 Missing or malformed X-Database-Id header, 404 Database not found

Get query run

Endpoint: GET /v1/query-runs/{id}

Path parameters:

  • id string — Query run ID

Response: 200 Query run details

{
  "bytes_scanned": null,
  "completed_at": null,
  "created_at": "2026-01-01T00:00:00Z",
  "error_message": null,
  "execution_time_ms": null,
  "id": "string",
  "result_id": null,
  "row_count": null,
  "rows_scanned": null,
  "saved_query_id": null,
  "saved_query_version": null,
  "server_processing_ms": null,
  "snapshot_id": "string",
  "sql_hash": "string",
  "sql_text": "string",
  "status": "string",
  "trace_id": null,
  "user_public_id": null,
  "warning_message": null
}

Errors: 400 Missing or malformed X-Database-Id header, 404 Query run or database not found

Uploads

Upload files, then reference them by ID when loading a managed table. There are two ways to get the bytes in:

  • Direct to the APIPOST /v1/files streams the raw request body through the server to storage. Simplest for a single file; accepts files up to 20 GiB.
  • Presigned session — create a session, then PUT the bytes straight to storage using the returned URL (bypassing the server), and finalize. This offloads the transfer from the server and is the better fit for many files.

Presigned upload flow

  1. POST /v1/uploads — returns a one-time finalize_token plus one of three shapes: • a single url (mode: single) — PUT the whole file to it; • a list of part_urls (mode: multipart) when you declared the size — PUT each part to its entry; • when you omit declared_size_bytes (streaming), mode: multipart with a part_size but no part_urls — call POST /v1/uploads/{upload_id}/parts with a batch of part_numbers to mint a presigned PUT URL for each part you're about to upload.
  2. PUT the bytes: the whole file to url, or each part to its URL (keeping each part's ETag). For a streaming upload, slice the file into part_size chunks and mint part URLs as you go.
  3. POST /v1/uploads/{upload_id}/finalize with the token (and, for multipart, the part ETags) to confirm the upload and make it usable.

Which approach to use

  • One file, or a handful — create a session, PUT, then finalize (or simply POST /v1/files).
  • Many small filesPOST /v1/uploads/batch creates up to 100 sessions in one call; upload and finalize each one independently, at your own pace.
  • A large file — use a presigned session. Small files are returned as a single PUT; larger files are returned as a multi-part upload automatically, which you can upload in parallel and resume part by part.
  • POST /v1/files remains available and streams the body through the server (up to 20 GiB); a presigned session avoids that server hop.

List uploads

Endpoint: GET /v1/files

Query parameters:

  • status string — Filter by upload status

Response: 200 List of uploads

{
  "uploads": [
    {
      "content_type": null,
      "created_at": "2026-01-01T00:00:00Z",
      "id": "string",
      "size_bytes": 0,
      "status": "string"
    }
  ]
}

Upload file

Endpoint: POST /v1/files

Request body: Raw bytes (application/octet-stream)

Response: 201 File uploaded

{
  "content_type": null,
  "created_at": "2026-01-01T00:00:00Z",
  "id": "string",
  "size_bytes": 0,
  "status": "string"
}

Errors: 400 Invalid request

Create upload session

Endpoint: POST /v1/uploads

Request body:

  • checksum_algo string,null — Integrity checksum algorithm you are volunteering for this file. Currently only sha256 is accepted. Optional; pair with checksum_value.
  • checksum_value string,null — Integrity checksum value, paired with checksum_algo. Optional.
  • content_encoding string,null — Content encoding to record for the uploaded file (for example gzip). Optional.
  • content_type string,null — Content type to record for the uploaded file (for example the Parquet, CSV, or JSON MIME type). Optional.
  • declared_size_bytes integer,null — The exact size, in bytes, of the file you will upload. Optional. When provided, it is validated at create time against the maximum allowed size, and again at finalize against the bytes actually stored — a mismatch fails the finalize. Omit it to create a streaming (unknown-size) upload: the session is always multi-part and returns no part URLs up front; instead you mint part URLs on demand from POST /v1/uploads/{upload_id}/parts as you upload, and finalize validates only that the file is non-empty.. Min: 0
  • filename string,null — Original file name, recorded with the upload for your own bookkeeping. Optional and advisory — it does not affect where the bytes are stored or how they are loaded.
  • part_size integer,null — Preferred size, in bytes, of each part for a large (multi-part) upload. Optional hint — the service clamps it to the allowed part-size range and to the maximum number of parts, and ignores it for small files uploaded with a single PUT. Omit to let the service choose.. Min: 0
{
  "checksum_algo": null,
  "checksum_value": null,
  "content_encoding": null,
  "content_type": null,
  "declared_size_bytes": null,
  "filename": null,
  "part_size": null
}

Response: 201 Upload session created

{
  "finalize_token": "string",
  "headers": {},
  "mode": "string",
  "part_size": null,
  "part_urls": null,
  "upload_id": "string",
  "url": null
}

Errors: 400 Invalid request (e.g. file too large, unsupported checksum algorithm), 501 Storage backend cannot issue upload URLs; use POST /v1/files instead

Create upload sessions in bulk

Endpoint: POST /v1/uploads/batch

Request body:

  • uploads CreateUploadRequest[] — required
    • checksum_algo string,null — Integrity checksum algorithm you are volunteering for this file. Currently only sha256 is accepted. Optional; pair with checksum_value.
    • checksum_value string,null — Integrity checksum value, paired with checksum_algo. Optional.
    • content_encoding string,null — Content encoding to record for the uploaded file (for example gzip). Optional.
    • content_type string,null — Content type to record for the uploaded file (for example the Parquet, CSV, or JSON MIME type). Optional.
    • declared_size_bytes integer,null — The exact size, in bytes, of the file you will upload. Optional. When provided, it is validated at create time against the maximum allowed size, and again at finalize against the bytes actually stored — a mismatch fails the finalize. Omit it to create a streaming (unknown-size) upload: the session is always multi-part and returns no part URLs up front; instead you mint part URLs on demand from POST /v1/uploads/{upload_id}/parts as you upload, and finalize validates only that the file is non-empty.. Min: 0
    • filename string,null — Original file name, recorded with the upload for your own bookkeeping. Optional and advisory — it does not affect where the bytes are stored or how they are loaded.
    • part_size integer,null — Preferred size, in bytes, of each part for a large (multi-part) upload. Optional hint — the service clamps it to the allowed part-size range and to the maximum number of parts, and ignores it for small files uploaded with a single PUT. Omit to let the service choose.. Min: 0
{
  "uploads": [
    {
      "checksum_algo": null,
      "checksum_value": null,
      "content_encoding": null,
      "content_type": null,
      "declared_size_bytes": null,
      "filename": null,
      "part_size": null
    }
  ]
}

Response: 201 Upload sessions created

{
  "uploads": [
    {
      "finalize_token": "string",
      "headers": {},
      "mode": "string",
      "part_size": null,
      "part_urls": null,
      "upload_id": "string",
      "url": null
    }
  ]
}

Errors: 400 Invalid request (e.g. a file too large, unsupported checksum algorithm), 501 Storage backend cannot issue upload URLs; use POST /v1/files instead

Finalize upload

Endpoint: POST /v1/uploads/{upload_id}/finalize

Path parameters:

  • upload_id string — Upload session ID returned at create time

Response: 200 Upload finalized

{
  "content_type": null,
  "created_at": "2026-01-01T00:00:00Z",
  "size_bytes": 0,
  "status": "string",
  "upload_id": "string"
}

Errors: 400 Invalid finalize token, uploaded size mismatch, missing object, or upload not finalizable, 404 Upload session not found

Mint upload part URLs

Endpoint: POST /v1/uploads/{upload_id}/parts

Path parameters:

  • upload_id string — Upload session ID returned at create time

Request body:

  • part_numbers integer[] — required. The 1-based part numbers to mint URLs for. Must be non-empty; each number must be between 1 and the maximum number of parts allowed.
{
  "part_numbers": [
    0
  ]
}

Response: 200 Minted part URLs

{
  "parts": [
    {
      "part_number": 0,
      "url": "string"
    }
  ]
}

Errors: 400 Invalid finalize token, invalid part numbers, batch too large, or the upload is not a multi-part upload, 404 Upload session not found, 501 Storage backend cannot issue upload URLs

Saved Queries

Save, version, and execute named SQL queries. Each update creates a new version, preserving the full history. Saved queries are automatically classified by category (e.g., aggregation, join, filtered scan) and can be executed by ID.

List saved queries

Endpoint: GET /v1/queries

Query parameters:

  • limit integer — Maximum number of results
  • offset integer — Pagination offset

Response: 200 List of saved queries

{
  "count": 0,
  "has_more": true,
  "limit": 0,
  "offset": 0,
  "queries": [
    {
      "created_at": "2026-01-01T00:00:00Z",
      "description": "string",
      "id": "string",
      "latest_version": 0,
      "name": "string",
      "tags": [
        "string"
      ],
      "updated_at": "2026-01-01T00:00:00Z"
    }
  ]
}

Create saved query

Endpoint: POST /v1/queries

Request body:

  • description string,null
  • name stringrequired
  • sql stringrequired
  • tags array,null
{
  "description": null,
  "name": "string",
  "sql": "string",
  "tags": null
}

Response: 201 Saved query created

{
  "category": null,
  "created_at": "2026-01-01T00:00:00Z",
  "description": "string",
  "has_aggregation": null,
  "has_group_by": null,
  "has_join": null,
  "has_limit": null,
  "has_order_by": null,
  "has_predicate": null,
  "id": "string",
  "latest_version": 0,
  "name": "string",
  "num_tables": null,
  "sql": "string",
  "sql_hash": "string",
  "table_size": null,
  "tags": [
    "string"
  ],
  "updated_at": "2026-01-01T00:00:00Z"
}

Errors: 400 Invalid request

Get saved query

Endpoint: GET /v1/queries/{id}

Path parameters:

  • id string — Saved query ID

Response: 200 Saved query details

{
  "category": null,
  "created_at": "2026-01-01T00:00:00Z",
  "description": "string",
  "has_aggregation": null,
  "has_group_by": null,
  "has_join": null,
  "has_limit": null,
  "has_order_by": null,
  "has_predicate": null,
  "id": "string",
  "latest_version": 0,
  "name": "string",
  "num_tables": null,
  "sql": "string",
  "sql_hash": "string",
  "table_size": null,
  "tags": [
    "string"
  ],
  "updated_at": "2026-01-01T00:00:00Z"
}

Errors: 404 Saved query not found

Update saved query

Endpoint: PUT /v1/queries/{id}

Path parameters:

  • id string — Saved query ID

Request body:

  • category_override string,null — Override the auto-detected category. Send null to clear (revert to auto).
  • description string,null
  • name string,null — Optional new name. When omitted the existing name is preserved.
  • sql string,null — Optional new SQL. When omitted the existing SQL is preserved.
  • table_size_override string,null — User annotation for table size. Send null to clear.
  • tags array,null
{
  "category_override": null,
  "description": null,
  "name": null,
  "sql": null,
  "table_size_override": null,
  "tags": null
}

Response: 200 Saved query updated

{
  "category": null,
  "created_at": "2026-01-01T00:00:00Z",
  "description": "string",
  "has_aggregation": null,
  "has_group_by": null,
  "has_join": null,
  "has_limit": null,
  "has_order_by": null,
  "has_predicate": null,
  "id": "string",
  "latest_version": 0,
  "name": "string",
  "num_tables": null,
  "sql": "string",
  "sql_hash": "string",
  "table_size": null,
  "tags": [
    "string"
  ],
  "updated_at": "2026-01-01T00:00:00Z"
}

Errors: 400 Invalid request, 404 Saved query not found

Delete saved query

Endpoint: DELETE /v1/queries/{id}

Path parameters:

  • id string — Saved query ID

Response: 204 Saved query deleted

Errors: 404 Saved query not found

Execute saved query

Endpoint: POST /v1/queries/{id}/execute

Path parameters:

  • id string — Saved query ID

Response: 200 Query executed

{
  "columns": [
    "string"
  ],
  "execution_time_ms": 0,
  "nullable": [
    true
  ],
  "preview_row_count": 0,
  "query_run_id": "string",
  "result_id": null,
  "row_count": 0,
  "rows": [
    [
      null
    ]
  ],
  "total_row_count": null,
  "truncated": true,
  "warning": null
}

Errors: 400 Invalid request (including a missing X-Database-Id header), 404 Saved query or database not found

List saved query versions

Endpoint: GET /v1/queries/{id}/versions

Path parameters:

  • id string — Saved query ID

Query parameters:

  • limit integer — Maximum number of versions
  • offset integer — Pagination offset

Response: 200 List of versions

{
  "count": 0,
  "has_more": true,
  "limit": 0,
  "offset": 0,
  "saved_query_id": "string",
  "versions": [
    {
      "category": null,
      "created_at": "2026-01-01T00:00:00Z",
      "has_aggregation": null,
      "has_group_by": null,
      "has_join": null,
      "has_limit": null,
      "has_order_by": null,
      "has_predicate": null,
      "num_tables": null,
      "sql": "string",
      "sql_hash": "string",
      "table_size": null,
      "version": 0
    }
  ]
}

Errors: 404 Saved query not found

Indexes

Create, list, and delete indexes on cached tables. Supports sorted indexes for range queries and BM25 full-text indexes for keyword search.

List indexes on a table

Endpoint: GET /v1/connections/{connection_id}/tables/{schema}/{table}/indexes

Path parameters:

  • connection_id string — Connection ID
  • schema string — Schema name
  • table string — Table name

Response: 200 Indexes listed

{
  "indexes": [
    {
      "columns": [
        "string"
      ],
      "created_at": "2026-01-01T00:00:00Z",
      "index_name": "string",
      "index_type": "string",
      "metric": null,
      "source_column": null,
      "status": "ready",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  ]
}

Errors: 404 Table not found, 500 Internal server error

Create an index on a table

Endpoint: POST /v1/connections/{connection_id}/tables/{schema}/{table}/indexes

Path parameters:

  • connection_id string — Connection ID
  • schema string — Schema name
  • table string — Table name

Request body:

  • async boolean — When true, create the index as a background job and return a job ID for polling.
  • async_after_ms integer,null — If set (requires async = true), wait up to this many milliseconds for the index build to finish: if it completes in time the index is returned (201), otherwise a 202 with a job ID to poll. Must be between 1000 and the server maximum; a value out of that range, or set without async = true, is rejected with 400.. Min: 1000
  • columns string[] — required. Columns to index. Required for all index types.
  • description string,null — User-facing description of the embedding (e.g., "product descriptions").
  • dimensions integer,null — Output vector dimensions. Some models support multiple dimension sizes (e.g., OpenAI text-embedding-3-small supports 512 or 1536). If omitted, the model's default dimensions are used. Min: 0
  • embedding_provider_id string,null — Embedding provider ID. When set for a vector index, the source column is treated as text and embeddings are generated automatically. The vector index is then built on the generated embedding column ({column}_embedding by default).
  • index_name stringrequired
  • index_type string — Index type: "sorted" (default), "bm25", or "vector"
  • metric string,null — Distance metric for vector indexes: "l2", "cosine", or "dot". When omitted, defaults to "l2" for float array columns or the provider's preferred metric for text columns with auto-embedding.
  • output_column string,null — Custom name for the generated embedding column. Defaults to {column}_embedding.
{
  "async": true,
  "async_after_ms": null,
  "columns": [
    "string"
  ],
  "description": null,
  "dimensions": null,
  "embedding_provider_id": null,
  "index_name": "string",
  "index_type": "string",
  "metric": null,
  "output_column": null
}

Response: 201 Index created

{
  "columns": [
    "string"
  ],
  "created_at": "2026-01-01T00:00:00Z",
  "index_name": "string",
  "index_type": "string",
  "metric": null,
  "source_column": null,
  "status": "ready",
  "updated_at": "2026-01-01T00:00:00Z"
}

Response: 202 Index build accepted and running in the background; poll the returned job for status

{
  "id": "string",
  "status": "pending",
  "status_url": "string"
}

Errors: 400 Invalid request, 404 Table not found, 500 Internal server error

Delete an index

Endpoint: DELETE /v1/connections/{connection_id}/tables/{schema}/{table}/indexes/{index_name}

Path parameters:

  • connection_id string — Connection ID
  • schema string — Schema name
  • table string — Table name
  • index_name string — Index name

Response: 204 Index deleted

Errors: 404 Index not found, 500 Internal server error

List indexes across tables in a database

Endpoint: GET /v1/indexes

Query parameters:

  • connection_id string — Filter to one connection
  • schema string — Filter by schema name
  • table string — Filter by table name
  • index_type string — Filter by index type
  • limit integer — Max indexes per page
  • cursor string — Pagination cursor

Response: 200 Indexes listed

{
  "count": 0,
  "has_more": true,
  "indexes": [
    {
      "columns": [
        "string"
      ],
      "created_at": "2026-01-01T00:00:00Z",
      "index_name": "string",
      "index_type": "string",
      "metric": null,
      "source_column": null,
      "status": "ready",
      "updated_at": "2026-01-01T00:00:00Z",
      "connection_id": null,
      "schema_name": "string",
      "table_name": "string"
    }
  ],
  "limit": 0,
  "next_cursor": null
}

Errors: 400 Missing X-Database-Id or bad cursor, 404 Database not found, 500 Internal server error

Embedding Providers

Manage embedding providers that generate vector embeddings for text columns. Providers can be service-based (e.g., OpenAI) or local. Register a provider, then reference it when creating vector indexes on text columns.

List embedding providers

Endpoint: GET /v1/embedding-providers

Response: 200 List of embedding providers

{
  "embedding_providers": [
    {
      "config": null,
      "created_at": "2026-01-01T00:00:00Z",
      "has_secret": true,
      "id": "string",
      "name": "string",
      "provider_type": "string",
      "source": "string",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  ]
}

Create embedding provider

Endpoint: POST /v1/embedding-providers

Request body:

  • api_key string,null — Inline API key. If provided, a secret is auto-created and referenced. Cannot be used together with secret_name.
  • config any — Provider-specific configuration (model name, base URL, dimensions, etc.)
  • name stringrequired
  • provider_type stringrequired. Provider type: "local" or "service"
  • secret_name string,null — Reference an existing secret by name (for service providers).
{
  "api_key": null,
  "config": null,
  "name": "string",
  "provider_type": "string",
  "secret_name": null
}

Response: 201 Embedding provider created

{
  "config": null,
  "created_at": "2026-01-01T00:00:00Z",
  "id": "string",
  "name": "string",
  "provider_type": "string"
}

Errors: 400 Invalid request, 409 Provider with this name already exists

Get embedding provider

Endpoint: GET /v1/embedding-providers/{id}

Path parameters:

  • id string — Embedding provider ID

Response: 200 Embedding provider details

{
  "config": null,
  "created_at": "2026-01-01T00:00:00Z",
  "has_secret": true,
  "id": "string",
  "name": "string",
  "provider_type": "string",
  "source": "string",
  "updated_at": "2026-01-01T00:00:00Z"
}

Errors: 404 Provider not found

Update embedding provider

Endpoint: PUT /v1/embedding-providers/{id}

Path parameters:

  • id string — Embedding provider ID

Request body:

  • api_key string,null — Inline API key. If provided, updates (or creates) the auto-managed secret.
  • config any
  • name string,null
  • secret_name string,null — Secret name containing the API key. Pass null to clear.
{
  "api_key": null,
  "config": null,
  "name": null,
  "secret_name": null
}

Response: 200 Embedding provider updated

{
  "id": "string",
  "name": "string",
  "updated_at": "2026-01-01T00:00:00Z"
}

Errors: 404 Provider not found

Delete embedding provider

Endpoint: DELETE /v1/embedding-providers/{id}

Path parameters:

  • id string — Embedding provider ID

Response: 204 Embedding provider deleted

Errors: 404 Provider not found

Jobs

Track background jobs. Jobs are submitted internally by other APIs when async execution is requested. Poll job status by ID or list all jobs.

List jobs

Endpoint: GET /v1/jobs

Query parameters:

  • job_type JobType — Filter by job type
  • status string — Filter by status (comma-separated, e.g. status=pending,running)
  • limit integer — Max results (default 50)
  • offset integer — Offset for pagination

Response: 200 List of jobs

{
  "jobs": [
    {
      "attempts": 0,
      "completed_at": null,
      "created_at": "2026-01-01T00:00:00Z",
      "error_message": null,
      "id": "string",
      "job_type": "noop",
      "result": null,
      "status": "pending"
    }
  ]
}

Get job status

Endpoint: GET /v1/jobs/{id}

Path parameters:

  • id string — Job ID

Response: 200 Job status

{
  "attempts": 0,
  "completed_at": null,
  "created_at": "2026-01-01T00:00:00Z",
  "error_message": null,
  "id": "string",
  "job_type": "noop",
  "result": null,
  "status": "pending"
}

Errors: 404 Job not found

Database context

Store and retrieve named text or Markdown documents scoped to a specific database.

List database contexts

Endpoint: GET /v1/databases/{database_id}/context

Path parameters:

  • database_id string — Database ID

Response: 200 Contexts

{
  "contexts": [
    {
      "content": "string",
      "name": "string",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  ]
}

Errors: 404 Database not found

Create or update database context

Endpoint: POST /v1/databases/{database_id}/context

Path parameters:

  • database_id string — Database ID

Request body:

  • content stringrequired
  • name stringrequired. Upsert key in the catalog. Validated with table-name rules (preserves case): ASCII letter or _ first; then alphanumeric or _ only; 1–128 chars; not a SQL reserved word.
{
  "content": "string",
  "name": "string"
}

Response: 200 Context saved

{
  "context": {
    "content": "string",
    "name": "string",
    "updated_at": "2026-01-01T00:00:00Z"
  }
}

Errors: 400 Invalid request, 404 Database not found

Get one database context

Endpoint: GET /v1/databases/{database_id}/context/{name}

Path parameters:

  • database_id string — Database ID
  • name string — Context key: same character rules as a table name

Response: 200 Context found

{
  "context": {
    "content": "string",
    "name": "string",
    "updated_at": "2026-01-01T00:00:00Z"
  }
}

Errors: 400 Invalid request, 404 Database or context not found

Delete database context

Endpoint: DELETE /v1/databases/{database_id}/context/{name}

Path parameters:

  • database_id string — Database ID
  • name string — Context key: same character rules as a table name

Response: 204 Context deleted

Errors: 400 Invalid request, 404 Database or context not found

Databases

Group catalogs into id-addressable databases. Each database auto-creates a default catalog and can attach existing connections under an optional alias. A query that carries the X-Database-Id header is planned only against that database's catalogs.

List databases

Endpoint: GET /v1/databases

Response: 200 List of databases

{
  "databases": [
    {
      "created_at": null,
      "default_catalog": "string",
      "default_schema": "string",
      "expires_at": null,
      "id": "string",
      "name": null
    }
  ]
}

Create database

Endpoint: POST /v1/databases

Request body:

  • default_catalog string,null — Optional name the database's auto-created default catalog answers to inside its query scope. Must be a valid SQL identifier ([a-z0-9_], not starting with a digit) and may not collide with the reserved catalog names hotdata or information_schema. Defaults to default when omitted, so default.main.<table> keeps working.
  • default_schema string,null — Optional schema that unqualified table names resolve to inside this database's query scope. Must be a valid SQL identifier ([a-z0-9_], not starting with a digit). When omitted, a database that declares exactly one schema adopts that schema as its default; otherwise unqualified names resolve to main. Fully-qualified names (<catalog>.<schema>.<table>) are unaffected, and a per-query default_schema still takes precedence.
  • expires_at string,null — When this database expires. Accepts either an RFC 3339 timestamp (e.g. "2026-06-01T00:00:00Z") or a relative duration suffixed with h (hours), m (minutes), or d (days) — for example "24h", "48h", or "7d". Omitted (or empty) means the database never expires. Expiry is best-effort: the database will not be deleted before expires_at, but cleanup may run later than the exact timestamp.
  • name string,null — Optional free-form display label (for UIs/CLIs). Not unique. Not an identifier — databases are always addressed by id.

Accepts the legacy description key as an alias so clients that predate the rename keep populating this field.

  • schemas DatabaseDefaultSchemaDecl[] — Optional schemas/tables to declare on the database's auto-created default catalog. Tables declared here can be loaded via the standard managed-table load endpoint targeting default_connection_id. Omitted or empty means the default catalog starts empty.
    • name stringrequired
    • tables DatabaseDefaultTableDecl[]
      • key string[] — Columns that uniquely identify a row, enabling the key-based load modes (delete, update, upsert) on this table: those loads match rows by these columns' values. Omit (the default) to declare no key; the table can still be loaded with replace and append, but key-based modes are then rejected.
      • name stringrequired
{
  "default_catalog": null,
  "default_schema": null,
  "expires_at": null,
  "name": null,
  "schemas": [
    {
      "name": "string",
      "tables": [
        {}
      ]
    }
  ]
}

Response: 201 Database created

{
  "default_catalog": "string",
  "default_connection_id": "string",
  "default_schema": "string",
  "expires_at": null,
  "id": "string",
  "name": null
}

Errors: 400 Invalid request, 500 Internal server error (e.g., managed-connection create succeeded but database row insert + rollback both failed)

Get database

Endpoint: GET /v1/databases/{database_id}

Path parameters:

  • database_id string — Database ID

Response: 200 Database details

{
  "attachments": [
    {
      "alias": null,
      "connection_id": "string"
    }
  ],
  "created_at": null,
  "default_catalog": "string",
  "default_connection_id": "string",
  "default_schema": "string",
  "expires_at": null,
  "id": "string",
  "name": null
}

Errors: 404 Database not found

Delete database

Endpoint: DELETE /v1/databases/{database_id}

Path parameters:

  • database_id string — Database ID

Response: 204 Database deleted

Errors: 404 Database not found

Attach catalog to database

Endpoint: POST /v1/databases/{database_id}/catalogs

Path parameters:

  • database_id string — Database ID

Request body:

  • alias string,null — Optional alias under which this catalog is reachable inside the database. When omitted, it is reachable by the connection's name.
  • connection_id stringrequired
{
  "alias": null,
  "connection_id": "string"
}

Response: 204 Catalog attached

Errors: 400 Invalid request, 404 Database or connection not found, 409 Catalog already attached or alias collides with an existing attachment

Detach catalog from database

Endpoint: DELETE /v1/databases/{database_id}/catalogs/{connection_id}

Path parameters:

  • database_id string — Database ID
  • connection_id string — Connection ID

Response: 204 Catalog detached

Errors: 400 Cannot detach a database's own default catalog, 404 Database or attachment not found

Fork database

Endpoint: POST /v1/databases/{database_id}/fork

Path parameters:

  • database_id string — Source database ID

Request body:

  • expires_at string,null — When the fork expires. Accepts either an RFC 3339 timestamp (e.g. "2026-06-01T00:00:00Z") or a relative duration suffixed with h (hours), m (minutes), or d (days) — for example "24h" or "7d". When omitted, a still-future expiry on the source is carried over; otherwise the fork never expires.
  • name string,null — Optional display label for the fork. When omitted, the source database's name (if any) is carried over.
{
  "expires_at": null,
  "name": null
}

Response: 201 Database forked

{
  "default_catalog": "string",
  "default_connection_id": "string",
  "default_schema": "string",
  "expires_at": null,
  "id": "string",
  "name": null
}

Errors: 400 The source database can't be forked as-is (for example, it uses a storage backend that does not support forking, or one of its tables has rows that were individually deleted or updated), 404 Source database not found

Add schema to database default catalog

Endpoint: POST /v1/databases/{database_id}/schemas

Path parameters:

  • database_id string — Database ID

Request body:

  • name stringrequired
  • tables AddManagedTableDecl[]
    • key string[] — Columns that uniquely identify a row, enabling the key-based load modes (delete, update, upsert) on this table: those loads match rows by these columns' values. Omit (the default) to declare no key; the table can still be loaded with replace and append, but key-based modes are then rejected.
    • name stringrequired
{
  "name": "string",
  "tables": [
    {
      "key": [
        "string"
      ],
      "name": "string"
    }
  ]
}

Response: 201 Schema added

{
  "connection_id": "string",
  "schema": "string",
  "tables": [
    "string"
  ]
}

Errors: 400 Invalid identifier, 404 Database not found, 409 Schema already exists

Add table to database default catalog

Endpoint: POST /v1/databases/{database_id}/schemas/{schema}/tables

Path parameters:

  • database_id string — Database ID
  • schema string — Schema name

Request body:

  • key string[] — Columns that uniquely identify a row, enabling the key-based load modes (delete, update, upsert) on this table: those loads match rows by these columns' values. Omit (the default) to declare no key; the table can still be loaded with replace and append, but key-based modes are then rejected.
  • name stringrequired
{
  "key": [
    "string"
  ],
  "name": "string"
}

Response: 201 Table added

{
  "connection_id": "string",
  "schema": "string",
  "table": "string"
}

Errors: 400 Invalid identifier, 404 Database or schema not found, 409 Table already exists

Load database table from upload or query result

Endpoint: POST /v1/databases/{database_id}/schemas/{schema}/tables/{table}/loads

Path parameters:

  • database_id string — Database ID
  • schema string — Schema name
  • table string — Table name

Request body:

  • async boolean — When true, run the load as a background job and return a job ID to poll instead of blocking until it finishes. Recommended for large uploads, which can take longer than an HTTP request should stay open.
  • async_after_ms integer,null — If set (requires async = true), wait up to this many milliseconds for the load to finish: if it completes in time the full result is returned (200), otherwise a 202 with a job ID to poll. Must be between 1000 and the server maximum; a value out of that range, or set without async = true, is rejected with 400.. Min: 1000
  • format string,null — File format of the upload: "csv", "json", or "parquet". Optional — when omitted, the format is auto-detected from the upload's Content-Type and, failing that, from the file contents. Provide it explicitly to override detection or when the contents are ambiguous. "json" expects newline-delimited JSON (one object per line), not a JSON array. Only applies to upload_id; query results are always parquet.
  • key array,null — Key columns identifying rows for "delete", "update", and "upsert" loads — the columns whose values decide which existing row an incoming row removes, updates, or replaces. Omit to use the key the table was created with. Keep the key consistent across loads of the same table: changing it re-targets which rows are matched. Ignored for "replace" and "append".
  • mode stringrequired. How the data is applied: "replace" overwrites the table's contents, "append" inserts the new rows on top of the existing data.
  • result_id string,null — ID of a persisted query result (see GET /v1/results/{result_id}) to publish as the table's contents. The result is copied into the table, so the table keeps its data even after the result expires. A result can be loaded into any number of tables. Provide either this or upload_id, not both.
  • upload_id string,null — ID of a previously-staged upload (see POST /v1/files). The upload is claimed atomically; concurrent loads against the same upload_id return 409. Provide either this or result_id, not both.
{
  "async": true,
  "async_after_ms": null,
  "format": null,
  "key": null,
  "mode": "string",
  "result_id": null,
  "upload_id": null
}

Response: 200 Table loaded

{
  "arrow_schema_json": "string",
  "connection_id": "string",
  "row_count": 0,
  "schema_name": "string",
  "table_name": "string"
}

Response: 202 Upload load accepted and running in the background; poll the returned job for status and result

{
  "id": "string",
  "status": "pending",
  "status_url": "string"
}

Errors: 400 Invalid request (bad mode, both or neither of upload_id/result_id, format combined with result_id, invalid identifier, bad parquet, or the result failed to compute), 404 Database, upload, or result not found, or the table was deleted, 409 Upload already consumed or in flight, the result is still being computed, or the incoming data changes a column's type incompatibly (only widening to a larger compatible type can be applied automatically); the existing data is unchanged and remains queryable

Usage

Get workspace usage snapshot

Endpoint: GET /v1/usage

Query parameters:

  • since string — Billing period start (ISO-8601). Defaults to the start of the current UTC calendar month when omitted.

Response: 200 Workspace usage snapshot

{
  "bytes_scanned": 0,
  "query_count": 0,
  "since": "2026-01-01T00:00:00Z",
  "storage_bytes": 0,
  "storage_captured_at": null
}

Error responses

All endpoints may return error responses in this format:

{
  "message": "Error description",
  "status": 400
}

Rate Limiting

API requests are subject to rate limiting. When rate limits are exceeded, the API returns a 429 Too Many Requests response with a Retry-After header.