HTTP API: SQL, REST and GraphQL
Run SQL, read and write tables PostgREST-style, query GraphQL and keep a transaction open across requests — parameters, row format, limits and errors.
Read and write your tables over HTTP three ways: plain SQL (/api/v1/sql/query), PostgREST-style REST (/rest/v1), or GraphQL. All three run as the caller, so access policies and row-level security apply the same way (How access is decided). SDKs: JavaScript: Database and documents, Python: Database and documents.
URL=https://<project-ref>.sovarias.in
ANON_KEY=<anon key>
ACCESS_TOKEN=<a signed-in user's access token>| Base URL | https://<project-ref>.sovarias.in — your project URL from Connect. |
| Keys | Send the project's key as apikey. Calls made for a signed-in user also send Authorization: Bearer <access token>. |
| Errors | A status code and {"detail": …} — a sentence, or {code, message} for errors your code may branch on (HTTP API). |
SQL
POST/api/v1/sql/query
Run one SQL statement as the caller.
Runs SELECT, INSERT, UPDATE, DELETE (with RETURNING) or EXPLAIN. Your access policies and row-level security decide what the caller may read and change, exactly as for every other route; the service role key passes them. One statement per request; change the schema with migrations or the table routes, not here.
Values go in params as $1, $2, … — never in the SQL string. Rows come back as arrays in the order of columns.
- Who
- Your app: the project's anon key (
apikey: <anon key>), with the user's access token once they are signed in. - Also at
/rest/sql/query
Headers
X-Statement-Timeout-Msintegeroptional- Run this statement with a longer (or shorter) timeout, in milliseconds, up to your plan's maximum (
sql.max_statement_timeout_ms).
Body
sqlstringrequired- One statement. Use
$1,$2, … for values. - 1–65,536 characters
paramsarray<string | integer | number | boolean | object | array<any> | null> | nulloptional- Values for
$1,$2, …, bound by the database (up to 100). Strings, numbers, booleans,null, objects and arrays (as JSON). - 0–100 items
txidinteger | nulloptional- Run inside a transaction opened with
POST /api/v1/data/rel/transaction/begin. - 1.0–…
Returns
columnsarray<string>- Column names, in order.
rowsarray<array<any>>- One array of values per row, in the order of
columns.numericvalues come back as strings so no precision is lost;timestamptzas ISO 8601 strings;json/jsonbas JSON. row_countinteger- Rows returned; for
INSERT,UPDATEandDELETEwithoutRETURNING, the rows changed —0when row-level security hid every row. txidinteger- The transaction the statement ran in (
0when it ran on its own). simulatedbooleantruewhen the request ran as a simulation: every check ran, the change was rolled back.
Responses
- 200Success.
- 400Not one statement of an allowed kind, invalid SQL, a bad parameter, or a schema change (
Only SELECT, INSERT, UPDATE, DELETE and EXPLAIN run here). - 403Your access policies or row-level security refuse this caller (
NO_POLICY_DEFINED,POLICY_DENY), or the statement calls a function or catalog app users can't use. - 408
SQL statement timeout exceeded— add an index or alimit, or raiseX-Statement-Timeout-Ms. - 409A constraint or serialization conflict.
- 503The project's database isn't reachable yet (still provisioning or resizing) — retry.
- 401No valid session or key.
- 422The body is missing a required field or has a field of the wrong type.
curl -s -X POST "$URL/api/v1/sql/query" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"sql": "select id, total, status from orders where customer_id = $1 order by id desc limit 2", "params": ["6e19c0a2-3f5b-4a8e-9b1d-0c7f2e8a4d51"]}'Response
{
"columns": [
"id",
"total",
"status"
],
"rows": [
[
1042,
"59.90",
"paid"
],
[
1031,
"12.50",
"refunded"
]
],
"row_count": 2,
"txid": 0,
"simulated": false
}POST/api/v1/sql/console
Run SQL from the SQL editor: several statements, schema changes, dry runs.
The console's SQL editor route, for the project team only. Runs up to 200 statements in order; schema changes are recorded in the migration history. With dry_run: true every statement runs and is rolled back, and impact says how many rows would change.
- Who
- A project team member's session (the console, or a token from
sov login) asAuthorization: Bearer <token>. The member's role must allow the action. - Also at
/rest/sql/console
Headers
X-Statement-Timeout-Msintegeroptional- Run this statement with a longer (or shorter) timeout, in milliseconds, up to your plan's maximum (
sql.max_statement_timeout_ms).
Body
sqlstringrequired- One or more statements separated by
;. dry_runbooleanoptionaldefaultfalse- Run, then roll everything back.
Returns
columns, rowsarray- The last statement's result (rows as arrays).
row_countinteger- Rows returned, or rows changed.
statementsinteger- How many statements ran.
schema_changesinteger- How many were schema changes.
dry_runbooleanimpactobject | null- Dry runs only:
rows_affected. duration_msintegermigration_versionstring | null- The migration history entry recorded for schema changes.
warningsarray<string>- Advice — for example a new table app users can't reach until it has row-level security and policies.
txidinteger
Responses
- 200Success.
- 400Empty or unparsable SQL, more than 200 statements, or a function that isn't allowed.
- 403Not a project member, or schema changes without the admin/owner role.
- 408A statement ran past the timeout.
- 401No valid session or key.
- 422The body is missing a required field or has a field of the wrong type.
curl -s -X POST "$URL/api/v1/sql/console" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"sql": "update orders set status = 'archived' where created_at < now() - interval '1 year'", "dry_run": true}'Response
{
"columns": [],
"rows": [],
"row_count": 318,
"statements": 1,
"schema_changes": 0,
"dry_run": true,
"impact": {
"rows_affected": 318
},
"duration_ms": 41,
"migration_version": null,
"warnings": [],
"txid": 0
}GET/api/v1/sql/schema-snapshot
The whole schema in one response: tables, columns, keys, indexes, functions and triggers.
What the console's table list and schema diagram use. Send the previous ETag as If-None-Match to get 304 Not Modified when nothing changed (any schema change, even over a direct connection, changes it). Sovaria's own tables are left out.
- Who
- A project team member's session (the console, or a token from
sov login) asAuthorization: Bearer <token>. The member's role must allow the action. - Also at
/rest/sql/schema-snapshot
Headers
If-None-Matchstringoptional- The
ETagfrom the previous response.
Responses
- 200Success.
- 304Not modified: your copy (from
If-None-Match) is current. - 401No valid session or key.
curl -s "$URL/api/v1/sql/schema-snapshot" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN"Response
{
"schemas": [
"public"
],
"tables": [
{
"schema": "public",
"name": "orders",
"kind": "table"
}
],
"columns": [
{
"schema": "public",
"table_name": "orders",
"column_name": "id",
"data_type": "bigint",
"udt_name": "int8",
"is_nullable": "NO",
"column_default": null,
"is_identity": "YES"
},
{
"schema": "public",
"table_name": "orders",
"column_name": "total",
"data_type": "numeric",
"udt_name": "numeric",
"is_nullable": "NO",
"column_default": null,
"is_identity": "NO"
}
],
"pks": [
{
"schema": "public",
"table_name": "orders",
"column_name": "id"
}
],
"fks": [
{
"schema": "public",
"table_name": "orders",
"column_name": "customer_id",
"ref_table": "customers",
"ref_column": "id"
}
],
"uniques": [],
"indexes": [
{
"schema": "public",
"table_name": "orders",
"name": "orders_customer_idx"
}
],
"functions": [],
"triggers": [],
"fingerprint": "9b1f0c4e7a2d…"
}Tables over REST (PostgREST compatible)
/rest/v1 speaks the PostgREST protocol, so supabase-js (from(), rpc()) and postgrest-py work by changing the URL and key. Responses and errors use PostgREST's shapes.
GET/rest/v1/{table}
Read rows, PostgREST style (works with supabase-js and postgrest-py).
Each request becomes one parameterised SELECT that runs like /api/v1/sql/query: your policies and row-level security apply. Rows come back as JSON objects; numeric columns are JSON numbers and timestamps ISO 8601 strings. At most 1,000 rows per request — page with limit/offset or Range.
- Who
- Your app: the project's anon key (
apikey: <anon key>), with the user's access token once they are signed in.
Path parameters
tablestringrequired- A table or view in
public.
Query parameters
selectstringoptional- Columns:
id,title,*,alias:column,column::type(one cast), JSON pathsmeta->>tag. Embedding related tables (author(name)) isn't supported yet — use a view orrpc. <column>stringoptional- A filter
operator.value:eq neq gt gte lt lte like ilike match imatch is in cs cd ov fts plfts phfts wfts, negated withnot.— e.g.status=eq.paid,id=in.(1,2,3),deleted_at=is.null. or / andstringoptional- Combined filters:
or=(status.eq.paid,total.gt.100); they nest. orderstringoptionalcolumn.asc|desc[.nullsfirst|.nullslast], comma-separated.limit, offsetintegeroptional- Paging (at most 1,000 rows per request).
Headers
Preferstringoptionalcount=exactadds the total toContent-Range(0-9/1043).Rangestringoptional0-9instead oflimit/offset.Acceptstringoptionalapplication/vnd.pgrst.object+jsonreturns one object instead of an array (.single()); any other count is406 PGRST116.Accept-Profilestringoptional- The schema; only your own (
public). Others are406 PGRST106.
Responses
- 200Success.
- 400A filter,
selectororderthe translator doesn't understand (PGRST100). - 401
PGRST301— no valid key or session. - 403
42501— refused by your policies. - 404
PGRST205— no such table or view. - 406
PGRST116(single object requested, other count) orPGRST106(schema not exposed).
- Errors use PostgREST's shape:
{"code": "42501", "message": "…", "details": null, "hint": null}.
curl -s "$URL/rest/v1/orders?select=id,total,status&status=eq.paid&order=id.desc&limit=2" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN"Response
[
{
"id": 1042,
"total": 59.9,
"status": "paid"
},
{
"id": 1029,
"total": 18.0,
"status": "paid"
}
]HEAD /rest/v1/{table}
Count rows without fetching them.
Same filters as GET; send Prefer: count=exact and read the total from Content-Range (*/1043). No body.
- Who
- Your app: the project's anon key (
apikey: <anon key>), with the user's access token once they are signed in.
Path parameters
tablestringrequired- A table or view in
public.
Responses
- 200Success.
- 401No valid session or key.
curl -s -X HEAD "$URL/rest/v1/orders?status=eq.paid" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Prefer: count=exact"POST/rest/v1/{table}
Insert one or many rows; upsert with Prefer: resolution=….
Send one object or an array of objects. By default nothing comes back (201, no body); Prefer: return=representation returns the inserted rows.
- Who
- Your app: the project's anon key (
apikey: <anon key>), with the user's access token once they are signed in.
Path parameters
tablestringrequired- A table in
public.
Query parameters
on_conflictstringoptional- Upsert target: the unique column(s), e.g.
on_conflict=sku. columnsstringoptional- Only insert these keys of the body.
selectstringoptional- Columns to return with
return=representation.
Headers
Preferstringoptionalreturn=representationorreturn=minimal;resolution=merge-duplicates(upsert) orresolution=ignore-duplicates.
Body
(row)object | array<object>required- Column names to values.
Responses
- 201Created. A body only with
Prefer: return=representation. - 400
PGRST102(body isn't JSON) or a column that doesn't exist. - 403
42501— refused by your policies or row security. - 409
23505— a unique value already exists (use upsert). - 401No valid session or key.
curl -s -X POST "$URL/rest/v1/orders" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Prefer: return=representation" \
-H 'Content-Type: application/json' \
-d '[{"customer_id": "6e19c0a2-…", "total": 59.9}]'Response
[
{
"id": 1043,
"customer_id": "6e19c0a2-3f5b-4a8e-9b1d-0c7f2e8a4d51",
"total": 59.9,
"status": "pending",
"created_at": "2026-10-17T12:40:11.204000+00:00"
}
]PATCH/rest/v1/{table}
Update the rows that match the filters.
At least one filter is required (21000 otherwise), so a missing filter can't change every row.
- Who
- Your app: the project's anon key (
apikey: <anon key>), with the user's access token once they are signed in.
Path parameters
tablestringrequired- A table in
public.
Query parameters
<column>stringrequired- Filters, as for
GET—id=eq.1043.
Headers
Preferstringoptionalreturn=representationreturns the updated rows.
Body
(changes)objectrequired- Columns to set.
Responses
- 200Updated (
204withoutreturn=representation). Rows row-level security hides aren't changed. - 400
21000— no filter. - 403
42501— refused by your policies. - 401No valid session or key.
curl -s -X PATCH "$URL/rest/v1/orders?id=eq.1043" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Prefer: return=representation" \
-H 'Content-Type: application/json' \
-d '{"status": "paid"}'Response
[
{
"id": 1043,
"customer_id": "6e19c0a2-3f5b-4a8e-9b1d-0c7f2e8a4d51",
"total": 59.9,
"status": "paid",
"created_at": "2026-10-17T12:40:11.204000+00:00"
}
]DELETE/rest/v1/{table}
Delete the rows that match the filters.
At least one filter is required (21000 otherwise).
- Who
- Your app: the project's anon key (
apikey: <anon key>), with the user's access token once they are signed in.
Path parameters
tablestringrequired- A table in
public.
Query parameters
<column>stringrequired- Filters, as for
GET.
Headers
Preferstringoptionalreturn=representationreturns the deleted rows.
Responses
- 200Deleted (
204, or the rows withreturn=representation). - 400
21000— no filter. - 403
42501— refused by your policies. - 401No valid session or key.
curl -s -X DELETE "$URL/rest/v1/orders?id=eq.1043" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN"POST/rest/v1/rpc/{function}
Call a Postgres function with named arguments.
The body's keys are the function's argument names. A function returning one value answers with that value; a set-returning function with an array of rows. App users need an access policy that allows fn:<name>.
- Who
- Your app: the project's anon key (
apikey: <anon key>), with the user's access token once they are signed in.
Path parameters
functionstringrequired- The function's name (in
public).
Body
(arguments)objectoptional- Argument names to values.
Responses
- 200Success.
- 403
42501— no rule lets this caller call the function. - 404No such function.
- 401No valid session or key.
curl -s -X POST "$URL/rest/v1/rpc/order_total" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"customer": "6e19c0a2-3f5b-4a8e-9b1d-0c7f2e8a4d51"}'Response
1284.5GET/rest/v1/rpc/{function}
Call a read-only function with arguments in the query string.
- Who
- Your app: the project's anon key (
apikey: <anon key>), with the user's access token once they are signed in.
Path parameters
functionstringrequired- The function's name (in
public).
Query parameters
<argument>stringoptional- Each argument by name.
Responses
- 200Success.
- 401No valid session or key.
curl -s "$URL/rest/v1/rpc/order_total?customer=6e19c0a2-…" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN"Response
1284.5GraphQL
POST/graphql/v1
Query and change your tables with GraphQL (pg_graphql).
Turn on the pg_graphql extension first (Schema › Extensions). The schema is generated from your tables; queries run as the caller, so row-level security applies.
- Who
- Your app: the project's anon key (
apikey: <anon key>), with the user's access token once they are signed in.
Body
querystringrequired- The GraphQL document.
- at least 1 characters
variablesobject | nulloptional- Values for its variables.
operationNamestring | nulloptional- Which operation to run when the document has several.
Responses
- 200Success.
- 200GraphQL errors (unknown field, bad argument) come back with
200and anerrorsarray, as GraphQL does. - 403Refused by your access policies.
- 401No valid session or key.
- 422The body is missing a required field or has a field of the wrong type.
curl -s -X POST "$URL/graphql/v1" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"query": "query { ordersCollection(first: 2, filter: {status: {eq: \"paid\"}}) { edges { node { id total } } } }"}'Response
{
"data": {
"ordersCollection": {
"edges": [
{
"node": {
"id": 1042,
"total": "59.90"
}
},
{
"node": {
"id": 1029,
"total": "18.00"
}
}
]
}
}
}Transactions across requests
POST/api/v1/data/rel/transaction/begin
Open a transaction that spans several requests.
Send the returned txid with each POST /api/v1/sql/query, then commit or roll back. Statements in it see a consistent snapshot. Keep transactions short and always commit or roll back.
- Who
- A signed-in app user:
Authorization: Bearer <access token>, plus the project'sapikey. - Also at
/rest/rel/transaction/begin
Returns
txidinteger- Send it as
txidwith your queries and to commit/rollback. snapshot_txidinteger- The snapshot the transaction reads from.
Responses
- 200Success.
- 503The database isn't ready.
- 401No valid session or key.
curl -s -X POST "$URL/api/v1/data/rel/transaction/begin" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN"Response
{
"txid": 81234,
"snapshot_txid": 81234
}POST/api/v1/data/rel/transaction/commit
Commit a transaction.
- Who
- A signed-in app user:
Authorization: Bearer <access token>, plus the project'sapikey. - Also at
/rest/rel/transaction/commit
Body
txidintegerrequired- From
begin. - 1.0–…
Responses
- 200Success.
- 404No such transaction, or it already finished.
- 409It was aborted (a conflict or an error in one of its statements); nothing was kept.
- 401No valid session or key.
- 422The body is missing a required field or has a field of the wrong type.
curl -s -X POST "$URL/api/v1/data/rel/transaction/commit" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"txid": 81234}'Response
{
"status": "ok",
"txid": 81234,
"committed": true
}POST/api/v1/data/rel/transaction/rollback
Roll a transaction back; nothing it did is kept.
- Who
- A signed-in app user:
Authorization: Bearer <access token>, plus the project'sapikey. - Also at
/rest/rel/transaction/rollback
Body
txidintegerrequired- From
begin. - 1.0–…
Responses
- 200Success.
- 404No such transaction, or it already finished.
- 401No valid session or key.
- 422The body is missing a required field or has a field of the wrong type.
curl -s -X POST "$URL/api/v1/data/rel/transaction/rollback" \
-H "apikey: $ANON_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"txid": 81234}'Response
{
"status": "ok",
"txid": 81234,
"rolled_back": true
}