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"}
| Field | Required | Rules |
|---|---|---|
name | Yes | Starts with a letter; ASCII letters, digits, _; up to 51 characters |
columns | Yes | At 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}
| Field | Required | Rules |
|---|---|---|
table | Yes | An existing table |
rid | Yes | Any string; your id for the row. Not personal data (why). |
fields | Yes | Column 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"
}
]
| Field | Required | Rules |
|---|---|---|
table | Yes | An existing table |
select | No | {"kind": "columns", "columns": [...]}. Empty or absent: rid, updated_ts and every column. |
filter | No | One condition; see below |
order_by | No | [{"column": "...", "descending": false}] |
limit, offset | No | Non-negative integers; offset at most 9223372036854775807 |
You can select, filter and order on rid, updated_ts and your own columns.
Filter conditions, by op:
op | Shape | Matches |
|---|---|---|
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:
| Route | Answer |
|---|---|
GET /v1/purge-requests | 200 with your database’s requests: [] |
POST /v1/purge-requests | 503 with purge requires Granite, which this deployment does not have configured; without it nothing irreversible is done |
POST /v1/purge-requests/{id}/approve, .../reject | 503 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:
| Route | Answer |
|---|---|
GET /v1/erasures | 200 with your database’s erasures: [] |
POST /v1/erasures | 503, 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.