Reference

The Base REST API

One bearer key, two scopes, and the same write path the app uses — validation, casting, history, recalc and webhooks all fire whether a row arrives from a person, an agent or a script.

Authentication

A bearer key, scoped read or write

Create a key in the app under workspace settings. It is shown once and stored only as a hash.

curl https://bips.ai/v1/bases \
  -H "Authorization: Bearer $BASE_KEY"
tables:read
Every GET, plus POST /query — which is read-only however it is spelled.
tables:write
Values: rows, cells, comments, automations. Everything that changes what a table holds.
schema:write
Shape: tables, columns and views — and the history rewinds that can move them.

Give a sync job tables:write and nothing else. It can fill rows all day and cannot drop a table, rename a column or undo its way through your schema. Adding columns during an import needs schema:write too, which is why create_missing_columns is refused without it.

Keys minted before this split kept the shape access they already had — a credential in use is never quietly narrowed. Mint a new key to get the narrow one.

A missing key and an insufficient scope both answer 404: a key learns nothing about a base it may not touch. Exceeding the rate limit — 120 requests a minute per key — answers 429 with a Retry-After header.

Conventions

Cells, keys and pages

A row's values live in cells, keyed by column key — a slug derived from the label when the column is created, and stable afterwards. Renaming a column does not move its key, so an integration written today keeps working after someone tidies the headers.

{
  "id": "9f1c…", "source": "api",
  "cells": { "name": "Ada", "amount": 499, "total": 998 },
  "created_at": "…", "updated_at": "…"
}

Computed columns — formula, lookup, rollup, count, reverse-link — are evaluated on read and merged into cells alongside the stored ones. They are never writable.

Pagination

List endpoints take limit (1–500, default 100) and offset, and answer with the page, the total, and the next offset — or null when there is no next page. Paging happens in the database, not after the fact.

Errors

Flat, machine-first, one snake-case code per failure: not_found, invalid_request, invalid_cell (with field), invalid_options, forbidden_columns (with fields), rate_limited. Batch failures add rows_index and return the rows that did land.

Schema

Tables and columns

EndpointDoes
Bases
GET /v1/basesEvery base in the workspace.
POST /v1/basesCreate a base — optionally copying an Airtable one into it, with airtable_token and airtable_base_id. schema:write
GET /v1/bases/:base/overviewTables with row counts and SQL view names.
Tables
GET …/tablesList.
GET …/tables/:tOne table and its columns.
POST …/tablesCreate — blank, from a template, or from imported data. schema:write
PATCH …/tables/:tRename, describe. schema:write
DELETE …/tables/:tArchive. History is kept. schema:write
Columns — all schema:write
POST …/tables/:t/columnsAdd a column of any of the 25 field types.
PATCH …/tables/:t/columns/:cRelabel, change options.
DELETE …/tables/:t/columns/:cArchive. Cell data is kept.
POST …/tables/:t/columns/reorderReorder by id.
GET …/tables/:t/columns/:key/link-choicesRows a link column may point at.
Rows

One row, or five hundred

A batch is one round trip, one history entry and one webhook — not five hundred of each.

EndpointDoes
GET …/tables/:t/rowsA page of rows. limit, offset.
GET …/tables/:t/rows/:rOne row.
GET …/tables/:t/display?viewId=Rows through a saved view — its filters, sorts, grouping and hidden columns applied server-side.
POST …/tables/:t/rowsCreate one.
POST …/tables/:t/rows/batchCreate up to 500.
PATCH …/tables/:t/rows/:rMerge cells into one row.
PATCH …/tables/:t/rows/batchMerge into up to 500.
POST …/tables/:t/rows/:r/duplicateCopy a row below itself.
POST …/tables/:t/rows/reorderMove rows.
DELETE …/tables/:t/rows/:rArchive one row.
curl -X POST "$BASE/v1/bases/$B/tables/$T/rows/batch" \
  -H "Authorization: Bearer $BASE_KEY" -H "content-type: application/json" \
  -d '{"rows":[{"cells":{"name":"Ada"}},{"cells":{"name":"Grace"}}]}'

