Everything you need to build, organize, and automate your business data.
Datatomically is a no-code database platform that combines the simplicity of a spreadsheet with the power of a relational database. You organize data into bases, which contain tables, which contain records.
Each record has one or more fields — typed columns that define the structure of your data. You can view the same table through multiple views like Grid, Kanban, Calendar, and more.
Start with a template. Head to the Templates page and pick one that fits your use case. You can always customize it after.
Get your first base running in under 5 minutes.
After signing up, you'll land in your workspace. A workspace holds all your bases. You can rename it from the workspace settings panel.
Click "+ New Base" to start from scratch, choose a template, or import a CSV from Airtable, Excel, or Google Sheets.
Click the "+ Field" button in the column header to add a new field. Choose from 20+ field types including text, number, select, date, and linked records.
Click "+ Add record" to add rows. Click any cell to edit inline. Press Tab to move right, Enter to move down.
Open workspace settings and click "Invite members." Set their role: Editor, Commenter, or Viewer.
Every base you can open in Datatomically is also available over a JSON REST API, so you can read, create, update and delete records from scripts, integrations and other systems.
Base URL: https://datatomically.com/api/v1The API uses the same request and response shapes as Airtable's records API (records, fields, offset, filterByFormula, typecast…), so most existing Airtable integrations and client libraries only need the base URL and the token changed.
Create a personal API token in the app under Account → API tokens. Tokens start with dt_ and are shown once — copy it straight away; only a hash is stored. Each token has read and/or write scopes. Pass it as a Bearer token on every request:
Authorization: Bearer dt_YOUR_API_TOKENA token acts as you: it can read every base in workspaces you belong to, and write to bases where you are an owner or editor. Requests without a valid token get 401 AUTHENTICATION_REQUIRED; requests you are not allowed to make get 403 NOT_AUTHORIZED.
Tables and fields can be addressed by id or by name (table names are matched case-insensitively). Two discovery endpoints return everything you need:
GET /api/v1/bases → { "bases": [{ "id", "name", "permissionLevel" }] }
GET /api/v1/bases/{baseId}/tables → { "tables": [{ "id", "name", "primaryFieldId", "fields": [{ "id", "name", "type", "options" }], "views": [{ "id", "name", "type" }] }] }GET https://datatomically.com/api/v1/{baseId}/{tableIdOrName}
Response:
{
"records": [
{
"id": "3f1c2a7e-5b7d-4c1e-9a0f-2d6b8e4c1a55",
"createdTime": "2026-03-15T10:22:00.000Z",
"fields": {
"Name": "Project Alpha",
"Status": "In Progress",
"Points": 5,
"Tags": ["design", "q2"]
}
}
],
"offset": "100"
}Field values are keyed by field name. Empty values (blank text, unchecked checkboxes, empty lists) are omitted, as in Airtable. Only stored fields are returned — computed fields (formula, lookup, rollup, count, autonumber, created/last-modified time) are calculated inside the app and are not part of the API.
Query parameters:
pageSize — records per page, default and maximum 100.offset — cursor from the previous response. Keep requesting with the returned offset until the response has none.maxRecords — stop after this many records in total.view — a view id or name; the view's filters and sorts are applied.filterByFormula — a formula; records where it is truthy are returned (see below).sort[0][field], sort[0][direction] — sort by field name or id, asc (default) or desc. Add sort[1]… for secondary sorts. Explicit sorts override the view's sorts.fields[]=Name&fields[]=Status (or fields=Name,Status) — return only these fields.returnFieldsByFieldId=true — key fields by field id instead of name.# curl
curl "https://datatomically.com/api/v1/{baseId}/Tasks?pageSize=50&view=Grid&sort%5B0%5D%5Bfield%5D=Due%20Date" \
-H "Authorization: Bearer dt_YOUR_API_TOKEN"
// JavaScript (fetch) — walk every page
const base = 'https://datatomically.com/api/v1/{baseId}/Tasks';
const headers = { Authorization: 'Bearer dt_YOUR_API_TOKEN' };
let offset, all = [];
do {
const url = base + (offset ? '?offset=' + offset : '');
const page = await fetch(url, { headers }).then(r => r.json());
all.push(...page.records);
offset = page.offset;
} while (offset);A single record is available at GET /api/v1/{baseId}/{table}/{recordId} (404 NOT_FOUND if it does not exist).
Use the same formula language as formula fields. Reference fields with {Field Name}; a record is returned when the formula evaluates to something truthy. Remember to URL-encode the parameter.
# Open tasks worth more than 3 points
curl -G "https://datatomically.com/api/v1/{baseId}/Tasks" \
-H "Authorization: Bearer dt_YOUR_API_TOKEN" \
--data-urlencode "filterByFormula=AND({Status} = 'Todo', {Points} > 3)"
# Records whose name contains "alpha", ignoring case
--data-urlencode "filterByFormula=FIND('alpha', LOWER({Name}))"
# Records with no due date
--data-urlencode "filterByFormula={Due Date} = BLANK()"
// JavaScript
const q = new URLSearchParams({ filterByFormula: "IS_AFTER({Due Date}, TODAY())", pageSize: 20 });
const { records } = await fetch(`${base}?${q}`, { headers }).then(r => r.json());An invalid formula or an unknown field name returns 422 INVALID_FILTER_BY_FORMULA.
Send { "records": [{ "fields": … }] } (up to 10 records per request) or a single { "fields": … }. Fields can be given by name or id; anything you leave out gets its empty default. Values are coerced to the field type: numbers, booleans for checkboxes, strings for text and dates (ISO 8601), arrays for multiple select, linked records (record ids) and attachments ({ "url": … }).
POST https://datatomically.com/api/v1/{baseId}/Tasks
Content-Type: application/json
{
"records": [
{ "fields": { "Name": "New Project", "Status": "Backlog", "Due Date": "2026-05-01" } },
{ "fields": { "Name": "Another one", "Points": 3 } }
],
"typecast": true
}
Response: { "records": [ { "id": "…", "createdTime": "…", "fields": { … } }, … ] }
# curl
curl -X POST "https://datatomically.com/api/v1/{baseId}/Tasks" \
-H "Authorization: Bearer dt_YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"fields":{"Name":"New Project","Status":"Backlog"}}'
// JavaScript
await fetch(base, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ records: [{ fields: { Name: 'New Project', Status: 'Backlog' } }] }),
});Unknown select options are rejected with 422 INVALID_MULTIPLE_CHOICE_OPTIONS unless you pass "typecast": true, which creates the option in the field.
PATCH merges the fields you send into the record; PUT replaces every editable field (fields you omit are cleared). Both accept up to 10 records.
PATCH https://datatomically.com/api/v1/{baseId}/Tasks
{ "records": [ { "id": "3f1c2a7e-…", "fields": { "Status": "Done" } } ] }
PATCH https://datatomically.com/api/v1/{baseId}/Tasks/{recordId}
{ "fields": { "Status": "Done" } }
PUT …/Tasks or …/Tasks/{recordId} — same bodies, replaces all fields
# curl
curl -X PATCH "https://datatomically.com/api/v1/{baseId}/Tasks/{recordId}" \
-H "Authorization: Bearer dt_YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"fields":{"Status":"Done"}}'DELETE https://datatomically.com/api/v1/{baseId}/Tasks/{recordId}
DELETE https://datatomically.com/api/v1/{baseId}/Tasks?records[]={id1}&records[]={id2} (up to 10; records=id1,id2 also works)
Response: { "records": [ { "id": "…", "deleted": true } ] }offset to page through the rest.filterByFormula and view filters stop reading as soon as a page is full. Sorting reads the whole table, which is sized for the plan limit of 50,000 records per base.X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). Over the limit you get 429 with a Retry-After header.Errors are always JSON with an Airtable-style error object:
{ "error": { "type": "UNKNOWN_FIELD_NAME", "message": "Unknown field name: \"Priorty\"" } }400 INVALID_REQUEST_BODY — body is not valid JSON.401 AUTHENTICATION_REQUIRED — missing, invalid or revoked token.403 NOT_AUTHORIZED — not a member of the workspace, not an editor, or the token lacks the scope.404 NOT_FOUND — unknown route, base, table or record.405 METHOD_NOT_ALLOWED — wrong HTTP method for the route.422 — validation: UNKNOWN_FIELD_NAME, INVALID_VALUE_FOR_COLUMN, INVALID_MULTIPLE_CHOICE_OPTIONS, INVALID_RECORD_ID, INVALID_FILTER_BY_FORMULA, VIEW_NAME_NOT_FOUND, INVALID_REQUEST_BODY (e.g. more than 10 records).422 PLAN_LIMIT_EXCEEDED — the workspace's plan doesn't allow more records in this base.429 TOO_MANY_REQUESTS — rate limit exceeded; wait for Retry-After seconds.500 SERVER_ERROR — something went wrong on our side; retry with backoff.To push data out when something changes, use an automation with the Send a webhook action (see Automations below) — it POSTs JSON to any URL, including Zapier and Make catch hooks. To pull data in, point your Airtable-compatible Zapier/Make step at this API's base URL with your token.
Automations react to changes in a base without any code. Each automation belongs to one base and has a single trigger (when) and one or more actions (then).
{Name} or {Due Date}.Record {Name} was created.{ "event", "base", "table", "record": { "id", "fields": { "Field name": "display value", … } } }.Automations execute inside the Datatomically app while it is open — they are evaluated on the device that made the change. Records changed purely through the REST API, or while nobody has the base open, do not trigger automations. For server-side workflows, poll the REST API or trigger your own webhook from your integration.
Click Run history in the Automations panel to see the last 50 runs for the base — when each automation fired, whether it succeeded, and the error message if an action failed.