Leaving Airtable? Bring your bases to Artavolo, checked record by record. See how

Airtable-compatible API

Artavolo answers Airtable's REST API at the same paths, so a script, the Airtable.js SDK or an HTTP step written for Airtable keeps working after two changes: the address and the token.

const Airtable = require('airtable')
const base = new Airtable({
  apiKey: process.env.ARTAVOLO_TOKEN,          // an Artavolo API token
  endpointUrl: 'https://artavolo.com',          // instead of https://api.airtable.com
}).base('appXXXXXXXXXXXXXX')                    // the same base id as in Airtable
curl -H "Authorization: Bearer $ARTAVOLO_TOKEN" \
  "https://artavolo.com/v0/appXXXXXXXXXXXXXX/Tasks?filterByFormula=%7BStatus%7D%3D'Done'"

The token is an Artavolo API token (Profile, API tokens), sent as Authorization: Bearer <token> (or Basic auth, or the API-KEY header, as for the data API). Its member acts with their own permissions: roles, table visibility and row rules apply, every write is in the audit log, deletes go to the trash, and webhooks and automations run as for any other change.

Ids

AirtableHere
base app...the Airtable id a base was imported from, or the base's Artavolo code
table tbl...the Airtable id it was imported with, its name, or its Artavolo id
field fld...the Airtable id it was imported with, or its name
record rec...the Airtable id it was imported with; a record made in Artavolo has recAV and its id in base 36 (twelve characters)

So links, scripts and saved record ids made in Airtable keep pointing at the same records after an import.

Routes

MethodPath
GET/v0/{base}/{table}list records
POST/v0/{base}/{table}/listRecordsthe same, with the parameters in the body
GET/v0/{base}/{table}/{record}one record
POST/v0/{base}/{table}{fields} or {records: [{fields}]}, at most 10; typecast is accepted
PATCH/v0/{base}/{table}/{record} or /v0/{base}/{table}{fields} or {records: [{id, fields}]}, at most 10
PUTthe sameas PATCH, and the fields not given are cleared
DELETE/v0/{base}/{table}/{record} or /v0/{base}/{table}?records[]=...at most 10; answers {id, deleted: true} / {records: [...]}
GET/v0/meta/whoamithe token's person: {id, email, name}
GET/v0/meta/basesthe bases the token may open: {bases: [{id, name, permissionLevel}]}
GET/v0/meta/bases/{base}/tables{tables: [{id, name, primaryFieldId, fields: [{id, name, type, options}], views}]}, field types as Airtable names them
POST, GET, DELETE/v0/hooks[/{id}]subscriptions to a table's changes, for Zapier, Make and n8n: see integrations

Listing takes pageSize (at most 100), offset, maxRecords, fields[], sort[0][field] and sort[0][direction], view (by name or id: its filter and sort apply), filterByFormula and returnFieldsByFieldId. For polling, three additions of Artavolo's own: sort[0][field]=CREATED_TIME() or LAST_MODIFIED_TIME() (no time field needed), createdAfter and modifiedAfter (an ISO time, exclusive), and recordMetadata[]=lastModifiedTime, which adds lastModifiedTime next to createdTime. Values are Airtable's JSON cell format: a single select as the option's name, a multiple select as names, links as record ids, attachments as {id, url, filename, size, type}, a collaborator as {id, email, name}, a checkbox true or left out, dates as YYYY-MM-DD or ISO date-times. Empty fields are left out, as Airtable does.

filterByFormula

The part the server can answer exactly: comparisons of a field with a value (=, !=, <, >, <=, >=), AND, OR, NOT, FIND, SEARCH, BLANK(), TRUE(), FALSE(), IS_BEFORE, IS_AFTER, IS_SAME and RECORD_ID(). Anything else answers Airtable's 422 INVALID_FILTER_BY_FORMULA, never a wider list.

Not here yet

performUpsert (422, said plainly: look the records up with filterByFormula, then create or update), changing a schema through the metadata API (it reads only), Airtable's own webhooks API (/v0/bases/{base}/webhooks, with its cursor-based payloads; subscriptions do that job here), record comments through /v0, cellFormat=string.

Errors

As Airtable writes them: {"error": {"type", "message"}} with 401 AUTHENTICATION_REQUIRED, 403 INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND, 404 NOT_FOUND, 422 INVALID_REQUEST_UNKNOWN, UNKNOWN_FIELD_NAME, INVALID_VALUE_FOR_COLUMN, INVALID_FILTER_BY_FORMULA. Over the request limit (600 a minute per token and 3,000 per workspace on the hosted service; see limits) the answer is 429 TOO_MANY_REQUESTS with a Retry-After header, as Airtable's own 429.

Export back to Airtable

GET /api/v2/bases/{code}/tables/{id}/export?format=airtable-csv
GET /api/v2/bases/{code}/tables/{id}/export?format=airtable-json

For anyone who may read the table. The CSV is what Airtable's own CSV import reads: a header of field names, multiple selects joined with commas, a checkbox as checked, links by the linked records' primary values, attachments as filename (url); a cell a spreadsheet would run as a formula starts with an apostrophe. The JSON is the /v0 list shape, every record. Leaving is as easy as arriving.

Tests: tests/airtable_v0_test.php, tests/integrations_test.php (the meta API, polling, subscriptions).

A question these pages do not answer? Write to us and we answer by e-mail, usually within one business day. Contact us