Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

API examples

Every endpoint, with real requests and responses. The full endpoint list is in the generated API reference.

Base URL: https://relstore.stackwell.tech. Request bodies are JSON (Content-Type: application/json). Examples use the agent client’s token unless they say otherwise; see Authentication. $AUTH below stands for:

AUTH=(-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json")

Create a table

POST /v1/tables (agent)

curl -sS https://relstore.stackwell.tech/v1/tables "${AUTH[@]}" \
  -d '{"name": "tasks", "columns": [
        {"name": "title", "type": "text"},
        {"name": "done", "type": "bool", "indexed": true},
        {"name": "priority", "type": "int"}]}'

Response, 200 OK:

{"created": "tasks"}
FieldRequiredRules
nameYesStarts with a letter; ASCII letters, digits, _; up to 51 characters
columnsYesAt least one. Each has name (same rules but up to 63 characters, not a reserved name), type (text, int, float, bool) and optional indexed (default false)

Any other field is refused (422). A table that already exists returns 409 with table "tasks" already exists.

Write a row

POST /v1/rows (agent)

curl -sS https://relstore.stackwell.tech/v1/rows "${AUTH[@]}" \
  -d '{"table": "tasks", "rid": "t1",
       "fields": {"title": "Write the docs", "done": false, "priority": 2}}'

Response, 200 OK, with the version this write appended:

{"version": 1}
FieldRequiredRules
tableYesAn existing table
ridYesAny string; your id for the row. Not personal data (why).
fieldsYesColumn name to value, for every column of the table: send null to clear one. A column left out, a key that is not a column, or a reserved name is 400, and nothing is written.

Writing the same rid again appends the next version. This marks t1 done; title and priority are sent again, since a write is the whole row:

curl -sS https://relstore.stackwell.tech/v1/rows "${AUTH[@]}" \
  -d '{"table": "tasks", "rid": "t1",
       "fields": {"title": "Write the docs", "done": true, "priority": 2}}'
{"version": 2}

Insert, patch and restore a row

These three append a version only from the row state they need; otherwise they are refused and write nothing. Each answers 200 OK with {"version": n}.

Insert, POST /v1/tables/{table}/rows (agent): a new row, with every column as for POST /v1/rows. 409 if the rid was ever written, even if it is soft-deleted (restore it instead).

curl -sS https://relstore.stackwell.tech/v1/tables/tasks/rows "${AUTH[@]}" \
  -d '{"rid": "t3", "fields": {"title": "Plan", "done": false, "priority": 1}}'

Patch, PATCH /v1/tables/{table}/rows/{rid} (agent): only the columns to change (null clears one); the others keep their current values, and the result is appended as a whole version. 404 if the rid was never written, 409 if it is soft-deleted, 400 for no columns or a key that is not a column.

curl -sS -X PATCH https://relstore.stackwell.tech/v1/tables/tasks/rows/t3 "${AUTH[@]}" \
  -d '{"fields": {"done": true}}'

Restore, POST /v1/tables/{table}/rows/{rid}/restore (agent), no body: a soft-deleted row comes back with the values of its last live version. 404 if the rid was never written, 409 if it is not soft-deleted or was never live.

curl -sS -X POST https://relstore.stackwell.tech/v1/tables/tasks/rows/t3/restore "${AUTH[@]}"

Query

POST /v1/query (agent or operator)

curl -sS https://relstore.stackwell.tech/v1/query "${AUTH[@]}" \
  -d '{"table": "tasks",
       "select": {"kind": "columns", "columns": ["rid", "title"]},
       "filter": {"op": "and", "all": [
         {"op": "eq", "column": "done", "value": false},
         {"op": "begins_with", "column": "title", "value": "Ship"}]},
       "order_by": [{"column": "priority", "descending": true}],
       "limit": 10}'

Response, 200 OK, an array of rows:

[
  {
    "rid": "t2",
    "title": "Ship it"
  }
]
FieldRequiredRules
tableYesAn existing table
selectNo{"kind": "columns", "columns": [...]}. Empty or absent: rid, updated_ts and every column.
filterNoOne condition; see below
order_byNo[{"column": "...", "descending": false}]
limit, offsetNoNon-negative integers; offset at most 9223372036854775807

You can select, filter and order on rid, updated_ts and your own columns. Filter conditions, by op:

opShapeMatches
eq, ne, lt, le, gt, ge{"op", "column", "value"}Comparison with value
between{"op", "column", "low", "high"}low to high, inclusive
in{"op", "column", "values": [...]}Any of values; an empty list matches nothing. A whole filter holds at most 65,534 values.
begins_with, contains{"op", "column", "value"}Text columns only; matched literally
is_null, not_null{"op", "column"}
and{"op": "and", "all": [...]}Every condition; an empty list matches everything
or{"op": "or", "any": [...]}Any condition; an empty list matches nothing
not{"op": "not", "predicate": {...}}The opposite

A value must suit the column’s type. Comparisons follow Postgres: eq with null matches nothing; use is_null.

Soft-delete a row

POST /v1/rows/{rid}/soft-delete (agent). The body names the table:

curl -sS https://relstore.stackwell.tech/v1/rows/t2/soft-delete "${AUTH[@]}" \
  -d '{"table": "tasks"}'

Response, 200 OK, with the tombstone’s version:

{"version": 2}

Put rid in the path URL-encoded.

Read a row’s history

GET /v1/rows/{rid}/history (agent or operator). This GET carries a JSON body naming the table:

curl -sS -X GET https://relstore.stackwell.tech/v1/rows/t2/history "${AUTH[@]}" \
  -d '{"table": "tasks"}'

Response, 200 OK. Row t2 was written, soft-deleted, then written again, by an agent client whose id is other_ag:

[
  {
    "deleted": false,
    "done": false,
    "env": "prod",
    "priority": 1,
    "title": "Ship it",
    "valid_from": "2026-11-05T20:45:04.971154Z",
    "version": 1,
    "written_as": "agent",
    "written_by": "other_ag"
  },
  {
    "deleted": true,
    "done": null,
    "env": "prod",
    "priority": null,
    "title": null,
    "valid_from": "2026-11-05T20:45:04.976163Z",
    "version": 2,
    "written_as": "agent",
    "written_by": "other_ag"
  },
  {
    "deleted": false,
    "done": true,
    "env": "prod",
    "priority": 1,
    "title": "Ship it",
    "valid_from": "2026-11-05T20:45:04.977697Z",
    "version": 3,
    "written_as": "agent",
    "written_by": "other_ag"
  }
]

Purge requests

Not yet enabled on the live service. The routes answer as follows today:

RouteAnswer
GET /v1/purge-requests200 with your database’s requests: []
POST /v1/purge-requests503 with purge requires Granite, which this deployment does not have configured; without it nothing irreversible is done
POST /v1/purge-requests/{id}/approve, .../reject503 for the operator client

Erasures

An erasure removes every row of every table in your database, history included; the tables and your clients stay. The operator client submits it (POST /v1/erasures, body {} or {"reason": "..."}), the database’s owner approves it, and then it is carried out. Not yet enabled on the live service:

RouteAnswer
GET /v1/erasures200 with your database’s erasures: []
POST /v1/erasures503, as for a purge request

Health

curl -fsS https://relstore.stackwell.tech/health

200 with ok when the service can reach its database, 503 with database unreachable when it can’t.