Skip to content

REST API Reference

Every non-system data object automatically exposes a full CRUD surface under /api/v1/data/:object. Objects are resolved per-request from the live schema registry, so a newly created object’s endpoints work immediately — no restart, no redeploy.

A machine-readable, always-current OpenAPI 3.1 spec is served at /api/v1/openapi.json.

  • Base URL: http://localhost:3000 in development.
  • Content type: application/json.
  • Auth: when ION_REQUIRE_AUTH is enabled, send an API key as X-API-Key: iond_… (or Authorization: Bearer iond_…), or a session cookie.
  • Response envelope: reads/writes of a single record return { "data": {…} }; lists return { "data": [...], "pagination": {…} }.
  • Errors: { "error": "<code>", "message": "<human message>" } with an appropriate HTTP status.
MethodPathPurpose
GET/api/v1/dataDiscovery — list available objects + their endpoints
GET/api/v1/data/:objectList records (search, filter, sort, paginate)
POST/api/v1/data/:objectCreate a record
POST/api/v1/data/:object/bulkBulk create ({ "data": [...] })
DELETE/api/v1/data/:object/bulkBulk delete ({ "ids": [...] })
GET/api/v1/data/:object/:idGet one record by id
PATCH/api/v1/data/:object/:idPartial update — send only the fields you’re changing
DELETE/api/v1/data/:object/:idDelete
POST/api/v1/data/:object/:id/links/:relAdd many_to_many links ({ "ids": [...] }, idempotent)
DELETE/api/v1/data/:object/:id/links/:relRemove many_to_many links ({ "ids": [...] })

There is deliberately no PUT full-replace verb: with runtime-defined schemas, replaying a stale full document would silently null out fields added since it was read. PATCH is the only update verb across REST, GraphQL, MCP, and the client SDK.

GET /api/v1/data/contacts?search=acme&status[neq]=archived&sort=-created_at&page=1&pageSize=25

The query language (search, field[operator]=value, sort, page, pageSize, select, expand) is documented in full in the Querying guide.

{
"data": [
{ "id": "", "full_name": "Ada Lovelace", "email": "ada@example.com", "status": "active" }
],
"pagination": {
"page": 1, "pageSize": 25, "totalCount": 1,
"totalPages": 1, "hasNextPage": false, "hasPreviousPage": false
}
}
Terminal window
curl -X POST http://localhost:3000/api/v1/data/contacts \
-H 'content-type: application/json' \
-d '{ "full_name": "Ada Lovelace", "email": "ada@example.com" }'
# 201 Created -> { "data": { "id": "…", … } }

System fields (id, created_at, updated_at, created_by, updated_by) are managed by the platform and ignored if supplied in the body. The *_by columns record the authenticated actor (user id, else API-key id): creates stamp both, updates re-stamp updated_by, and anonymous writes leave them null.

Terminal window
curl http://localhost:3000/api/v1/data/contacts/<id> # 200 or 404
curl -X PATCH http://localhost:3000/api/v1/data/contacts/<id> \
-H 'content-type: application/json' -d '{ "status": "archived" }' # 200 or 404
curl -X DELETE http://localhost:3000/api/v1/data/contacts/<id> # 204 or 404
Terminal window
# Bulk create
curl -X POST http://localhost:3000/api/v1/data/contacts/bulk \
-H 'content-type: application/json' \
-d '{ "data": [ { "full_name": "A" }, { "full_name": "B" } ] }'
# 201 -> { "count": 2, "ids": ["…","…"] }
# Bulk delete
curl -X DELETE http://localhost:3000/api/v1/data/contacts/bulk \
-H 'content-type: application/json' \
-d '{ "ids": ["…","…"] }'
# -> { "count": 2, "ids": [...] }
CodeWhen
200Successful read/update
201Record(s) created
204Record deleted
400Malformed body or unknown filter field
401 / 403Auth required / insufficient permission (when enforcement is on)
404Unknown object or record
import { IonDriveClient } from '@ion-drive/client';
const ion = new IonDriveClient({ baseUrl: 'http://localhost:3000', apiKey });
const contacts = ion.from('contacts');
await contacts.select(); // GET (awaitable, all)
await contacts.select().search('acme').range(0, 24); // GET (fluent query)
await contacts.select().eq('id', id).single(); // GET one (throws unless 1)
await contacts.get(id); // GET /:id (null on 404)
await contacts.insert({ full_name: 'Ada' }); // POST (returns the row)
await contacts.insert([{ full_name: 'A' }, { full_name: 'B' }]); // POST /bulk (summary)
await contacts.update(id, { status: 'archived' }); // PATCH (null on 404)
await contacts.delete(id); // DELETE (false on 404)
await contacts.bulkDelete([id1, id2]); // DELETE /bulk

See the Querying guide for the full fluent API (filters, .order, .range, .single/.maybeSingle).

GET /api/v1/openapi.json returns an OpenAPI 3.1 document generated from the current schema. It documents every object’s fields, the list query parameters (including search and the filter operators), and request/response schemas — point Swagger UI, Postman, or a codegen tool at it.