Guides/Data API

Data API

HTTP contract for workspace tables — records, schema types, and FILE uploads. Not SQL, not Prisma.

Data API

The Data API is the public HTTP contract for workspace tables. External apps read and write live Onnie records with a Bearer key. Rows show up in the grid. Automations on record.created / record.updated can mint Work.

It is not SQL, not Prisma, and not a second Postgres. There is no published npm client. Types come from live columns via GET /schema.ts.

This API
Tables and records

Catalog, schema, CRUD, FILE upload. JSON over HTTPS. The write gate stays Onnie.

Automations
Work mint

Rules on record created or updated mint Work. This API has no mint_work and no task routes.

What it is not

Not a database product

You never get a connection string, PostgREST, GraphQL, or Prisma against the cluster. Computed cells (SERIAL, FORMULA, AI, LOOKUP, ROLLUP) are read-only. Schema mutation (create table, add column) is not on this API — change columns in Onnie.

v1 also does not expose pages, skills, or tasks. Keys minted in Settings only offer tables:read and tables:write.

Prerequisites

  1. Workspace Database is on. If it is off, every Data route returns 403 database.disabled. Keys are not auto-revoked.
  2. A workspace admin mints a key: Settings → API Keys → Create new key. Copy it once.
  3. Use tables:read and/or tables:write. Write implies read.

See Authentication for the Bearer header, key format, and revoke behavior.

Base URL

https://api.onnie.ai/api/public/v1

JSON. Version is in the path. Table and record ids in the path are UUIDs (GET /tables returns id plus alias_id). Unknown table in this workspace → 404 table.notFound.

Surface

JobMethodPathScope
List tablesGET/tablesread
Get table + columnsGET/tables/{tableId}read
Schema JSONGET/schemaread
Generated TypeScriptGET/schema.tsread
List recordsGET/tables/{tableId}/recordsread
Get recordGET/tables/{tableId}/records/{recordId}read
Create record(s)POST/tables/{tableId}/recordswrite
Patch recordPATCH/tables/{tableId}/records/{recordId}write
Delete recordDELETE/tables/{tableId}/records/{recordId}write
FILE presignPOST…/files/{columnName}/presignwrite
FILE registerPOST…/files/{columnName}/registerwrite
FILE sign (GET URL)GET…/files/{columnName}/signread

Canonical first write:

curl -X POST https://api.onnie.ai/api/public/v1/tables/$TABLE_ID/records \
  -H "Authorization: Bearer onn_live_..." \
  -H "Content-Type: application/json" \
  -d '{"fields":{"Title":"Inbound lead"}}'

200 with { "record": { … } }. Create is not 201. Then an Automation on record.created can mint Work. Five-minute walkthrough: Quickstart.

Record shape

Every public record looks like:

{
  "id": "…",
  "table_id": "…",
  "created_at": "2026-09-12T12:00:00.000Z",
  "updated_at": "2026-09-12T12:00:00.000Z",
  "fields": { "Title": "Inbound lead" }
}

fields uses column names and the public codec (REFERENCE ids, FILE metadata, ISO datetimes). The API omits created_by, updated_by, and form-submit bookkeeping. Inserts from a key leave created_by null — a key is not a member.

Field types

Keys in fields must be real column names. Unknown key → 422 fields.unknown.

ColumnReadWriteNotes
TEXT, LONGTEXT, EMAIL, PHONE, URLstringstring
NUMBERnumbernumberJSON number, not a string
BOOLEANbooleanboolean
DATETIMEISO-8601 stringISO-8601 stringNot Date, not unix ms. Other shapes → 422 datetime.invalid
SELECToption stringsameOptions come from column settings { value }
MULTISELECTstring[]string[]
REFERENCE, max is 1 or omittedstring | nullstring | nullPublic id. Stored internally as a pointer
REFERENCE, max > 1string[]string[]
FILEFileValue[]omittedBytes via presign → PUT → register
SERIAL, FORMULA, AI, LOOKUP, ROLLUPpresent, read-onlystrippedSending them does not persist

Optional columns (required: false) are T | null on write. Omit a key on PATCH to leave it unchanged; send null to clear an optional cell. Required write columns must be present on create. PATCH only validates keys you send.

REFERENCE writes

v1 writes only entity_type = records (table → table ids). Other entity types may appear as ids on read; writing them is 422 reference.unsupported. No nested expand.

GET /tables/{tableId} returns a client-needed settings subset: SELECT options, REFERENCE max / entity_type / target_table_id, FILE limits. Use GET /schema.ts for the typed form of the same columns.

