Docs

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.

Updated Oct 18, 2026

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 URLhttps://<project-ref>.sovarias.in — your project URL from Connect.
KeysSend the project's key as apikey. Calls made for a signed-in user also send Authorization: Bearer <access token>.
ErrorsA 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. numeric values come back as strings so no precision is lost; timestamptz as ISO 8601 strings; json/jsonb as JSON.
row_countinteger
Rows returned; for INSERT, UPDATE and DELETE without RETURNING, the rows changed — 0 when row-level security hid every row.
txidinteger
The transaction the statement ran in (0 when it ran on its own).
simulatedboolean
true when 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.
  • 408SQL statement timeout exceeded — add an index or a limit, or raise X-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) as Authorization: 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_runbooleanoptionaldefault false
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_runboolean
impactobject | null
Dry runs only: rows_affected.
duration_msinteger
migration_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) as Authorization: Bearer <token>. The member's role must allow the action.
Also at
/rest/sql/schema-snapshot

Headers

If-None-Matchstringoptional
The ETag from 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 paths meta->>tag. Embedding related tables (author(name)) isn't supported yet — use a view or rpc.
<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 with not. — 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.
orderstringoptional
column.asc|desc[.nullsfirst|.nullslast], comma-separated.
limit, offsetintegeroptional
Paging (at most 1,000 rows per request).

Headers

Preferstringoptional
count=exact adds the total to Content-Range (0-9/1043).
Rangestringoptional
0-9 instead of limit/offset.
Acceptstringoptional
application/vnd.pgrst.object+json returns one object instead of an array (.single()); any other count is 406 PGRST116.
Accept-Profilestringoptional
The schema; only your own (public). Others are 406 PGRST106.

Responses

  • 200Success.
  • 400A filter, select or order the translator doesn't understand (PGRST100).
  • 401PGRST301 — no valid key or session.
  • 40342501 — refused by your policies.
  • 404PGRST205 — no such table or view.
  • 406PGRST116 (single object requested, other count) or PGRST106 (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

Preferstringoptional
return=representation or return=minimal; resolution=merge-duplicates (upsert) or resolution=ignore-duplicates.

Body

(row)object | array<object>required
Column names to values.

Responses

  • 201Created. A body only with Prefer: return=representation.
  • 400PGRST102 (body isn't JSON) or a column that doesn't exist.
  • 40342501 — refused by your policies or row security.
  • 40923505 — 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

Preferstringoptional
return=representation returns the updated rows.

Body

(changes)objectrequired
Columns to set.

Responses

  • 200Updated (204 without return=representation). Rows row-level security hides aren't changed.
  • 40021000 — no filter.
  • 40342501 — 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

Preferstringoptional
return=representation returns the deleted rows.

Responses

  • 200Deleted (204, or the rows with return=representation).
  • 40021000 — no filter.
  • 40342501 — 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.
  • 40342501 — 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.5

GET/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.5

GraphQL

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 200 and an errors array, 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's apikey.
Also at
/rest/rel/transaction/begin

Returns

txidinteger
Send it as txid with 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's apikey.
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's apikey.
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
}
Something unclear or missing on this page?Tell us