# Tables and rows with the API

> Create tables from columns or premades, change columns, and add, change, delete, filter and import rows with an Avox API key.

Source: https://docs.theavoxai.com/avox-api/tables

Create and change [Tables](https://docs.theavoxai.com/tables) and their rows from your own code or an AI assistant. Every
change is checked exactly as in the dashboard: column types, required and unique columns, your
plan's tables and rows per table. Needs `tables:read` / `tables:write` for tables and columns,
and `rows:read` / `rows:write` for rows. Basics (keys, paging, versions, retries, limits):
[Builder API quick start](https://docs.theavoxai.com/avox-api/builder).

The JavaScript examples use the `avox()` helper from [Builder API reference](https://docs.theavoxai.com/avox-api/reference).

## Tables

A table's fields: `id`, `name`, `slug` (its API name, usable instead of the id in every path),
`description`, `icon`, `color`, `kind` (`list`, `calendar`, `catalog`, `lookup`, `custom`),
`status` (`active` or `archived`), `premadeId`, `rowCount`, `columnCount`, `usedByCount`,
`createdAt`, `updatedAt`, `archivedAt`, `etag`, and with a single table, `settings` and `columns`.

A column's fields: `id`, `key` (its API name), `name`, `type` (see
[Column types](https://docs.theavoxai.com/tables/column-types); `GET /api/v1/catalog` lists them), `required`, `unique`,
`defaultValue`, `options` (choices, currency, decimals, formula expression...), `description`,
`position`, `hidden`.

### GET /api/v1/tables

Active and archived tables, newest change first, with `limits` (`maxTables`, `usedTables`,
`maxRowsPerTable`). Query: `status` (`active` or `archived`), `limit` (1 to 100, default 50),
`cursor`.

**curl**

```bash
curl "https://api.avox.com/api/v1/tables?status=active" -H "Authorization: Bearer $AVOX_API_KEY"
```

**JavaScript**

```js
const { data, next_cursor } = await avox("/tables?status=active");
```

### POST /api/v1/tables

Creates a table, in one of two ways. Counts against your plan's tables; send an
`Idempotency-Key`.

**From a premade table** (ids in `GET /api/v1/catalog`): `premadeId`, and optionally `name`,
`description`, `withSamples` (add the premade's sample rows) and `currency` (3 letters, for money
columns).

**From your own columns:** `name`, `columns` (1 to 50, each with a `key`, `name` and `type`, and
optionally `required`, `unique`, `defaultValue`, `options`, `description`, `hidden`), and
optionally `description`, `icon`, `color`, `kind`, `currency`. Keys are small letters, numbers
and `_`, starting with a letter; names and keys must be different from each other. A choice
column's options each need an `id` (`o` and 6 to 12 small letters or numbers, unique in the
column), a `label` and a `color` (`gray`, `brand`, `coral`, `sky`, `violet`, `emerald`, `amber`,
`rose`).

**curl**

```bash
curl -X POST https://api.avox.com/api/v1/tables \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5b0f0c8e-9d7a-4f43-8a1e-6c2b3d4e5f70" \
  -d '{
    "name": "Orders",
    "kind": "list",
    "columns": [
      { "key": "order_no", "name": "Order number", "type": "text", "required": true, "unique": true },
      { "key": "customer", "name": "Customer", "type": "phone" },
      { "key": "status", "name": "Status", "type": "select", "options": { "choices": [
        { "id": "oplaced01", "label": "Placed", "color": "gray" },
        { "id": "oshipped1", "label": "Shipped", "color": "sky" },
        { "id": "odeliver1", "label": "Delivered", "color": "emerald" }
      ] } },
      { "key": "total", "name": "Total", "type": "currency", "options": { "currency": "INR" } }
    ]
  }'
```

**JavaScript**

```js
const { ok, data, error } = await avox("/tables", {
  method: "POST",
  headers: { "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({ premadeId: "appointments", withSamples: false }),
});
```

Answers `201` with `{ table, samplesAdded }` and the table's `ETag`. Errors you may see:
`plan_limit_reached` (all your plan's tables are in use), `invalid_payload` (a column's key,
name or type; `details` points at it, like `columns[2].key`), `name_taken`.

### GET /api/v1/tables/\{tableId\}

One table with its columns. `tableId` is the id or the API name.

**curl**

```bash
curl https://api.avox.com/api/v1/tables/orders -H "Authorization: Bearer $AVOX_API_KEY"
```

**JavaScript**

```js
const { data } = await avox("/tables/orders");
const statusColumn = data.table.columns.find((c) => c.key === "status");
```

```json
{
  "ok": true,
  "request_id": "req_...",
  "data": {
    "table": {
      "id": "0b6c...", "name": "Orders", "slug": "orders", "kind": "list", "status": "active", "rowCount": 0,
      "etag": "\"Zx3m...\"",
      "columns": [{ "id": "5e1a...", "key": "order_no", "name": "Order number", "type": "text", "required": true, "unique": true, "defaultValue": null, "options": {}, "position": 0, "hidden": false }]
    }
  }
}
```

### PATCH /api/v1/tables/\{tableId\}

Changes `name`, `description`, `icon`, `color`, `kind`, `slug` (the API name: small letters,
numbers and `_`) and `settings`. Archived tables can be renamed. Send `If-Match` with the
table's ETag to be sure nobody changed it since you read it.

**curl**

```bash
curl -X PATCH https://api.avox.com/api/v1/tables/orders \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "Zx3m..."' \
  -d '{ "name": "Online orders", "description": "Orders from the website" }'
```

**JavaScript**

```js
await avox(`/tables/${table.id}`, { method: "PATCH", headers: { "If-Match": table.etag }, body: JSON.stringify({ name: "Online orders" }) });
```

### DELETE /api/v1/tables/\{tableId\}

Deletes the table, its rows and its "Used by" links. While Flows, automations or templates use
it, the answer is `in_use` with the list in `details`; add `?force=true` to delete anyway (they
stop finding it). Ask the person first.

**curl**

```bash
curl -X DELETE https://api.avox.com/api/v1/tables/orders -H "Authorization: Bearer $AVOX_API_KEY"
```

**JavaScript**

```js
await avox("/tables/orders", { method: "DELETE" });
```

### POST /api/v1/tables/\{tableId\}/archive

Archives a table: it can still be read (Flows keep working), but not changed, and it still
counts against your plan. When it's in use the answer is `in_use`; send `{ "force": true }` to
archive anyway (those uses are marked as needing attention).

**curl**

```bash
curl -X POST https://api.avox.com/api/v1/tables/orders/archive -H "Authorization: Bearer $AVOX_API_KEY"
```

**JavaScript**

```js
await avox("/tables/orders/archive", { method: "POST", body: JSON.stringify({ force: false }) });
```

### POST /api/v1/tables/\{tableId\}/restore

Makes an archived table active again (and clears the "needs attention" mark its archiving set).

**curl**

```bash
curl -X POST https://api.avox.com/api/v1/tables/orders/restore -H "Authorization: Bearer $AVOX_API_KEY"
```

**JavaScript**

```js
await avox("/tables/orders/restore", { method: "POST" });
```

## Columns

### POST /api/v1/tables/\{tableId\}/columns

Adds a column: `column` (`name`, `type`, and optionally `key`, `required`, `unique`,
`defaultValue`, `options`, `description`, `hidden`), and optionally `afterColumn` or
`beforeColumn` (an id or key; else it goes last). Up to 50 columns per table.

**curl**

```bash
curl -X POST https://api.avox.com/api/v1/tables/orders/columns \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "column": { "key": "delivered_on", "name": "Delivered on", "type": "date" }, "afterColumn": "status" }'
```

**JavaScript**

```js
await avox("/tables/orders/columns", { method: "POST", body: JSON.stringify({ column: { name: "Notes", type: "long_text" } }) });
```

### PATCH /api/v1/tables/\{tableId\}/columns/\{columnId\}

Changes a column (`columnId` is its id or key): `name`, `key`, `type`, `options`, `required`,
`unique`, `defaultValue`, `description`, `hidden`, and `timeZone` (for converting dates and
times). Changing the type converts every value. When some values won't fit the new type, or
Flows, automations or templates use the column, the answer is `confirmation_needed` with how
many values would be emptied and what uses it; send the same change with `"confirm": true` to go
ahead. Making a column unique is refused (`rows_invalid`, with examples) while values repeat.
Removing a choice from `options.choices` clears it from the cells that had it.

**curl**

```bash
curl -X PATCH https://api.avox.com/api/v1/tables/orders/columns/total \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "required": true }'
```

**JavaScript**

```js
await avox("/tables/orders/columns/customer", { method: "PATCH", body: JSON.stringify({ type: "text", confirm: true }) });
```

The answer may carry `warnings`: rows with no value in a column just made required
(`empty_required`), or a column that couldn't stay unique after a type change (`unique_dropped`).

### DELETE /api/v1/tables/\{tableId\}/columns/\{columnId\}

Deletes a column and its cells. When Flows, automations, templates or formula columns use it,
the answer is `in_use`; add `?force=true` to delete anyway (they're marked as needing attention).
A table keeps at least one column.

**curl**

```bash
curl -X DELETE "https://api.avox.com/api/v1/tables/orders/columns/notes?force=true" -H "Authorization: Bearer $AVOX_API_KEY"
```

**JavaScript**

```js
await avox("/tables/orders/columns/notes", { method: "DELETE" });
```

## Rows

A row's fields: `id`, `seq` (its number), `values` (by column key: select columns as their
labels, multiple choice as a list of labels, auto numbers as shown), `source` (where it was made:
`dashboard`, `api`, `flow`, `automation`, `import`...), `createdAt`, `updatedAt`, `etag`.

Writing: send values by column key. Values are checked by each column's type exactly as typed in
the dashboard (a date as `2026-11-02`, a phone with its country code, a choice by its label).
Formula and auto number columns are filled in by Avox. Required columns need a value on every
new row, and unique columns refuse a value another row has. Every write request is **all or
nothing**: one bad row, or going over your plan's rows per table, and nothing is saved.

### GET /api/v1/tables/\{tableId\}/rows

Query: `filters` (a JSON array of `{ "column", "op", "value" }`; `column` is a key, name or id;
ops per type on [Column types](https://docs.theavoxai.com/tables/column-types): `is`, `is_not`, `contains`,
`not_contains`, `starts_with`, `is_empty`, `not_empty`, `gt`, `gte`, `lt`, `lte`, `has_any`,
`has_all`, `is_true`, `is_false`), `sort` (`key:asc` or `key:desc`, up to 3, comma separated;
`_created`, `_updated` and `_seq` work too), `q` (search), `limit` (1 to 500, default 100),
`cursor`. Answers `{ rows, total }`.

**curl**

```bash
curl -G https://api.avox.com/api/v1/tables/orders/rows \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  --data-urlencode 'filters=[{"column":"status","op":"is","value":"Shipped"}]' \
  --data-urlencode 'sort=total:desc' \
  --data-urlencode 'limit=50'
```

**JavaScript**

```js
const filters = encodeURIComponent(JSON.stringify([{ column: "status", op: "is", value: "Shipped" }]));
let cursor = null;
do {
  const page = await avox(`/tables/orders/rows?filters=${filters}&limit=100${cursor ? `&cursor=${cursor}` : ""}`);
  for (const row of page.data.rows) console.log(row.values.order_no);
  cursor = page.next_cursor;
} while (cursor);
```

### POST /api/v1/tables/\{tableId\}/rows

Adds up to 1,000 rows: `{ "rows": [ { <key>: value } ], "timeZone"?: "Asia/Kolkata" }`. Send an
`Idempotency-Key` so a retry never adds them twice. Answers `201` with `{ rows, insertedIds }`.

**curl**

```bash
curl -X POST https://api.avox.com/api/v1/tables/orders/rows \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 1f4e7b2a-8c3d-4e5f-9a0b-1c2d3e4f5a6b" \
  -d '{ "rows": [
        { "order_no": "A-1001", "customer": "+919876543210", "status": "Placed", "total": 1499 },
        { "order_no": "A-1002", "customer": "+919812345678", "status": "Shipped", "total": 650 }
      ] }'
```

**JavaScript**

```js
const { ok, data, error } = await avox("/tables/orders/rows", {
  method: "POST",
  headers: { "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({ rows: [{ order_no: "A-1003", status: "Placed", total: 320 }] }),
});
if (!ok && error.code === "rows_invalid") console.log(error.details); // [{ path: "rows[0].total", message: "...", code: "..." }]
```

A refused request names each problem: `rows_invalid` with `details` like
`{ "path": "rows[1].order_no", "code": "unique", "message": "Another row already has this value" }`,
or `plan_limit_reached` when the rows don't fit in the table on your plan.

### PATCH /api/v1/tables/\{tableId\}/rows

Changes up to 1,000 rows: `{ "rows": [ { "id", "values": { <key>: value }, "updatedAt"? } ] }`.
Only the values you send change. With `updatedAt` (the row's as you read it), the whole request
is refused with `version_conflict` if any of those rows changed since; read them again and retry.

**curl**

```bash
curl -X PATCH https://api.avox.com/api/v1/tables/orders/rows \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rows": [ { "id": "7a1b...", "values": { "status": "Delivered" }, "updatedAt": "2026-10-23T08:15:30.123456+00:00" } ] }'
```

**JavaScript**

```js
await avox("/tables/orders/rows", { method: "PATCH", body: JSON.stringify({ rows: [{ id: row.id, values: { status: "Delivered" }, updatedAt: row.updatedAt }] }) });
```

### POST /api/v1/tables/\{tableId\}/rows/delete

Deletes up to 1,000 rows: `{ "rows": [ { "id", "updatedAt"? } ] }`, all or nothing. Deleted rows
can be brought back from the dashboard for 30 days.

**curl**

```bash
curl -X POST https://api.avox.com/api/v1/tables/orders/rows/delete \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rows": [ { "id": "7a1b..." } ] }'
```

**JavaScript**

```js
await avox("/tables/orders/rows/delete", { method: "POST", body: JSON.stringify({ rows: ids.map((id) => ({ id })) }) });
```

### GET /api/v1/tables/\{tableId\}/rows/\{rowId\}

One row, with its `ETag`.

**curl**

```bash
curl https://api.avox.com/api/v1/tables/orders/rows/7a1b... -H "Authorization: Bearer $AVOX_API_KEY"
```

**JavaScript**

```js
const { data } = await avox(`/tables/orders/rows/${rowId}`);
```

### PATCH /api/v1/tables/\{tableId\}/rows/\{rowId\}

Changes one row: `{ "values": { <key>: value } }`. With `If-Match` (the row's ETag) it's refused
when the row changed since.

**curl**

```bash
curl -X PATCH https://api.avox.com/api/v1/tables/orders/rows/7a1b... \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "values": { "status": "Shipped" } }'
```

**JavaScript**

```js
await avox(`/tables/orders/rows/${row.id}`, { method: "PATCH", headers: { "If-Match": row.etag }, body: JSON.stringify({ values: { status: "Shipped" } }) });
```

### DELETE /api/v1/tables/\{tableId\}/rows/\{rowId\}

Deletes one row (`If-Match` optional).

**curl**

```bash
curl -X DELETE https://api.avox.com/api/v1/tables/orders/rows/7a1b... -H "Authorization: Bearer $AVOX_API_KEY"
```

**JavaScript**

```js
await avox(`/tables/orders/rows/${rowId}`, { method: "DELETE" });
```

## Import a CSV

### POST /api/v1/tables/\{tableId\}/rows/import

Imports CSV text (comma, semicolon or tab separated; up to 10,000 rows and 8 MB):

- `csv`: the text.
- `mode`: `dry_run` (default: reports what would happen, writes nothing) or `commit`.
- `header`: the first line is the headings (default `true`).
- `columns`: file heading to column key, `"skip"`, or `{ "new": { "name", "type" } }`. Headings
  you don't map are matched to column keys and names; the rest are skipped (listed in
  `warnings`).
- `matchColumn`: a unique column key; rows whose value already exists are updated instead of
  added.
- `addChoices`, `skipInvalid`, `timeZone`.

Run a dry run first, read `report` (`rows`, `inserts`, `updates`, `skipped`, `problems`,
`issues` with the file row and column of each problem, `room` left in the table), fix what it
says, then commit. A commit is checked again first and refused (`import_invalid`) when rows have
problems, unless `skipInvalid` is `true`. Test keys may run dry runs.

**curl**

```bash
curl -X POST https://api.avox.com/api/v1/tables/orders/rows/import \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mode": "dry_run", "matchColumn": "order_no",
        "csv": "Order number,Status,Total\nA-1001,Delivered,1499\nA-1004,Placed,899" }'
```

**JavaScript**

```js
const csv = await fs.promises.readFile("orders.csv", "utf8");
const dry = await avox("/tables/orders/rows/import", { method: "POST", body: JSON.stringify({ csv, mode: "dry_run" }) });
if (dry.data.report.problems === 0) {
  await avox("/tables/orders/rows/import", { method: "POST", headers: { "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ csv, mode: "commit" }) });
}
```

## Errors you might see

| Code | What to do |
| --- | --- |
| [`rows_invalid`](https://docs.theavoxai.com/api/errors#rows_invalid) | Fix each `details[].path` (row and column) and send the request again. |
| [`plan_limit_reached`](https://docs.theavoxai.com/api/errors#plan_limit_reached) | Archive or delete what you don't need, or move to a bigger plan. |
| [`not_active`](https://docs.theavoxai.com/api/errors#not_active) | The table is archived: restore it first. |
| [`in_use`](https://docs.theavoxai.com/api/errors#in_use) | Tell the person what uses it; send `force` only if they agree. |
| [`confirmation_needed`](https://docs.theavoxai.com/api/errors#confirmation_needed) | Tell the person what would be emptied; send `confirm: true` only if they agree. |
| [`version_conflict`](https://docs.theavoxai.com/api/errors#version_conflict) | Read the item again and retry with its new ETag or `updatedAt`. |
| [`import_invalid`](https://docs.theavoxai.com/api/errors#import_invalid) | Fix the file rows the report lists, or send `skipInvalid: true`. |

## Related
