Skip to main content
Powabase exposes several HTTP surfaces — the agentic /api/*, PostgREST /rest/v1/*, GoTrue /auth/v1/*, Storage /storage/v1/*, and Realtime. They were built independently (most are upstream Supabase services Powabase ships) and have slightly different conventions. This page documents the shared patterns and where they diverge. For specific endpoints, see the relevant /api-reference/* pages.

Header conventions

Two headers on every authenticated request:
apikey: <Anon Key | Service Role Key>
Authorization: Bearer <Anon Key | Service Role Key | User Access Token>
The apikey header is required by Kong’s key-auth plugin for routing. The Authorization header is what the downstream service (PostgREST, GoTrue, etc.) actually verifies for role assignment. They can be different values: apikey: Anon + Authorization: Bearer <user-access-token> is the standard browser-side pattern after sign-in. For Realtime WebSocket connections, both pieces come in as query parameters: ?apikey=<KEY>&vsn=1.0.0. WebSocket APIs in browsers don’t let you set headers on the upgrade request.

PUT vs PATCH for updates

The agentic /api/* surface is inconsistent here:
ResourceUpdate method
/api/agents/{id}PATCH
/api/tools/{id}PUT
/api/orchestrations/{id}PUT
/api/sources/{id}PATCH
/api/workflows/{id}PATCH
The pattern was that PATCH is partial-update (body keys not present aren’t changed) and PUT is full-replace, but in practice the platform’s PUT endpoints also work as partial updates. The choice is historical, not principled. PostgREST uses PATCH for partial updates and supports PUT only when you specify Prefer: resolution=merge-duplicates (which is really an upsert). GoTrue uses PUT on /user and /admin/users/{id} — both partial. Storage uses PUT to overwrite an object at the same path. If you’re writing client code, match what the docs say for each endpoint. Don’t try to derive PUT-vs-PATCH from first principles.

Path versioning

The BaaS services have versioned paths: /auth/v1/, /rest/v1/, /storage/v1/, /realtime/v1/. The agentic /api/* surface is not versioned in the path. This isn’t a hard product commitment — it’s just where we are today. If the agentic surface needs versioning later, the most likely move is to introduce /api/v2/* alongside the existing /api/*. If you want maximum future-proofing, build a tiny indirection layer in your client code that constructs the URL from a constant.

Resource naming

Most resources use plural-noun URLs:
  • /api/agents/{id} (not /api/agent/{id})
  • /api/knowledge-bases/{id} (with a hyphen)
  • /api/ai-provider-keys (also hyphenated)
A handful of endpoints use snake_case in the path:
  • /api/ai-provider-keys/platform_supported (underscore — this is the one most people get wrong)
  • /api/config/kb-defaults (hyphen)
Match the docs literally. The platform’s routing is case- and character-sensitive.

Error envelopes

The agentic /api/* surface uses {"error": "<message>"} as the standard error envelope. Some endpoints add code or error_code for machine-readable identifiers:
{ "error": "Webhook not found", "error_code": "WORKFLOW_NOT_FOUND" }
{ "error": "BYOK key decrypt failed", "code": "provider_key_decrypt_failed", "provider": "openai" }
PostgREST uses a different shape — a JSON object with code, message, details, hint:
{
  "code": "23505",
  "message": "duplicate key value violates unique constraint",
  "details": "Key (email)=(alice@example.com) already exists.",
  "hint": null
}
The code is the Postgres SQLSTATE (5-character class). GoTrue uses yet another shape — {"error": "code", "error_description": "..."} for OAuth-style errors and {"msg": "..."} for some validation errors:
{ "error": "invalid_grant", "error_description": "Invalid login credentials" }
Storage’s error shape is {"statusCode", "error", "message"}:
{ "statusCode": "400", "error": "InvalidRequest", "message": "..." }
This diversity is unfortunate. If you’re writing a unified error handler in your client, fall back to checking response.status first, then try .error || .message || .error_description || .msg in that order.

Pagination

PostgREST uses HTTP Range headers for offset pagination and the Content-Range response header for the total count:
Range: 0-19
→ Content-Range: 0-19/247
The agentic /api/* surface uses query string pagination with explicit limit and offset params:
GET /api/agents?limit=20&offset=0
 { "agents": [...], "total": 47, "limit": 20, "offset": 0 }
Defaults vary per endpoint — usually limit=50 with a cap of 200. For listing endpoints that return many items (KB sources, source page texts), prefer the agentic endpoints’ explicit pagination. The PostgREST Range approach is fine but easier to forget.

Response shapes for lists

Inconsistency worth knowing about. List endpoints across the agentic surface use a wrapped shape:
{ "agents": [...], "total": N, "limit": L, "offset": O }
But not always. Some return a bare array. Some return { items: [...], total, ... }. The audit-flagged-and-corrected wrong examples in PR A (#8) were spots where this inconsistency tripped the docs themselves. When in doubt, log the response and read its shape. The reference pages should show the actual shape — file an issue if you find one that doesn’t.

Idempotency

The agentic /api/* surface does not honor an Idempotency-Key header (despite the convention being common). Idempotency is computed internally for billing charges (see Billing model) but the API itself doesn’t deduplicate user-supplied retry headers. In practice: retrying a POST /api/agents/{id}/run/stream from a timeout creates a second run. Build your retry logic to handle that — usually by waiting longer between retries, or by checking session state before retrying. PostgREST doesn’t honor Idempotency-Key either. For inserts that need idempotency, use unique constraints + upserts (Prefer: resolution=merge-duplicates).

Common headers worth knowing

A short list of headers that change behavior across multiple endpoints:
HeaderWhereEffect
Prefer: return=representationPostgREST writesReturn inserted/updated rows in response body
Prefer: count=exactPostgREST readsInclude total count in Content-Range
Prefer: resolution=merge-duplicatesPostgREST POSTUpsert semantics
Accept-Profile: <schema>PostgREST readsTarget a non-public schema
Content-Profile: <schema>PostgREST writesSame, for writes
Accept: application/vnd.pgrst.object+jsonPostgREST readsReturn single object, not array; fail if not exactly one row
x-upsert: trueStorage uploadOverwrite existing object
Range: 0-19PostgREST readsOffset-based slicing

Authentication conventions across services

ServiceAuth shape
Agentic /api/*apikey + Authorization: Bearer
PostgREST /rest/v1/*apikey + Authorization: Bearer
GoTrue /auth/v1/*apikey + Authorization: Bearer (Anon Key OR user access token)
Storage /storage/v1/*apikey + Authorization: Bearer
Realtime /realtime/v1/* (WS)?apikey=<KEY> query parameter
Realtime /realtime/v1/api (REST)apikey + Authorization: Bearer headers
/api/webhooks/{id}Authorization: Bearer <webhook_secret> or ?token=<secret> — no apikey
The webhook trigger endpoint is the odd one out: no apikey, only the webhook secret.

Next steps

Auth & Connection

Where the headers all come from and how to assemble them.

Glossary

The terminology that goes alongside the conventions.

Common pitfalls

The errors that come from getting the conventions wrong.

PostgREST advanced

The Prefer and Accept header patterns that change PostgREST behavior.