Published Sep 16, 2025 ⦁ Updated Oct 11, 2026 ⦁ 16 min read
Supabase REST API: Quickstart with curl and JavaScript

Supabase REST API: Quickstart with curl and JavaScript

Every Supabase project comes with a REST API, the Data API, generated from your Postgres schema by PostgREST. It lives at https://<project-ref>.supabase.co/rest/v1/<table>. You call it with your project's API key in the apikey header, and Row Level Security (RLS) decides which rows each caller can read or change. This guide covers where to find the Supabase API URL and keys, how the new publishable and secret keys differ from the legacy anon and service_role keys, and working curl and supabase-js examples for select, insert, update and delete.

What the Supabase Data API is

According to the Supabase API docs, Supabase builds the API from your database structure. Create a table or a view and it becomes a REST endpoint. Schema changes show up in the API right away, and you don't write any server code. Under the hood it is PostgREST, so the URL filter syntax (?id=eq.1, ?order=score.desc) and the error codes are PostgREST's.

The Data API is what the supabase-js client calls when you run supabase.from('table'). You can also call it directly with curl, fetch, or any HTTP client.

Where to find the Supabase API URL and API keys

  1. Open your project in the Supabase Dashboard.
  2. Click Connect at the top of the project page. The dialog shows the project URL (https://<project-ref>.supabase.co) and the publishable key.
  3. For every key, including legacy ones, go to Settings > API Keys.
  4. The Data API settings (exposed schemas, the API URL) are under Integrations > Data API.

You can also list keys with the CLI: supabase projects api-keys --project-ref <ref>. A local stack (supabase start) prints its own local URL and keys, which are separate from the hosted ones.

Publishable and secret keys vs legacy anon and service_role

Supabase now issues two kinds of keys (API keys docs):

Key Format Where it goes What it can do
Publishable sb_publishable_... Browser, mobile app, CLI Low privilege. RLS limits it.
Secret sb_secret_... Servers, Edge Functions, cron jobs Bypasses RLS. Never ship it to a client.
Legacy anon JWT starting eyJ Same as publishable Same role as publishable
Legacy service_role JWT starting eyJ Same as secret Same role as secret

Things to know:

  • Supabase plans to deprecate the legacy anon and service_role keys by the end of 2026. Creating new keys does not turn the old ones off. You deactivate them yourself in Settings > API Keys once nothing uses them.
  • If a browser sends a secret key, Supabase detects it from the User-Agent and returns 401. That does not protect a leaked key, since anyone can send it with curl. Delete a leaked secret key immediately.
  • Send publishable and secret keys in the apikey header. They are not JWTs, so don't put them in Authorization: Bearer. With the legacy keys, many examples sent the same key in both headers. That pattern doesn't carry over.

Supabase PAT (personal access token) is a different thing. A PAT, created under your account's Access Tokens page, authenticates you to the Management API (api.supabase.com), the CLI and tools that manage projects. It is not used for the Data API and should never be put in an app.

Before your first request: grants and RLS

Two layers decide whether a request works (Securing your API):

  • Grants decide whether a role (anon, authenticated, service_role) can touch the table at all.
  • RLS policies decide which rows that role sees or changes.

Supabase is changing the default grants (announcement). New projects created since May 30, 2026 no longer expose new public tables automatically, and from October 30, 2026 the same applies to new tables in existing projects. Existing tables keep their grants. If your new table returns a permission error, grant access explicitly:

create table public.leaderboard (
  id bigint generated always as identity primary key,
  player text not null,
  score int not null default 0,
  created_at timestamptz not null default now()
);

alter table public.leaderboard enable row level security;

grant select on public.leaderboard to anon, authenticated;
grant insert, update, delete on public.leaderboard to authenticated;

create policy "anyone can read" on public.leaderboard
  for select to anon, authenticated using (true);

create policy "signed-in users can write" on public.leaderboard
  for insert to authenticated with check (true);

Tables created in the Table Editor have RLS on by default. Tables created in SQL do not, so always add the enable row level security line. A table with RLS on and no policy returns no rows to anon and authenticated.

curl examples

Set two shell variables first:

export SB_URL="https://<project-ref>.supabase.co"
export SB_KEY="sb_publishable_..."

Select (with columns, a filter, ordering and a limit):

curl "$SB_URL/rest/v1/leaderboard?select=player,score&score=gte.100&order=score.desc&limit=10" \
  -H "apikey: $SB_KEY"

Common filter operators: eq, neq, gt, gte, lt, lte, like, ilike, in.(a,b), is.null. They all follow the column=operator.value pattern.

Insert (Prefer: return=representation returns the new row):

curl -X POST "$SB_URL/rest/v1/leaderboard" \
  -H "apikey: $SB_KEY" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Prefer: return=representation" \
  -d '{"player": "ada", "score": 120}'

Update (always filter, or you target every row you are allowed to change):

curl -X PATCH "$SB_URL/rest/v1/leaderboard?id=eq.1" \
  -H "apikey: $SB_KEY" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"score": 150}'

