# Databases Source: https://www.hotdata.dev/docs/api-reference/databases Site index: https://www.hotdata.dev/llms.txt 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 `GET /v1/databases` List databases in the workspace, newest first, one page at a time. When no `limit` is given a default page size is applied, so a single call returns at most one page rather than every database. If the response's `has_more` is true, pass its `next_cursor` value back as the `cursor` query parameter to fetch the next page. Pass `search` to return only databases whose name contains that text (case-insensitive). Pass `batch` with the `batch_id` returned by a bulk-creation call to list only that batch's databases. **Query parameters** - `limit` `integer` — Maximum number of databases to return in this page (1–100). Values outside the range are clamped. - `cursor` `string` — Opaque pagination cursor from a previous response's `next_cursor`. - `search` `string` — Case-insensitive substring filter on the database name. When set, only databases whose name contains this text are returned; paging and newest-first ordering are unchanged. - `batch` `string` — List only the databases belonging to one bulk-creation batch, identified by the `batch_id` that call returned. Bulk-created databases also appear in the unfiltered listing alongside every other database; this narrows the listing to one batch. Paging works the same way, but results are ordered by database id rather than newest-first, because every database in a batch is created at once. **Response** `200` — One page of databases - `count` `integer,null` — Number of databases returned in this page. Min: `0` - `databases` `DatabaseSummary`[] — **required** - `created_at` `string,null` — When the database was created. - `default_catalog` `string` — **required**. Name the database's default catalog answers to inside its query scope. - `default_schema` `string` — **required**. Schema that unqualified table names resolve to inside this database's query scope. `main` unless the database declares a single schema or a `default_schema` was set at create time. - `expires_at` `string,null` - `id` `string` — **required** - `name` `string,null` - `has_more` `boolean,null` — Whether more databases exist beyond this page. - `limit` `integer,null` — Page size applied to this response (after clamping to the maximum). Min: `0` - `next_cursor` `string,null` — Opaque cursor for the next page; present only when `has_more` is true. ```json { "count": 0, "databases": [ { "created_at": "2026-01-01T00:00:00Z", "default_catalog": "string", "default_schema": "string", "expires_at": "2026-01-01T00:00:00Z", "id": "string", "name": "string" } ], "has_more": true, "limit": 0, "next_cursor": "string" } ``` ## Create database `POST /v1/databases` Create a new database (a metadata-only grouping). A managed default catalog is auto-created and addressable inside the database as `default` (or the optional `default_catalog` name), with a `main` schema pre-declared so `default.main.` works out of the box. The optional `name` is a free-form display label and is not required to be unique; when omitted, a label derived from the new database's ID is assigned. Optional `default_catalog` overrides the name the default catalog answers to; it must be a valid SQL identifier and may not collide with the reserved catalog names `hotdata` or `information_schema`. Optional `schemas` declares additional schemas/tables on the default catalog at create time; declared tables can be loaded via the standard managed-tables-load endpoint targeting `default_connection_id`. Optional `expires_at` sets when the database expires — accepts either an RFC 3339 timestamp or a relative duration suffixed with `h` (hours), `m` (minutes), or `d` (days), e.g. `24h`, `48h`, `90m`, `7d`. When omitted, 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. **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.
` 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 (`..
`) 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). When omitted, a label derived from the database's ID is assigned. 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` `string` — **required** - `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` `string` — **required** - `partition_by` `TablePartitionKey`[] — Partition keys for this table, applied in order. Omit for no partitioning. Declared when the table is created and fixed thereafter. - `sorted_by` `TableSortKey`[] — Sort keys for this table, applied in order. Omit for no sort order. Declared when the table is created and fixed thereafter. ```json { "schemas": [ { "name": "sales" } ] } ``` **Response** `201` — Database created - `default_catalog` `string` — **required**. Name the database's default catalog answers to inside its query scope (`default` unless overridden at create time). - `default_connection_id` `string` — **required**. Internal id of the connection that backs this database's `default` catalog. Workspace-level connection endpoints (list, get, health, delete, cache purge) refuse to act on this id — it is exposed only for the managed-tables load endpoint (`POST /v1/connections/{id}/schemas/{s}/tables/{t}/loads`) so callers can load data into tables declared at database-create time. Addressing it directly in SQL is not the recommended path — use `default` inside an `X-Database-Id` scope instead. - `default_schema` `string` — **required**. Schema that unqualified table names resolve to inside this database's query scope. `main` unless the database declares a single schema or a `default_schema` was set at create time. - `expires_at` `string,null` — When this database expires. - `id` `string` — **required** - `name` `string,null` ```json { "default_catalog": "string", "default_connection_id": "string", "default_schema": "string", "expires_at": "2026-01-01T00:00:00Z", "id": "string", "name": "string" } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid request | | `500` | Internal server error | ## Create many databases at once `POST /v1/databases/bulk` Create many databases from one template in a single request. The databases are created in the background: the response returns immediately with a batch and a job to poll. The databases are not returned inline. List the ones a batch created with `GET /databases?batch=`; they also appear in the normal database listing alongside every other database. Each database gets a default catalog and schema. Declare tables on all of them by passing `schemas`, in the same shape a single create accepts — a batch of 10,000 declaring one table yields 10,000 databases that each hold that table and are ready to load, with no follow-up call per database. Omit `schemas` and the databases are created empty. Either way, load data into them exactly as you would a database created individually. **Request body** - `count` `integer` — **required**. How many databases to create. Min: `1`. Max: `10000` - `default_catalog` `string,null` — Name the default catalog answers to inside each database, as on a single create. Defaults to `default`. - `default_schema` `string,null` — Schema that unqualified table names resolve to inside each database. - `expires_at` `string,null` — When the created databases expire. Accepts an RFC 3339 timestamp or a relative duration such as `24h`, `90m`, or `7d`. - `idempotency_key` `string,null` — Repeat this value to retry a request safely. A retry carrying a key that was already used returns the original batch — the same `batch_id` and the same databases — instead of creating a second set. The key identifies the request, not its contents: reusing a key with a different `count` or template returns the original batch unchanged rather than reporting a mismatch. Use a fresh key per distinct request. - `name_template` `string,null` — Optional display-label pattern for each database. `{index}` is replaced with the database's zero-based position — for example `tenant-{index}` produces `tenant-0`, `tenant-1`, and so on. When omitted, each database is labelled from its own ID. Labels are not identifiers and are not required to be unique. - `schemas` `DatabaseDefaultSchemaDecl`[] — Schemas and tables to declare on every database in the batch, in the same shape a single create accepts. The declaration applies identically to each database, so a batch of 10,000 declaring one table yields 10,000 databases that each hold that table and are ready to load — with no follow-up call per database. Omitted or empty means each database starts with no tables. - `name` `string` — **required** - `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` `string` — **required** - `partition_by` `TablePartitionKey`[] — Partition keys for this table, applied in order. Omit for no partitioning. Declared when the table is created and fixed thereafter. - `sorted_by` `TableSortKey`[] — Sort keys for this table, applied in order. Omit for no sort order. Declared when the table is created and fixed thereafter. ```json { "count": 100, "default_catalog": "default", "default_schema": "main", "expires_at": "7d", "idempotency_key": "3f6b1c1e-9a2b-4a5f-9a1e-2f0d6a7c8b91", "name_template": "tenant-{index}", "schemas": [ { "name": "sales" } ] } ``` **Response** `202` — Batch accepted and filling in the background - `batch_id` `string` — **required** - `cancel_requested` `boolean` — **required**. True once stopping has been requested. Databases already created are kept; only further creation stops. - `count` `integer` — **required**. How many databases the batch was asked to create. - `created_count` `integer` — **required**. How many exist so far. Advances as the batch fills. - `expires_at` `string,null` - `job_id` `string,null` — Job filling this batch. Poll it for status. - `status_url` `string,null` ```json { "batch_id": "string", "cancel_requested": true, "count": 0, "created_count": 0, "expires_at": "2026-01-01T00:00:00Z", "job_id": "string", "status_url": "string" } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid count, template, or expiry | | `409` | A batch with this idempotency key is already running | ## Get a database batch `GET /v1/databases/bulk/{batch_id}` Fetch a batch by id: how many databases were requested and how many exist so far. Poll this to follow progress. **Path parameters** - `batch_id` `string` — **required**. Batch ID **Response** `200` — The batch - `batch_id` `string` — **required** - `cancel_requested` `boolean` — **required**. True once stopping has been requested. Databases already created are kept; only further creation stops. - `count` `integer` — **required**. How many databases the batch was asked to create. - `created_count` `integer` — **required**. How many exist so far. Advances as the batch fills. - `expires_at` `string,null` - `job_id` `string,null` — Job filling this batch. Poll it for status. - `status_url` `string,null` ```json { "batch_id": "string", "cancel_requested": true, "count": 0, "created_count": 0, "expires_at": "2026-01-01T00:00:00Z", "job_id": "string", "status_url": "string" } ``` **Errors** | Status | Description | | ------ | ----------- | | `404` | Batch not found | ## Delete a database batch `DELETE /v1/databases/bulk/{batch_id}` Stop a batch that is still filling and delete the databases it created, then the batch itself. Only batches whose databases hold no data can be removed this way. Tables that were declared but never loaded do not prevent it, so a batch created with `schemas` stays deletable. If any database in the batch has had data loaded into it, the request is rejected and those databases must be deleted one at a time — removing a database that holds data is per-database work that cannot be batched. **Path parameters** - `batch_id` `string` — **required**. Batch ID **Response** `200` — Batch deleted - `batch_id` `string` — **required** - `deleted_count` `integer` — **required**. How many databases were removed. ```json { "batch_id": "string", "deleted_count": 0 } ``` **Errors** | Status | Description | | ------ | ----------- | | `404` | Batch not found | | `409` | Some of the batch's databases hold loaded data | ## Count databases `GET /v1/databases/count` Return the total number of databases in the workspace. This is the whole-workspace total, not a page size: the `count` field on the listing reports how many rows that one page returned, so totalling a workspace from `GET /v1/databases` means walking every page. Pass `search` to count only databases whose name contains that text (case-insensitive), or `batch` with the `batch_id` returned by a bulk-creation call to count only that batch's databases. The filters mean exactly what they mean on the listing, so a count and a listing given the same filters describe the same set. **Query parameters** - `search` `string` — Case-insensitive substring filter on the database name. When set, only databases whose name contains this text are counted. - `batch` `string` — Count only the databases belonging to one bulk-creation batch, identified by the `batch_id` that call returned. **Response** `200` — Total databases in the workspace - `total` `integer` — **required**. Total databases matching the filters, across all pages. ```json { "total": 0 } ``` ## Get database `GET /v1/databases/{database_id}` Fetch a database by id. The `name` field is a display label only; it is not accepted as an identifier here. **Path parameters** - `database_id` `string` — **required**. Database ID **Response** `200` — Database details - `attachments` `DatabaseAttachmentInfo`[] — **required** - `alias` `string,null` — Alias under which this catalog is reachable inside the database. When `None`, the catalog is reachable by its original connection name. - `connection_id` `string` — **required** - `created_at` `string,null` — When the database was created. - `default_catalog` `string` — **required**. Name the database's default catalog answers to inside its query scope (`default` unless overridden at create time). - `default_connection_id` `string` — **required** - `default_schema` `string` — **required**. Schema that unqualified table names resolve to inside this database's query scope. `main` unless the database declares a single schema or a `default_schema` was set at create time. - `expires_at` `string,null` — When this database expires. - `id` `string` — **required** - `name` `string,null` ```json { "attachments": [ { "alias": "string", "connection_id": "string" } ], "created_at": "2026-01-01T00:00:00Z", "default_catalog": "string", "default_connection_id": "string", "default_schema": "string", "expires_at": "2026-01-01T00:00:00Z", "id": "string", "name": "string" } ``` **Errors** | Status | Description | | ------ | ----------- | | `404` | Database not found | ## Delete database `DELETE /v1/databases/{database_id}` Delete a database and its auto-created default catalog. Attached catalogs are detached (their underlying connections are not deleted). **Path parameters** - `database_id` `string` — **required**. Database ID **Response** `204` — Database deleted **Errors** | Status | Description | | ------ | ----------- | | `404` | Database not found | ## Attach catalog to database `POST /v1/databases/{database_id}/catalogs` Attach an existing connection (catalog) to a database with an optional alias. Inside the database the catalog is reachable as the alias (when set) or its original name. **Path parameters** - `database_id` `string` — **required**. 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` `string` — **required** ```json { "alias": "warehouse", "connection_id": "connk9p34y6n3wd25rq4f5zr37e3p3" } ``` **Response** `204` — Catalog attached **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid request | | `404` | Database or connection not found | | `409` | Catalog already attached or alias collides with an existing attachment | ## Detach catalog from database `DELETE /v1/databases/{database_id}/catalogs/{connection_id}` **Path parameters** - `database_id` `string` — **required**. Database ID - `connection_id` `string` — **required**. Connection ID **Response** `204` — Catalog detached **Errors** | Status | Description | | ------ | ----------- | | `400` | Cannot detach a database's own default catalog | | `404` | Database or attachment not found | ## Fork database `POST /v1/databases/{database_id}/fork` Create a new database that is an independent fork of an existing one. The fork has its own default catalog and contains the same schemas, tables, and data as the source; the source is left unchanged. External catalogs attached to the source are re-attached to the fork. Optional `name` sets the fork's display label; when omitted, the fork takes the source's label followed by a short suffix derived from the fork's own ID, so the two stay distinguishable. Optional `expires_at` sets when the fork expires — accepts an RFC 3339 timestamp or a relative duration suffixed with `h` (hours), `m` (minutes), or `d` (days), e.g. `24h`, `90m`, `7d`. When omitted, a still-future expiry on the source is carried over; otherwise the fork never expires. Any indexes on the source's tables are not carried over. **Path parameters** - `database_id` `string` — **required**. 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 fork takes the source's label followed by a short suffix derived from the fork's own ID, so the two stay distinguishable. A source with no usable label of its own gives a fork named from that ID alone. All fields are optional. Send only the ones you want to set. **Response** `201` — Database forked - `default_catalog` `string` — **required**. Name the database's default catalog answers to inside its query scope (`default` unless overridden at create time). - `default_connection_id` `string` — **required**. Internal id of the connection that backs this database's `default` catalog. Workspace-level connection endpoints (list, get, health, delete, cache purge) refuse to act on this id — it is exposed only for the managed-tables load endpoint (`POST /v1/connections/{id}/schemas/{s}/tables/{t}/loads`) so callers can load data into tables declared at database-create time. Addressing it directly in SQL is not the recommended path — use `default` inside an `X-Database-Id` scope instead. - `default_schema` `string` — **required**. Schema that unqualified table names resolve to inside this database's query scope. `main` unless the database declares a single schema or a `default_schema` was set at create time. - `expires_at` `string,null` — When this database expires. - `id` `string` — **required** - `name` `string,null` ```json { "default_catalog": "string", "default_connection_id": "string", "default_schema": "string", "expires_at": "2026-01-01T00:00:00Z", "id": "string", "name": "string" } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | The source database can't be forked as-is (for example, one of its tables has rows that were individually deleted or updated) | | `404` | Source database not found | ## Add schema to database default catalog `POST /v1/databases/{database_id}/schemas` Declare a new schema (and optionally its tables) on the database's auto-created default catalog after creation. The schema becomes reachable inside the database scope (e.g. `default..
` and `information_schema.schemata`) without the caller naming the database's default connection. Identifiers are normalized to lowercase. **Path parameters** - `database_id` `string` — **required**. Database ID **Request body** - `name` `string` — **required** - `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` `string` — **required** - `partition_by` `TablePartitionKey`[] — Partition keys for this table, applied in order. Omit for no partitioning. Declared when the table is created and fixed thereafter. - `column` `string` — **required**. Column the key reads. - `transform` `string` — **required**. How the value is derived from the column. One of `identity` (the column value itself), `year`, `month`, `day`, or `hour`. - `sorted_by` `TableSortKey`[] — Sort keys for this table, applied in order. Omit for no sort order. Declared when the table is created and fixed thereafter. - `column` `string` — **required** - `direction` `string,null` — `asc` (the default) or `desc`. Null when the table was declared without an explicit direction for this key. - `nulls` `string,null` — Where nulls are placed: `first` or `last`. Defaults to the SQL default for the chosen direction. Null when the table was declared without an explicit placement for this key. ```json { "name": "sales", "tables": [ { "key": [ "order_id" ], "name": "orders" } ] } ``` **Response** `201` — Schema added - `connection_id` `string` — **required**. Connection backing the catalog the schema was added to. For a database default catalog this is the database's `default_connection_id`. - `schema` `string` — **required** - `tables` `string`[] — **required** ```json { "connection_id": "string", "schema": "string", "tables": [ "string" ] } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid identifier | | `404` | Database not found | | `409` | Schema already exists | ## Add table to database default catalog `POST /v1/databases/{database_id}/schemas/{schema}/tables` Declare a new table on an existing schema of the database's default catalog after creation. The table is added empty (declared-but-unloaded) and can be populated via the managed-table load endpoint targeting the default connection. Identifiers are normalized to lowercase. **Path parameters** - `database_id` `string` — **required**. Database ID - `schema` `string` — **required**. 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` `string` — **required** - `partition_by` `TablePartitionKey`[] — Partition keys for this table, applied in order. Omit for no partitioning. Declared when the table is created and fixed thereafter. - `column` `string` — **required**. Column the key reads. - `transform` `string` — **required**. How the value is derived from the column. One of `identity` (the column value itself), `year`, `month`, `day`, or `hour`. - `sorted_by` `TableSortKey`[] — Sort keys for this table, applied in order. Omit for no sort order. Declared when the table is created and fixed thereafter. - `column` `string` — **required** - `direction` `string,null` — `asc` (the default) or `desc`. Null when the table was declared without an explicit direction for this key. - `nulls` `string,null` — Where nulls are placed: `first` or `last`. Defaults to the SQL default for the chosen direction. Null when the table was declared without an explicit placement for this key. ```json { "key": [ "order_id" ], "name": "orders", "partition_by": [ { "column": "created_at", "transform": "day" } ], "sorted_by": [ { "column": "created_at", "direction": "asc", "nulls": "last" } ] } ``` **Response** `201` — Table added - `connection_id` `string` — **required** - `schema` `string` — **required** - `table` `string` — **required** ```json { "connection_id": "string", "schema": "string", "table": "string" } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid identifier | | `404` | Database or schema not found | | `409` | Table already exists | ## Load database table from inline data, upload, or query result `POST /v1/databases/{database_id}/schemas/{schema}/tables/{table}/loads` Publish data as the new contents of a table on the database's default catalog, from one of three sources — provide exactly one. The database-scoped equivalent of the connection-scoped managed-table load — addressed by `database_id`, so no `default_connection_id` is needed. With `data`, CSV text is sent inline in this request, up to 2 MiB; column types are detected from the data unless `columns` declares them, and a larger payload is rejected with 413 and the error code `INLINE_DATA_TOO_LARGE`, at which point the data should be uploaded and loaded by `upload_id` instead. With `upload_id`, a previously-uploaded file is published: CSV, JSON, and Parquet are supported; the format is auto-detected or set via `format`. With `result_id`, a persisted query result is copied into the table, so the table keeps its data even after the result expires. If the target table (or its schema) has not been declared yet, it is created automatically as part of the load — declaring tables up front is optional. `mode` selects how the data is applied: `replace` overwrites the table's contents, `append` inserts the new rows on top of the existing data. Concurrent loads against the same upload return 409. For an upload or inline data, set `async` to run the load in the background and get back a job ID to poll; add `async_after_ms` to wait briefly for it to finish before falling back to a job ID. A `result_id` load runs synchronously. **Path parameters** - `database_id` `string` — **required**. Database ID - `schema` `string` — **required**. Schema name - `table` `string` — **required**. 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. Default: `false` - `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` - `columns` `object,null` — Column types for inline `data`, keyed by column name. Optional — types are detected from the data when omitted. Each value is either a type name (`"VARCHAR"`, `"BIGINT"`, `"DECIMAL(10,2)"`) or an object carrying explicit parameters (`{"type": "DECIMAL", "precision": 10, "scale": 2}`). Supported types: `VARCHAR`, `TEXT`, `STRING`, `CHAR`, `BOOLEAN`, `TINYINT`, `SMALLINT`, `INTEGER`, `BIGINT`, `UTINYINT`, `USMALLINT`, `UINTEGER`, `UBIGINT`, `REAL`, `FLOAT`, `DOUBLE`, `DECIMAL`, `NUMERIC`, `DATE`, `TIME`, `TIMESTAMP`, `TIMESTAMPTZ`, `BINARY`, `BLOB`, `UUID`, and `JSON`. When given, it must name every column in the CSV header and no others. Only valid together with `data`. - `data` `string,null` — The data to load, sent inline in this request instead of being uploaded first — the quickest way to get a small table in. CSV text with a header row, up to 2 MiB. Larger payloads are rejected with `413` and the error code `INLINE_DATA_TOO_LARGE`; upload the file (see `POST /v1/uploads`) and load it by `upload_id` instead. Column types are detected from the data unless `columns` declares them. Provide exactly one of this, `upload_id`, or `result_id`. - `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. With inline `data` the only accepted value is `"csv"` (the default); upload the file and load it by `upload_id` for any other format. Not valid with `result_id` — query results are always parquet. - `idempotency_key` `string,null` — A key of your own that makes this load safe to retry. Send the same key again — after a timeout, a dropped connection, or any answer you did not receive — and the load runs at most once: the retry returns the original result instead of loading the rows a second time. Generate the key before the first attempt (a UUID is a good choice) and reuse it for every retry of that same data. Use a new key for the next batch: sending different data under a key already used returns `409`, and loads nothing. A retry sent while the first attempt is still running also returns `409`, because the table is busy with it; wait and send the same key again. A `409` never means the data was loaded twice — under one key it is loaded once or not at all. Only valid with inline `data`. A load from `upload_id` is already safe to retry — re-send the same `upload_id`. Keys are at most 255 characters. - `key` `string`[] | `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` `string` — **required**. 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 exactly one of this, `upload_id`, or `data`. - `upload_id` `string,null` — ID of a previously-staged upload (see `POST /v1/uploads`). The upload is claimed atomically; concurrent loads against the same `upload_id` return 409. Provide exactly one of this, `result_id`, or `data`. ```json { "data": "order_id,customer_id,amount\n1001,42,1999\n1002,7,4550\n", "mode": "replace" } ``` **Response** `200` — Table loaded - `arrow_schema_json` `string` — **required**. Schema of the loaded table, as JSON. - `connection_id` `string` — **required** - `row_count` `integer` — **required**. Total number of rows in the table after the load. Min: `0` - `schema_name` `string` — **required** - `table_name` `string` — **required** ```json { "arrow_schema_json": "string", "connection_id": "string", "row_count": 0, "schema_name": "string", "table_name": "string" } ``` **Response** `202` — Load accepted and running in the background; poll the returned job for status and result - `id` `string` — **required**. Job ID for status polling. - `status` `JobStatus` — **required**. Current status of a background job. - `status_url` `string` — **required**. URL to poll for job status. ```json { "id": "string", "status": "pending", "status_url": "string" } ``` **Errors** | Status | Description | | ------ | ----------- | | `400` | Invalid request (bad mode, none or several of `upload_id`/`result_id`/`data`, `format` combined with `result_id`, `columns` without `data`, a non-CSV inline `format`, unparseable inline data, 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 | | `413` | Inline `data` is over the 2 MiB limit (error code `INLINE_DATA_TOO_LARGE`); upload the data and load it by `upload_id` instead |