All documentation

Data API

The tables an access token reaches, the headers every request needs, and how to pick a schema.

Where requests go

Once you hold an access token, you talk to the database directly at https://nvkkanirwnjjvuwxsjch.supabase.co/rest/v1 rather than through our own routes. It is a PostgREST interface over the same tables the dashboard reads, so a table name is a path, a column filter is a query parameter, and the rows you get back are the rows your workspace is allowed to see. The playground on this site drives exactly this surface, and the curl it prints under each call is the call you can paste into a terminal.

Headers

HeaderValueWhen
apikeyThe publishable keyAlways. It identifies the project.
AuthorizationBearer plus the access tokenAlways. It identifies you.
Accept-Profilepublic / calls_historyOn reads, to name the schema.
Content-Profilepublic / calls_historyOn writes, to name the schema.
Preferreturn=representationOptional on a write, when you want the created or updated row back.

Name the schema

Orders, clients, campaigns and account data live in public. Everything the agents produce, calls, transcripts, messages and outcomes, lives in calls_history. An unqualified request resolves against whichever schema is exposed first, and that is not guaranteed to be public, so name it on every call. If you skip it and get a 404 reading Could not find the table 'api.orders', that mismatch is why. In supabase-js the equivalent is .schema("calls_history"), or db: { schema: "public" } once when you create the client.

Tables you can reach

Verbs a table refuses are left out rather than listed and denied. A ledger like credits is written by billing, never by hand.

TableMethodsHolds
public.ordersGET, POST, PATCH, DELETEOrders in your workspace. Creating one here inserts the row directly, which skips the phone validation and agent assignment that POST /api/orders performs, so it will not be queued for a confirmation call.
public.clientsGET, POST, PATCH, DELETEPeople your campaigns call.
public.campaignsGET, POST, PATCH, DELETEOutbound campaigns and whether they are running.
public.campaign_clientsGET, POST, DELETEThe join between a campaign and the clients on it, carrying each one's status and the operation that called them. No workspace column of its own, so it is scoped through the campaign it belongs to.
calls_history.voice_agentsGET, POST, PATCHYour voice and WhatsApp agents. Create and edit them here. is_orders_handler marks the one that picks up new orders, and there should be at most one per workspace. A direct insert skips the plan limit check and the number assignment the Agents page performs, so an agent made here starts with no WhatsApp number attached.
calls_history.operationsGETEvery call and chat the agents ran. Written by the platform as calls happen, so it is read-only here.
calls_history.transcriptsGETWhat was said on a call. Filter by operation with operation_id=eq.<id> rather than listing the table.
calls_history.messagesGETChat turns for an operation. Same idea as transcripts, for the chat channels.
calls_history.leadsGETLeads captured by the agents.
calls_history.outcomeGETHow each operation resolved: confirmed, and the failure_reason when it was not.
calls_history.whatsapp_conversationsGETConversation threads behind the chat operations.
public.creditsGETCredit ledger entries. Read-only: balances move through billing and call accounting, never by hand.
public.subscriptionsGETYour active plan, as Stripe reports it. Written by the billing webhook.
public.workspacesGET, POST, PATCHThe workspaces you belong to. Creating one is a function call, not an insert, and it also writes your owner row in roles.
public.rolesGETWho belongs to which workspace, and as what. Creating a workspace writes your owner row here for you. Membership beyond that is changed through the Members page, not here.
public.notificationsGET, PATCHWorkspace notifications. PATCH hidden to dismiss one.
public.api_keysGETYour own keys. key_hash is not readable by any account, so selecting it should fail. Create and revoke through /api/api-keys, which is session-authenticated.

A read and a write

cURL
# Orders, newest first.
curl "https://nvkkanirwnjjvuwxsjch.supabase.co/rest/v1/orders?select=id,client_name,status,created_at&order=created_at.desc&limit=5" \
  -H "apikey: $PUBLISHABLE_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept-Profile: public"

# Mark one confirmed.
curl -X PATCH "https://nvkkanirwnjjvuwxsjch.supabase.co/rest/v1/orders?id=eq.3f1c..." \
  -H "apikey: $PUBLISHABLE_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Profile: public" \
  -H "Content-Type: application/json" \
  -H "Prefer: return=representation" \
  -d '{ "status": "confirmed" }'

A PATCH or DELETE without a filter applies to every row the policies allow. Always send a filter.

The same thing in supabase-js

TypeScript
import { createClient } from "@supabase/supabase-js"

const supabase = createClient(
  "https://nvkkanirwnjjvuwxsjch.supabase.co",
  process.env.SUPABASE_PUBLISHABLE_KEY!,
  { accessToken: async () => getAccessToken() },
)

const { data: orders } = await supabase
  .from("orders")
  .select("id, client_name, status, created_at")
  .order("created_at", { ascending: false })
  .limit(5)

// Call history lives in the other schema.
const { data: operations } = await supabase
  .schema("calls_history")
  .from("operations")
  .select("id, status, created_at")
  .limit(5)

accessToken is a function and the client calls it before every request, so putting the caching from Authentication inside it is all the token lifetime handling you need. Do not pass the token as a custom Authorization header, that pattern is deprecated.

When something fails

SymptomCause
401 "JWT expired"The token outlived its hour. Exchange the key again.
401 "No API key found in request"The apikey header is missing. Both headers are required, not either one.
PGRST106The schema in your profile header is not exposed by the project.
An empty array where you expected your own rowsThe table's policies scope to something other than your user, or grant nothing to authenticated. Check the same query in the playground.

Inserting orders

One thing the Data API cannot do for you: a row inserted straight into orders is stored and readable, but it skips the phone normalisation and the agent assignment that the dashboard performs, so it is never queued for a confirmation call. Insert directly for testing and for data you only want to read back. Orders that need to be called should be created in the dashboard or through an import until an order creation endpoint that takes an access token exists.