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
| Airtable | Here |
|---|---|
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
| Method | Path | |
|---|---|---|
| GET | /v0/{base}/{table} | list records |
| POST | /v0/{base}/{table}/listRecords | the 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 |
| PUT | the same | as 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/whoami | the token's person: {id, email, name} |
| GET | /v0/meta/bases | the 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).