Authentication & Scopes
API key header
Every /v1/* request must send the key in the X-API-Key header. There is no OAuth, no bearer token, and no cookie session on this API.
curl "https://rjpxllsycikfabazvkdh.supabase.co/functions/v1/directory-api/v1/categories" \
-H "X-API-Key: your-api-key-here"| Condition | Status | Body |
|---|---|---|
No X-API-Key header | 401 | { "error": "Missing X-API-Key header" } |
| Unknown or deactivated key | 401 | { "error": "Invalid API key" } |
expires_at in the past | 401 | { "error": "API key expired" } |
| Per-minute limit exceeded | 429 | { "error": "Rate limit exceeded" } |
| Key lacks the required scope | 403 | { "error": "Forbidden: <scope> required" } |
Complete scope listChanged in 1.1.0
A key carries an array of scopes. The wildcard * satisfies every check. These 15 scopes plus * are everything the API enforces. Since 1.1.0 the three internal-visibility scopes can be granted explicitly on a key; the access rules themselves are unchanged.
| Scope | Unlocks | Sensitivity |
|---|---|---|
businesses.read | GET /v1/businesses, /v1/businesses/:id, /v1/businesses/slug/:slug, sub-resources | Public data |
businesses.write | POST /v1/businesses, PUT /v1/businesses/:id, POST /v1/businesses/:id/performance | Mutates the directory |
businesses.internal.read | Draft / admin_only businesses, and honours ?visibility= on list + search | Internal — non-public data |
businesses.assets.read | GET /v1/businesses/:id/assets (may include internal documents) | Internal — non-public data |
products.read | GET /v1/products, /v1/products/:id, /v1/businesses/:id/products, price history | Public data |
products.write | POST /v1/products, PUT /v1/products/:id | Mutates the catalog |
products.internal.read | Non-active / admin_only products on list, detail, business products, price history and search | Internal — non-public data |
categories.read | GET /v1/categories, /v1/categories/:id, /v1/product-categories | Public data |
regions.read | GET /v1/regions, /v1/regions/:id, /v1/countries | Public data |
search | GET /v1/search, /v1/semantic-search, /v1/hybrid-search | Public data |
rfq.read | GET /v1/rfq, /v1/rfq/:id, /v1/rfq/:id/quotes, /v1/rfq/:id/quotes/:supplierId | Buyer contact data |
rfq.write | Create RFQs, update RFQs, attach/detach suppliers (also satisfies RFQ list/detail reads) | Buyer contact data |
quotes.write | POST /v1/rfq/:id/quotes — record a supplier quote | Pricing data |
agent.query | The Agent Gateway (separate base URL) | AI surface |
webhooks.manage | POST/GET/DELETE /v1/webhooks | System |
* | Everything above, including all internal scopes | Dangerous — first-party only |
Do not hand out * to third parties. It silently grants the three internal scopes, which expose draft listings and internal business documents. Grant the internal scopes explicitly when a trusted first-party integration genuinely needs them.
Visibility rules
- Without
businesses.internal.read, business list/detail/slug/search results are filtered tovisibility = public, and any?visibility=query parameter is silently ignored rather than rejected. - Without
products.internal.read, product results are filtered tostatus = activeANDvisibility = public, on list, detail, business products, price history and search. /v1/semantic-searchand/v1/hybrid-searchforceinclude_internal = falsefor every caller, including*keys. A caller-suppliedinclude_internalparameter has no effect.GET /v1/businesses/:id/assetsrequiresbusinesses.assets.read(or*) and returns 403 otherwise.GET /v1/businesses/:id/capabilitiesapplies no visibility filter — any key withbusinesses.readsees all capability rows for that business. Filter on your side if that matters to you.
Rate limiting — honest description
Each key has a rate_limit_per_minute (default 60). The limiter counts requests in a rolling 60-second window in memory, per running function instance. It therefore resets on cold start and is not shared between concurrently running instances.
Treat rate limiting as best-effort throttling, not a hard contractual guarantee. The effective ceiling can be higher than the configured number under load, and requests can be rejected at 429 sooner than you expect after a burst. Always handle 429 with exponential backoff.
No X-RateLimit-* response headers are sent. The only signal is the 429 status.
Request logging
Every authenticated request is logged (method, path, status, latency, IP) and the key's last_used_at is refreshed. Ask the Buyamia team if you need a usage report for your key.