Rows written this way are stamped source: "api", which is what automation payloads report and what the activity feed shows. Deletes are soft: the row is archived, and the change log can put it back.

Queries

Ask in SQL instead of paging

Every table is exposed as a read-only SQLite view named t_<slug>, so aggregates, joins and searches are one request rather than a download and a loop.

curl -X POST "$BASE/v1/bases/$B/query" \
  -H "Authorization: Bearer $BASE_KEY" -H "content-type: application/json" \
  -d '{"sql":"SELECT status, COUNT(*) n FROM t_orders GROUP BY status"}'

A single SELECT or WITH, nothing else, enforced in four independent layers and run inside a transaction that always rolls back. limit defaults to 200 and caps at 1,000.

Computed columns are not in the views. Formula, lookup, rollup, count and reverse-link values are evaluated by the formula engine on read, and SQLite cannot call it. Read those through the row endpoints. GET …/tables/:t reports each column's sql_type so you can discover the shape rather than guess it.

Around the data

Views, comments, history

EndpointDoes
Views — all schema:write
POST …/tables/:t/viewsCreate a grid, calendar, gallery, gantt, chart, pivot, kanban or list view.
PATCH …/tables/:t/views/:vChange filters, sorts, grouping, colours, hidden columns.
DELETE …/tables/:t/views/:vRemove one. A table keeps at least one.
Comments
GET …/tables/:t/rows/:r/commentsA row's thread.
POST …/tables/:t/rows/:r/commentsAdd a comment.
History
GET …/tables/:t/historyUndo tree, saved versions, recent changes.
GET …/tables/:t/rows/:r/historyField-level before/after for one row.
GET …/tables/:t/asofThe table as it stood at a moment.
POST …/tables/:t/undoUndo the last change unit. schema:write
POST …/tables/:t/redoRedo along the newest branch. schema:write
POST …/tables/:t/gotoJump to any node of the undo tree. schema:write
POST …/tables/:t/save-versionName a snapshot.
POST …/tables/:t/restoreRestore a saved version. schema:write
Data in and out

Import and export

EndpointDoes
POST …/tablesWith mode: "import", creates a table from the data.
POST …/tables/:t/importAppends into an existing table, matching headers to columns. create_missing_columns needs schema:write.
POST …/tables/:t/pasteWrites a rectangle of cells.
GET …/tables/:t/export.csvThe whole table as CSV.

Import accepts CSV, TSV, JSON, NDJSON, a markdown table or XLSX — inline in data, as a base64 data: URL, or as a url for Base to fetch (public hosts only, 5 MB cap, every redirect re-checked). Types are inferred conservatively; at most 5,000 rows land per call and truncated tells you when the rest did not.

Automations

Signed outbound webhooks

A rule watches a table or a form and POSTs a signed JSON body to your URL when it fires.

EndpointDoes
GET …/automationsList rules with their last delivery status.
POST …/automationsCreate. The signing secret comes back once.
PATCH …/automations/:aRename, retarget, enable or disable.
DELETE …/automations/:aDelete the rule and its log.
POST …/automations/:a/testSend a signed ping now.
GET …/automations/:a/deliveriesThe newest 100 deliveries.
POST …/automations/:a/deliveries/:d/retryRe-send one.
Boundaries

What an API key cannot reach

The API is default-deny: a path that is not on its allowlist answers 404 whatever the key. Two things are held back on purpose.

Forms
Submissions carry hashed submitter IPs, and publishing a form ends in minting a public token that lives outside the base. Forms are driven from the app and from MCP, not from a key.
Base bootstrap
Creating and seeding a base is a control-plane operation, not a data one.

There is a second machine surface. Base's MCP server covers everything here plus forms, and speaks OAuth as well as API keys — so an assistant can drive a base the way a person drives the app. See Base vs Airtable over MCP.

How this API lines up against Airtable's, endpoint by endpoint — including what Base is missing — is written up in the repository as docs/airtable-api-audit.md. Matching Airtable was never the goal; measuring the distance was worth doing anyway.

Start

Mint a key and read a table