REST API
The REST API lives under /api/v1 on your Rowflare instance. It is modeled on the Airtable Web API: the same record shape (id, createdTime, fields), field names as keys, typecast, filterByFormula, sort, view, pageSize and offset.
An OpenAPI 3.1 description of these routes is at rowflare.com/openapi.json (also docs/openapi.json in the repository).
Authentication
Create a personal access token in Settings, API tokens. Send it as a bearer token:
curl -H "Authorization: Bearer $ROWFLARE_TOKEN" https://rowflare.example.com/api/v1/bases
A token acts as the user who created it, with that user’s role in each workspace (viewer tokens can read but not write). Tokens are stored hashed; you see the value once. Delete a token to revoke it.
Bases and schema
GET /api/v1/bases
{ "bases": [{ "id": "appXXXXXXXXXXXXXXXX", "name": "Project tracker", "permissionLevel": "edit" }] }
GET /api/v1/bases/{baseId}/tables returns every table with its fields (id, name, type, options) and views.
Records
Tables can be addressed by id (tbl...) or by name (URL-encoded). Record ids are integers.
List records
GET /api/v1/{baseId}/{table}
| Parameter | Meaning |
|---|---|
pageSize |
1 to 100, default 100 |
offset |
Value of offset from the previous page |
maxRecords |
Stop after this many records |
view |
View id or name: applies its filters and sorts |
sort[0][field], sort[0][direction] |
Sort by field name or id, asc or desc; add sort[1]... for more |
filterByFormula |
A formula; records where it is truthy are returned |
fields[] |
Only return these fields (repeatable) |
returnFieldsByFieldId |
true to key fields by id instead of name |
curl -G -H "Authorization: Bearer $ROWFLARE_TOKEN" \
"https://rowflare.example.com/api/v1/$BASE/Tasks" \
--data-urlencode 'filterByFormula=AND({Status} = "Done", {Estimate} > 5)' \
--data-urlencode 'sort[0][field]=Due' --data-urlencode 'pageSize=50'
{
"records": [
{ "id": 17, "createdTime": "2026-10-13T01:57:43.000Z", "fields": { "Name": "Ship landing page", "Status": "Done", "Estimate": 8 } }
],
"offset": "50"
}
Get one record
GET /api/v1/{baseId}/{table}/{recordId}
Create records
POST /api/v1/{baseId}/{table} with up to 1,000 records:
{ "records": [{ "fields": { "Name": "From the API", "Status": "Review", "Project": ["Website relaunch"] } }], "typecast": true }
A body of { "fields": {...} } creates one record and returns it directly.
Update records
PATCH /api/v1/{baseId}/{table} with { "records": [{ "id": 17, "fields": {...} }] }, or PATCH /api/v1/{baseId}/{table}/{recordId} with { "fields": {...} }. Fields you leave out are unchanged. PUT works the same but clears every writable field you leave out.
Delete records
DELETE /api/v1/{baseId}/{table}?records[]=17&records[]=18 or DELETE /api/v1/{baseId}/{table}/{recordId}.
Cell values
| Field type | Read | Write |
|---|---|---|
| Single line text, long text, email, URL, phone | string | string |
| Number | number | number, or a numeric string with typecast |
| Checkbox | true or absent |
boolean |
| Date | "2026-10-13" or an ISO datetime when the field includes time |
ISO string; with typecast, common formats like 10/13/2026 |
| Single select | option name | option name; with typecast, unknown names create the option |
| Multiple select | array of option names | array of names (or a comma-separated string with typecast) |
| Link to another record | array of record ids | array of record ids; with typecast, primary field values, and unknown values create linked records |
| Attachment | array of { id, filename, size, type, url } |
upload in the app; the API does not accept files yet |
| User | { id, email, name } (array when multiple) |
user id or email of a workspace member |
| Lookup, rollup, formula, created time, last modified time, autonumber | computed value | read-only: writing returns 422 |
Empty cells are omitted from fields, as in Airtable.
Automations
These routes use the session of a signed-in editor or owner (the same requests the automations page makes), not a personal access token. Definitions are stored with table and field ids; see the MCP tools to create them by name.
| Route | What it does |
|---|---|
GET /api/bases/:base/automations |
The automations, each with its last run, and usage: { used, limit } for this month |
POST /api/bases/:base/automations |
One operation: { "op": "create", "name", "config", "enabled"? }, { "op": "update", "id", "name"?, "config"? }, { "op": "delete", "id" }, { "op": "setEnabled", "id", "enabled" } or { "op": "regenerateWebhook", "id" } |
GET /api/bases/:base/automations/:id/runs |
Runs, newest first, 50 per page; ?status=failed filters, ?before=<run id> pages |
POST /api/bases/:base/automations/:id/test |
Runs the automation once with { "recordId" } or a sample { "body" }; writes are real, other automations are not triggered, the run does not count toward the monthly limit |
POST /api/bases/:base/automations/:id/run |
Runs a button automation for { "recordId" } and answers with { id, status, error }; viewers may call it when the automation allows them |
POST /api/bases/:base/runs/:runId/retry |
Queues a failed run again with the automation as it is now (so a fixed step applies); it resumes at the step that failed |
Webhooks
An automation with the trigger “When a webhook is called” gets a URL with a secret token: POST /api/hooks/:baseId/:token. The token is the credential, so no session or API token is needed, and regenerating it from the automation’s menu stops the old URL.
- The body may be JSON or a form (
application/x-www-form-urlencoded), up to 256 KB. Steps read it as{{trigger.body.<path>}}. - The answer is
202 { "runId": 42 }right away; the run happens in the background and shows in the run log. Once a run has succeeded, the log keeps the first 8 KB of a larger body. - 400 for a body that is not valid JSON, 404 for an unknown token or a switched off automation, 413 for a body over 256 KB, 429 after 60 calls in a minute for one base.
curl -X POST https://rowflare.example.com/api/hooks/appXXXXXXXX/TOKEN \
-H "Content-Type: application/json" \
-d '{"name": "Ada Lovelace", "email": "ada@example.com"}'
AI fields
An AI field is a text, long text, single select, multiple select, number, date or checkbox field with options.ai (serviceId, model, prompt with field ids in braces such as {fldXXXX}, imageFieldIds, autoUpdate). Records return its values like any other field of that type. These routes take a session or a personal access token:
| Route | What it does |
|---|---|
GET /api/bases/:base/ai-services |
The workspace’s AI services: { id, name, kind, model, keyRejected, calls }, never keys |
GET /api/bases/:base/tables/:table/ai/:field |
Progress and problems: { queued, done, total, missing, broken, errors: [{ rowId, error }] } (first 100 errors) |
POST /api/bases/:base/tables/:table/ai/:field/generate |
{ "rowIds": [...] } (at most 5,000), { "missingOnly": true } (records never generated and still empty), or {} for the whole column; answers { queued } right away and generates in the background |
POST /api/bases/:base/tables/:table/ai/:field/stop |
Removes the field’s queued cells; answers of calls already running are dropped |
POST /api/bases/:base/ai/preview |
{ tableId, rowId, field: { type, options } }: runs a field as it would be saved on one record and returns { answer, value, error?, prompt, cut } without writing; counts as one call |
Services themselves are managed in the web app (/api/workspaces/:ws/ai-services, owners only).
Errors
Errors return JSON { "error": "message" } with status 400 (bad request), 401 (missing or bad token), 403 (no permission), 404 (unknown base, table or record), 402 (a plan limit was reached, with a limit object; see below) or 422 (invalid field or value).
Plan limits
Self-hosted instances have no plan limits apart from AUTOMATION_MONTHLY_RUNS and AI_WORKERS_MONTHLY_CALLS. A hosted edition can set limits per workspace (bases, records per base, attachment storage, automation runs and Workers AI calls per month). A write that would go over one fails as a whole with status 402:
{ "error": "This plan allows 1,000 records per base; 12 more can be added", "limit": { "key": "records", "limit": 1000, "upgradeUrl": "https://..." } }
key is one of bases, records, attachments, automationRuns, workersAiCalls. New linked records created by typecast count as records. Limits never delete or hide data: over a lowered limit, records stay readable and editable, and deleting records frees room at once. Automation runs over the limit do not fail a request; they are recorded as skipped.
The app’s own API (session cookie) has GET /api/workspaces/{workspaceId}/usage, which returns the plan and the usage against each limit (limit is null when there is none):
{ "plan": { "name": "Self-hosted", "paid": true }, "usage": [{ "key": "bases", "label": "Bases", "used": 4, "limit": null }, { "key": "automationRuns", "label": "Automation runs this month in the busiest base", "used": 120, "limit": 10000 }] }
Account routes
Used by the app’s login pages (JSON bodies; they need no token):
| Route | Body | Answer |
|---|---|---|
POST /api/auth/forgot |
{ "email" } |
Always { "ok": true }; mails a reset link when the account exists. 400 “Email is not set up on this instance” without account emails. |
POST /api/auth/reset |
{ "token", "password" } |
Sets the password (8 characters or more), ends the account’s other sessions and signs this browser in. 400 for an unknown, used or expired link. |
POST /api/auth/verify |
{ "token" } |
Confirms the email address. |
POST /api/auth/verify/resend |
none (signed in) | Mails a new confirmation link; 429 after 5 emails to the address in an hour. |
GET /api/auth/me returns user.emailVerified, and GET /api/config returns accountMail (whether the instance can send these emails).
Limits
- Up to 1,000 records per write request, 100 per page when listing.
filterByFormulais evaluated over the whole table (or view), so it reads every row: fine for tens of thousands of rows, but prefer a view with filters for frequent queries on large tables.- Every request is handled by the base’s Durable Object, which processes requests one at a time; a single base comfortably handles hundreds of requests per second for small writes.