Developers

REST API v1

OpenRows' public, versioned API follows the shape of Airtable's Web API, so integrations and scripts written for Airtable work with few changes. It lives under /api/v1 and is described, with every other route, in the OpenAPI document at GET /api/openapi.json.

Authentication

Send a personal access token (Settings → "Developer: personal access tokens"):

Authorization: Bearer or_pat_…

A token reaches what its owner can reach, narrowed to its scopes and its workspaces or bases. The signed-in session cookie works too (same origin only). Integrations acting for other people register an OAuth app and get or_oat_… access tokens that work the same way: see oauth.md.

Scope Endpoints
schema.bases:read GET /meta/bases, GET /meta/bases/:baseId/tables
schema.bases:write POST /meta/bases/:baseId/tables, POST …/tables/:table/fields, PATCH …/fields/:fieldId; new select options with typecast
data.records:read listing and reading records
data.records:write creating, updating and deleting records
data.recordComments:read / :write listing / posting comments
webhook:manage a base's webhooks (see Webhooks)
automations:read listing and reading a base's automations and their runs
automations:manage turning automations on and off
automations:run triggering webhook-received and button-clicked automations

With Airtable's client

import Airtable from 'airtable';

const base = new Airtable({
  apiKey: process.env.OPENROWS_TOKEN,
  endpointUrl: 'https://app.openrows.io/api',
  apiVersion: '1.0.0', // the client builds `/v<major>/…` URLs
}).base('<base id>');

const records = await base('Projects').select({ filterByFormula: "{Status} = 'Done'" }).all();

test/integration/v1-airtable.test.ts runs the official client (0.12) against the server on every CI run.

With the OpenRows SDK

@openrows/sdk is a typed client generated from these endpoints' contracts: batching to 10, paging, Retry-After on 429 and errors with their request id. It is on npm with provenance: npm install @openrows/sdk.

import { OpenRows } from '@openrows/sdk';

const openrows = new OpenRows({ apiKey: process.env.OPENROWS_TOKEN });
const done = await openrows
  .table('<base id>', 'Projects')
  .select({ filterByFormula: "{Status} = 'Done'" })
  .all();

From n8n

@openrows/n8n-nodes-openrows adds an OpenRows node (records: get, get many, search, create, update, create or update, delete) and an OpenRows Trigger (a signed webhook it registers on activation) to n8n, with a personal access token. In a self-hosted n8n: Settings → Community nodes → Install, @openrows/n8n-nodes-openrows (AGPL-3.0-only, so not an n8n verified node and not on n8n Cloud).

From AI agents (MCP)

AI agents (Claude Code, Cursor, any MCP client) use the API through the MCP server: remote at POST /mcp with the token as a bearer header, or locally with @openrows/mcp-server over stdio (npx -y @openrows/mcp-server, on npm). See mcp.md (releases too) and the /docs/api/mcp page.

Endpoints

Method and path (under /api/v1) Airtable's
GET /meta/bases List bases (permissionLevel: read, comment, edit, create, owner)
GET /meta/bases/:baseId/tables Get base schema
POST /meta/bases/:baseId/tables Create table (the first field is the primary field)
POST /meta/bases/:baseId/tables/:tableIdOrName/fields Create field
PATCH /meta/bases/:baseId/tables/:tableIdOrName/fields/:fieldId Update field (name, description)
GET /:baseId/:tableIdOrName List records
POST /:baseId/:tableIdOrName/listRecords List records, parameters in the body
POST /:baseId/:tableIdOrName Create records (fields, or up to 10 records)
PATCH / PUT /:baseId/:tableIdOrName Update / replace up to 10 records, performUpsert
DELETE /:baseId/:tableIdOrName?records[]=… Delete up to 10 records
GET / PATCH / PUT / DELETE /:baseId/:tableIdOrName/:recordId One record
GET / POST /:baseId/:tableIdOrName/:recordId/comments Comments

Tables are named by id or name, and only inside the base in the path. Cells are keyed by field name, or by field id with returnFieldsByFieldId; writes accept either. Empty cells are left out of answers. Deletes move records to the trash.

Ids

Ids are OpenRows' UUIDs: a base, table, field, view, record or comment id is the same UUID the app and the internal API use. Airtable's prefixes (app…, tbl…, fld…, viw…, rec…, usr…) aren't used; anything that only passes ids through works unchanged.

Cell values (cellFormat=json)

