Skip to main content
Every project with the Data API switched on serves its database’s tables, views and functions over HTTP at 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_api module switched on: in the console (Data API, Turn on), or PATCH /projects/<project id> with the project’s whole services list including data_api. Turning it off answers service_not_enabled within 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 url of GET /databases/<database id>/data_api on the management API.

Keys and roles

  • Send the key in the apikey header. A key in the query string (?apikey=) is refused with api_key_in_url.
  • Authorization: Bearer <the same key> is accepted and ignored. Any other bearer token is refused with user_token_invalid (401, with a WWW-Authenticate: Bearer error="invalid_token" challenge): the Data API takes project keys only.
  • Every answer carries an x-request-id header. Quote it when you contact support.

Exposing tables

A new table is not reachable through the Data API. A publishable call to it answers 403 data_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 for anon to return rows, for example create policy "read open todos" on public.todos for select to anon using (not done);
  • create_table changes take the same optional exposure.
  • Functions: set_function_exposure with the function’s name, its argument types as signature, and exposed: true or false. A function is callable only in an exposed schema.
  • The table list and the table detail on the management API report each table’s exposure and policyCount; custom means 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 declared stable or immutable.
  • A RAISE in your function answers data_function_raised with your message as detail. 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 with PATCH /databases/<database id>/data_api (exposedSchemas, at most 20). History, the system catalogs and schemas starting pg_ cannot be exposed.
  • Pick a schema per request with Accept-Profile (reads) or Content-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 with api_key_kind_not_allowed.

Preferences

The Prefer 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 _ -> on POST, PATCH or DELETE to make a retry safe. GET, HEAD and PUT are 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, Prefer and 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: 409 idempotency_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: corsOrigins on PATCH /databases/<database id>/data_api, each exactly scheme://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 204 with Access-Control-Allow-Methods: GET, HEAD, POST, PUT, PATCH, DELETE, the allowed request headers (including apikey, authorization and idempotency-key) and Access-Control-Max-Age: 600. From any other origin: 403 cors_origin_not_allowed.
  • Answers to a listed origin echo it in Access-Control-Allow-Origin with Vary: Origin, and expose Content-Range, Content-Location, Location, Preference-Applied, x-request-id and Retry-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 (answers 202), or
  • databases.dataApi.reloadSchema(<database id>) in the SDK, or
  • the project MCP’s data_api_run_schema_reload tool.

Paused databases

Errors

Every refusal is an RFC 9457 problem, Content-Type: application/problem+json:
  • code is stable: it is never renamed or reused. Branch on it, not on detail.
  • postgres is present when the database itself refused the request and the status is below 500. It carries the database’s error code (SQLSTATE) as code. A secret key also gets message, details and hint; a publishable key gets code only.
  • With @supabase/postgrest-js, error.code is this code. Code written for a client that compared error.code with a SQLSTATE such as "23505" reads error.postgres.code instead.

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 as detail. 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:
Pass only the apikey header: the client then sends no Authorization header of its own.

Not supported