https://<slug>.<project domain>/rest/v1. This page
is the reference for that endpoint: what it accepts, what it refuses, and every
error it answers with. It is current as of 2026-10-27.
Before you start
- The project has a database and the
data_apimodule switched on: in the console (Data API, Turn on), orPATCH /projects/<project id>with the project’s wholeserviceslist includingdata_api. Turning it off answersservice_not_enabledwithin about 30 seconds and keeps your table exposure and settings for when you turn it back on. - You have a project API key: a publishable key (
dtn_pk_...) or a secret key (dtn_sk_...). - The table you call is exposed to the Data API (see Exposing tables).
- The project’s Data API URL is the
urlofGET /databases/<database id>/data_apion the management API.
Keys and roles
- Send the key in the
apikeyheader. A key in the query string (?apikey=) is refused withapi_key_in_url. Authorization: Bearer <the same key>is accepted and ignored. Any other bearer token is refused withuser_token_invalid(401, with aWWW-Authenticate: Bearer error="invalid_token"challenge): the Data API takes project keys only.- Every answer carries an
x-request-idheader. Quote it when you contact support.
Exposing tables
A new table is not reachable through the Data API. A publishable call to it answers 403data_access_denied; a secret call reads it as the owner.
Expose a table with a schema change on the management API:
- Exposing a table also turns its row-level security on. With no policy, an
exposed table answers no rows to the publishable key:
200 []. Add a policy foranonto return rows, for examplecreate policy "read open todos" on public.todos for select to anon using (not done); create_tablechanges take the same optionalexposure.- Functions:
set_function_exposurewith the function’s name, its argument types assignature, andexposed: trueorfalse. A function is callable only in an exposed schema. - The table list and the table detail on the management API report each
table’s
exposureandpolicyCount;custommeans grants that match none of the three, made over a direct connection. Setting an exposure replaces them. - Every exposure change is recorded in the database’s History like any other schema change.
Reading
Lists are plain JSON arrays paged by offset and range. They are not the
{ data, nextCursor } envelope the management API uses.
Writing
Updates and deletes need a filter. A
PATCH or DELETE on a table whose
query holds only select, order, limit, offset, columns or
on_conflict is refused with filter_required before anything runs. limit
alone is not a filter.
Updates and deletes are bounded. Each runs with handling=strict and a
max-affected cap, so a change that would touch more rows than the cap
changes nothing and answers max_affected_exceeded. The cap is the project’s
limit (1,000 rows by default), or your own lower Prefer: max-affected=<n>.
Functions
POST /rest/v1/rpc/<function>with the arguments as one JSON object.GET /rest/v1/rpc/<function>?<argument>=<value>for functions declaredstableorimmutable.- A
RAISEin your function answersdata_function_raisedwith your message asdetail. A status your function sets is answered with that status and the code in A status your function sets.
Schemas and the API description
- The project’s exposed schemas default to
public. Change the list withPATCH /databases/<database id>/data_api(exposedSchemas, at most 20). History, the system catalogs and schemas startingpg_cannot be exposed. - Pick a schema per request with
Accept-Profile(reads) orContent-Profile(writes). GET /rest/v1/with a secret key answers an OpenAPI 2.0 description of what the owner may reach. A publishable key is refused withapi_key_kind_not_allowed.
Preferences
ThePrefer header (RFC 7240) is read as a comma-separated list.
Limits
The project’s Data API stops after 10 minutes without a request. The first
request after that waits while it starts; if the start takes more than 2
seconds it answers
unavailable with Retry-After: 1.
Idempotency
- Send
Idempotency-Key: <1 to 255 of A-Z a-z 0-9 _ ->onPOST,PATCHorDELETEto make a retry safe.GET,HEADandPUTare idempotent already, so the header is ignored on them. - A key is scoped to the project and the API key that sent it. The same key
with the same request (method, path, query,
Content-Type,Preferand body) replays the first answer, for 24 hours. - The same key with a different request: 422
idempotency_key_reused. The same key while the first request still runs: 409idempotency_request_in_progress. - Answers with a 2xx or 4xx status are kept for replay. A 5xx answer is not, so a retry under the same key runs again.
- An answer body larger than 1 MiB is not kept: its replay has the same status and headers and an empty body.
CORS
- Browsers may call the Data API only from origins on the project’s list:
corsOriginsonPATCH /databases/<database id>/data_api, each exactlyscheme://host[:port], at most 20. The list is empty by default, so no browser origin is allowed until you add one.*allows every origin and must be the only entry. - A preflight from a listed origin answers
204withAccess-Control-Allow-Methods: GET, HEAD, POST, PUT, PATCH, DELETE, the allowed request headers (includingapikey,authorizationandidempotency-key) andAccess-Control-Max-Age: 600. From any other origin: 403cors_origin_not_allowed. - Answers to a listed origin echo it in
Access-Control-Allow-OriginwithVary: Origin, and exposeContent-Range,Content-Location,Location,Preference-Applied,x-request-idandRetry-After. - Use the publishable key in browsers. A secret key works from a browser too, so never ship it in one.
Schema changes and reloads
The Data API reads your tables and functions once and keeps that picture. A change made through the management API, the console or the management MCP is picked up within about 2 seconds, with no action from you. A change made over a direct database connection is not. After one, ask for a reload:POST /databases/<database id>/data_api/schema_reloads(answers202), ordatabases.dataApi.reloadSchema(<database id>)in the SDK, or- the project MCP’s
data_api_run_schema_reloadtool.
Paused databases
Errors
Every refusal is an RFC 9457 problem,Content-Type: application/problem+json:
codeis stable: it is never renamed or reused. Branch on it, not ondetail.postgresis present when the database itself refused the request and the status is below 500. It carries the database’s error code (SQLSTATE) ascode. A secret key also getsmessage,detailsandhint; a publishable key getscodeonly.- With
@supabase/postgrest-js,error.codeis thiscode. Code written for a client that comparederror.codewith a SQLSTATE such as"23505"readserror.postgres.codeinstead.
Error codes
A status your function sets
A function that sets its own response status gets that status and the code below, with your function’s message asdetail. Any other 4xx status takes
bad_request; any other status takes internal.
Client example
The Data API speaks the grammar@supabase/postgrest-js writes; version
2.118.0 is the one our compatibility test runs. This snippet is run by that
test as written here:
apikey header: the client then sends no Authorization
header of its own.