Field Value
Single select the choice name
Multiple select choice names
Link to another record linked record ids
User, created by, last modified by { id, name, email } (email for workspace members only)
Attachment [{ id, filename, size, type, width?, height?, url }]; url needs the same credential and redirects to a short-lived signed URL
Others as in the app's API (numbers, ISO dates, text, booleans)

Field types answer with Airtable's name (multipleRecordLinks, multipleSelects, multilineText, phoneNumber, multipleAttachments, singleCollaborator, …) and ours in openrowsType. Creating a field takes either name; options are OpenRows' (new select choices may be just { name }).

filterByFormula

Formulas run as database queries, never row by row. Supported:

  • AND(…), OR(…), NOT(…), TRUE(), FALSE()
  • a field compared with a value: =, !=, <, <=, >, >= (text, numbers, TRUE(), FALSE(), BLANK() or '', and TODAY() for dates); text comparisons ignore case
  • a field on its own: checked (checkbox), or not empty
  • FIND('text', {Field}) and SEARCH('text', {Field}), as a condition, > 0 or = 0 (both ignore case)
  • IS_BEFORE({Date}, '2026-01-31'), IS_AFTER(…), IS_SAME(…, 'day') (dates compare by day)
  • RECORD_ID() = '<id>'

Anything else (other functions, arithmetic, comparing two fields) answers 422 INVALID_FILTER_BY_FORMULA.

Errors

{ "error": { "type": "INVALID_FILTER_BY_FORMULA", "message": "…", "code": "INVALID_FILTER_BY_FORMULA", "requestId": "…" } }

type is Airtable's error type where one fits (AUTHENTICATION_REQUIRED, INVALID_PERMISSIONS, NOT_FOUND, INVALID_REQUEST_UNKNOWN, INVALID_MULTIPLE_CHOICE_OPTIONS, TOO_MANY_REQUESTS, SERVER_ERROR), code OpenRows' stable code. Statuses are OpenRows': 401, 403 (role, scope, or a table or field edit permission), 404 (missing, or outside your access), 409, 413, 422, 429.

Limits

Pages of at most 100 records, batches of at most 10.

Rate limits are token buckets kept in Postgres, so every server instance shares them: per base, 5 requests a second with a burst of 10 (every caller of the base together), and per personal access token (on every route) or signed-in user (on /api/v1), 50 a second with a burst of 100. Self-hosters change them with API_RATE_LIMIT_BASE, API_RATE_LIMIT_BASE_BURST, API_RATE_LIMIT_TOKEN and API_RATE_LIMIT_TOKEN_BURST (RATE_LIMIT_STORE=memory keeps them per process). Answers carry RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy for the bucket closest to empty; a refusal is 429 TOO_MANY_REQUESTS with Retry-After and the bucket in error.limit (base, token or user). A base the caller can't reach takes nothing from its bucket. Each token's requests per day are shown in Settings.

The rendered reference, with these guides and a "From Airtable" page, is at /docs/api.

Webhooks

Push notifications of record, field, table and form events, signed with HMAC-SHA256, managed under /api/v1/bases/{baseId}/webhooks with the webhook:manage scope: see webhooks.md.

Automations

The automations a base's creators built in the app, under /api/v1/bases/{baseId}/automations. Every route needs the base's creator role and its scope; building and changing automations stays in the app.

Method and path (under /api/v1/bases/:baseId) Scope What
GET /automations automations:read The base's automations: trigger, actions, enabled, last run, next scheduled run (never the webhook URL or its last request)
GET /automations/:automationId automations:read One automation
GET /automations/:automationId/runs?limit=&status= automations:read Its runs of the last 30 days, newest first, with each step's (redacted) input and output
PATCH /automations/:automationId { "enabled": true } automations:manage Turns it on or off. Turning it on makes it run as you, as in the app; one without actions can't be turned on (422)
POST /automations/:automationId/trigger automations:run Queues a run: { "body": … } for a webhook-received automation (any JSON, at most 100 KB, as if POSTed to its URL; {} when left out), { "recordId": "…" } for a button-clicked one (a click on that record's button by you: you need to be able to edit the record, the table's edit permission included). Answers { "queued": true, "triggerId": "…" }; the run shows in /runs once the worker ran it. 409 for an automation that is off or has another trigger

Runs run as the automation's creator, in the worker, under the workspace's monthly runs and outbound limits, exactly as when its own trigger fires.

Edit this page on GitHub