Writes

All record writes go through one kernel. Formula / lookup / rollup recompute via the existing database trigger — public writes do not HMAC a second recompute.

Create

Body is { "fields": { … } } or bulk { "records": [ { … }, … ] }.

  • Single → 200 { "record": … }
  • Bulk → 200 { "records": [ … ] }
  • Bulk cap 50; over → 422 bulk.tooMany
  • Required FILE is not required on create (attach after the row exists)
  • Computed keys are stripped; FILE JSON → 422 file.use_upload
  • Enqueues record.created with source: "api" (fire-and-forget; enqueue failure does not fail HTTP)

Patch

Body { "fields": { … } } is partial. Empty diff → 200 with the current record and no automation. Otherwise record.updated with changed_columns.

Delete

200 { "ok": true, "id": "…" }. Purges that record’s table files, then deletes the row. No record.deleted automation (v1 Automations have no delete trigger).

FILE cells

The record must already exist. Do not POST or PATCH FileValue JSON to attach (422 file.use_upload).

  1. POST …/presign with { "files": [{ "filename", "mime_type", "size_bytes" }] } — does not enqueue Automations
  2. PUT bytes to each presignedUrl
  3. POST …/register with { "keys": ["…"] } — patches the cell through the same write kernel and can fire record.updated

Reads return FileValue[] metadata, not bytes. GET …/sign mints short-lived GET URLs for keys already on that cell.

type FileValue = {
  id: string
  key: string
  filename: string
  mime_type: string
  size_bytes: number
  uploaded_at: string // ISO
  storage_id?: string
}

MIME, size, and count follow the FILE column (and workspace storage headroom). Failures: file.mimeNotAllowed, file.tooLarge, file.tooMany, file.malformed, 413 storage.limitReached.

Clearing files

  • PATCH { "fields": { "Attachment": [] } } clears that FILE cell. Objects still referenced by other FILE cells on the same record are kept. Unreferenced objects for that record are purged. Required FILE cannot be cleared (422 fields.required).
  • DELETE on the record purges that record’s table files, then removes the row.

Schema and types

GET /schema.ts is generated at request time from live table_columns for the key’s workspace. Content-Type: text/plain; charset=utf-8.

Per table (export name = sanitized table name):

export type OrdersRecord = {
  id: string
  table_id: string
  created_at: string
  updated_at: string
  fields: OrdersFields
}
export type OrdersFields = { /* read: includes computed + FILE */ }
export type OrdersWrite = { /* create/patch: no computed, no FILE */ }

PATCH bodies are Partial<NameWrite>. Collision on sanitized names suffixes _ + short alias_id. The header comment in the file is the contract: regenerate after a column add, rename, or type change.

GET /schema is the structured JSON equivalent (tables, columns, tsKind) if you generate another language. OpenAPI in these docs is generic (Record / FileValue) — per-table unions live in schema.ts, not in the YAML.

Automations and Work

EventWhen
record.createdAfter a successful create (including each bulk row)
record.updatedAfter a patch or FILE register that actually changed columns
(none on delete)Delete does not enqueue

Work stays on Automations. Design the table and the rule in Onnie; the external app only writes records.

List, filters, pagination

GET /tables/{tableId}/records returns { "records", "next_cursor" }. Order is always created_at desc, id desc. Default limit 50, max 100 (higher values clamp). GET /tables is the full catalog — no cursor.

Equality filters only: ?{exactColumnName}=eq.{urlencodedValue}, ANDed. TEXT / EMAIL / PHONE / URL / SELECT / BOOLEAN / NUMBER / DATETIME. Computed, FILE, REFERENCE, MULTISELECT, LONGTEXT → 400 filter.unsupported. No gt / like / in / OR, no sort parameter.

Depth: Pagination & filters.

Errors and rate limits

Every error uses the same envelope:

{ "error": { "code": "record.notFound", "message": "Record not found", "details": {} } }

Per-key RPM bucket (default 120). Exhausted → 429 rate.limited. There is no separate API-call meter in v1.

Full code table: Errors. Bucket: Rate limits.

v1 does not include

  • SQL, Prisma, connection strings, GraphQL, PostgREST
  • mint_work / task, page, or skill routes
  • Schema mutation (create table / add column)
  • Nested expand, realtime, CSV import, Idempotency-Key
  • Multipart create-with-files (create the row first)
  • OAuth / user-delegated keys / per-table ACL
  • A published @onnie/data-client
  • Auto-revoke when Database is turned off