REST API
Read, insert, update and delete rows. The syntax follows PostgREST.
Every path lives under https://gridie.ai/api/v1.
List tables
GET /api/v1/tablesReturns the tables this key can work with, along with their columns. Use it to check the shape before writing a client.
{
"tables": [
{
"name": "orders",
"label": "Orders",
"description": null,
"rowCount": 128,
"columns": [
{ "name": "id", "type": "INTEGER", "nullable": false, "primaryKey": true },
{ "name": "customer", "type": "TEXT", "nullable": true, "label": "Customer name" }
]
}
]
}name is what you use in requests, label is what appears in the Gridie interface. An empty list means no table has been opened to the API yet.
This endpoint is a Gridie addition, which is why the response is an object with a tables key rather than a bare array.
Read rows
GET /api/v1/tables/ordersThe response is a bare array of rows, with no wrapper key.
Query parameters
| Name | Format | Description |
|---|---|---|
select | comma list | Columns to return. Defaults to all |
order | column.direction | Such as created_at.desc. Comma separated for several |
limit | integer | Defaults to 50, capped at 1000 |
offset | integer | Rows to skip |
| any other name | operator.value | A column filter, such as status=eq.paid |
curl "https://gridie.ai/api/v1/tables/orders?select=id,customer,total&status=eq.paid&total=gte.10000&order=created_at.desc&limit=20" \
-H "apikey: $GRIDIE_API_KEY"Name a column that does not exist and you get a 400 telling you which name was wrong. It is never silently ignored.
Filter operators
| Operator | Meaning | Example |
|---|---|---|
eq | Equals | status=eq.paid |
neq | Not equals | status=neq.draft |
gt gte | Greater than, or equal | total=gte.10000 |
lt lte | Less than, or equal | total=lt.500 |
like | Pattern match, * is the wildcard | name=like.Ada* |
ilike | Same, ignoring case | email=ilike.*@gmail.com |
is | Matches null, true or false | paid_at=is.null |
in | Any of a list | status=in.(paid,shipped) |
Prefix with not. to negate the whole condition that follows.
status=not.eq.paid
paid_at=not.is.nullSeveral parameters combine with AND.
Ordering
order=created_at.desc
order=priority.asc.nullslast,created_at.descDirection is asc or desc, and null placement is nullsfirst or nullslast.
Counting
We do not count by default. Ask for it with a header when you need a total for pagination.
curl "https://gridie.ai/api/v1/tables/orders?limit=20" \
-H "apikey: $GRIDIE_API_KEY" \
-H "Prefer: count=exact"It arrives in the Content-Range header.
Content-Range: 0-19/128Without the header the total is *. Counting every time would mean a second query on every read.
A single row
Ask for one object with the Accept header. This is what supabase-js sends for .single().
curl "https://gridie.ai/api/v1/tables/orders?id=eq.1" \
-H "apikey: $GRIDIE_API_KEY" \
-H "Accept: application/vnd.pgrst.object+json"If the result is not exactly one row, you get a 406.
Insert rows
POST /api/v1/tables/ordersSend one object or an array of objects as the body. There is no wrapper key.
curl -X POST "https://gridie.ai/api/v1/tables/orders" \
-H "apikey: $GRIDIE_API_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '[{"customer":"Ada Lovelace","total":42000,"status":"paid"}]'With Prefer: return=representation you get a 201 and the stored rows. Without it, a bare 204.
When you send several rows, every row must carry the same set of columns. If the columns differ per row, the request alone does not say which values should fall back to defaults, so we reject it with a 400.
Upsert
Add Prefer: resolution=merge-duplicates to overwrite on a primary key collision. This is the header supabase-js sends for .upsert().
curl -X POST "https://gridie.ai/api/v1/tables/orders" \
-H "apikey: $GRIDIE_API_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: resolution=merge-duplicates,return=representation" \
-d '[{"id":1,"status":"shipped"}]'Use resolution=ignore-duplicates to skip colliding rows instead.
Upsert needs a primary key on the table. Without one there is nothing to call a collision, so the request fails with a 400.
Update rows
PATCH /api/v1/tables/ordersThe body holds the new values as a single object, and query parameters decide which rows to change.
curl -X PATCH "https://gridie.ai/api/v1/tables/orders?id=eq.1" \
-H "apikey: $GRIDIE_API_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{"status":"archived"}'A request with no filter is refused. An unfiltered UPDATE rewrites the whole table. If you really do mean every row, say so with a condition that is always true, such as id=gte.0.
Delete rows
DELETE /api/v1/tables/orderscurl -X DELETE "https://gridie.ai/api/v1/tables/orders?status=eq.draft" \
-H "apikey: $GRIDIE_API_KEY" \
-H "Prefer: return=representation"As with updates, a request without a filter is refused.
How many rows a write returns
Updates and deletes touch every row the filter matches. The rows carried back by Prefer: return=representation stop at 1000, so the response’s Content-Range leaves the total as *.
Content-Range: 0-999/*Do not read the number of returned rows as the number of rows changed. Past 1000 they differ.
Request headers
| Header | What it does |
|---|---|
apikey | Authentication. Authorization: Bearer <key> also works |
Prefer: return=representation | Return the written rows. Otherwise 204 |
Prefer: count=exact | Count all matches and report it in Content-Range |
Prefer: resolution=merge-duplicates | Turn a POST into an upsert |
Prefer: resolution=ignore-duplicates | Skip colliding rows |
Accept: application/vnd.pgrst.object+json | Return a single row as an object |
Connecting with the Supabase client is covered in Supabase Compatibility, and the codes a failed request returns are in Error Codes.