Delete:

curl -X DELETE "$SB_URL/rest/v1/leaderboard?id=eq.1" \
  -H "apikey: $SB_KEY" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN"

Upsert: send a POST with Prefer: resolution=merge-duplicates. A row whose primary key already exists is updated instead of rejected.

Auth headers in short

  • apikey is required on every request. It identifies the project and sets the base role.
  • Authorization: Bearer <access_token> is optional. It carries a signed-in user's JWT from Supabase Auth, so RLS policies run as authenticated and auth.uid() returns that user. Without it, the request runs as anon.
  • On a server, sending the secret key in apikey (and no user token) runs as service_role and skips RLS.

The same thing with supabase-js

Install the client with npm install @supabase/supabase-js, then:

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  'https://<project-ref>.supabase.co',
  'sb_publishable_...'
);

// select
const { data: top, error } = await supabase
  .from('leaderboard')
  .select('player, score')
  .gte('score', 100)
  .order('score', { ascending: false })
  .limit(10);

// insert and return the new row
const { data: row } = await supabase
  .from('leaderboard')
  .insert({ player: 'ada', score: 120 })
  .select()
  .single();

// update
await supabase.from('leaderboard').update({ score: 150 }).eq('id', 1);

// delete
await supabase.from('leaderboard').delete().eq('id', 1);

The client adds the apikey header for you. Once a user signs in with supabase.auth, it also sends their access token, so RLS sees them as authenticated. Python works the same way with supabase-py (from supabase import create_client).

Common Supabase API errors

Status / code Usual cause Fix
401 "No API key found in request" Missing apikey header Add the header (or ?apikey= for a quick browser test)
401 "Invalid API key" Wrong project, typo, or a deactivated legacy key Copy the key again from Settings > API Keys
401 with a secret key from a browser Secret keys are blocked in browsers Use the publishable key on the client
401/403 code 42501 "permission denied for table" Role has no grant on the table grant select ... to anon, authenticated
42501 "new row violates row-level security policy" No insert/update policy matches Add a policy, or send the user's access token
200 with [] RLS is on and no select policy matches Add a select policy for that role
404 PGRST205 Table not found (wrong name, or schema not exposed) Check the name and Integrations > Data API exposed schemas
400 PGRST100 Bad query string syntax Check column=operator.value formatting
406 PGRST116 .single() got zero or several rows Use .maybeSingle() or tighten the filter
409 code 23505 Unique constraint violation Upsert, or check for the row first

The full list is in the PostgREST error reference. For a general walkthrough of 4xx debugging, see our guide to 400 Bad Request in REST APIs.

Limitations worth knowing

  • The Data API exposes tables, views and functions, not arbitrary SQL. For complex logic, write a Postgres function and call it at /rest/v1/rpc/<function>.
  • Anything the publishable key can reach is public to anyone who opens your site. Your RLS policies are the only security boundary.
  • Large result sets are capped by the project's max rows setting. Page through them with limit and offset or a Range header.

If you are still choosing a backend, compare it on the Supabase listing on DevHunt or browse free Firebase alternatives. For other APIs you can try without a credit card, see free APIs for developers.

FAQ

What is the Supabase API URL?

It is https://<project-ref>.supabase.co, and the REST endpoint for a table is https://<project-ref>.supabase.co/rest/v1/<table>. You'll find it in the Connect dialog of your project or under Integrations > Data API.

How do I get my Supabase URL and anon key?

Click Connect in your project dashboard to see the URL and the publishable key, which replaces the anon key. The legacy anon key is still listed under Settings > API Keys until you deactivate it, but new code should use the sb_publishable_ key.

Is the Supabase publishable key safe to expose?

Yes, it is meant to be public. It only works as the anon or authenticated role, so what it can do depends entirely on your grants and RLS policies. The secret key and the service_role key must stay on the server.

Is the Supabase API the same as PostgREST?

The Data API is PostgREST running behind Supabase's API gateway. The gateway adds key checks and connects PostgREST to Supabase Auth. The query syntax, headers like Prefer, and error codes are standard PostgREST.

What is a Supabase PAT used for?

A personal access token authenticates you, the account owner, to the Management API and the CLI, for example to create projects or run migrations. It cannot read table data through the Data API, and it should never go into application code.