REST API
The versioned /v1 HTTP surface — search, self-enrichment, community members, and enrichment.
The public REST API is a versioned /v1 HTTP surface served by the api-gateway edge
function. Business logic lives in transport-agnostic handlers shared with the
MCP server, so the two never diverge.
The machine-readable contract is
openapi.yaml at the repo
root — load it into Swagger UI / Redoc or generate a client from it. The narrative reference
is docs/API_V1.md.
This page is a summary; the spec is the field-level source of truth.
Base URL
Endpoints are served under the api-gateway function on your target Supabase project:
https://<project-ref>.supabase.co/functions/v1/api-gateway| Environment | Project ref |
|---|---|
| Production | ohtxbovwsyktbjldbjao |
| Staging | opkcrawzztcovawrteho |
| Dev | udsgxgdircziocvqcvrv |
Authenticate with an API key (see Authentication).
Error envelope
Every non-2xx response is a uniform envelope:
{ "error": { "code": "not_found", "message": "…", "request_id": "…" } }Common codes include bad_request / invalid_request (400), unauthorized (401),
forbidden (403), not_found (404), conflict (409), unprocessable (422),
owner_consent_required (422), rate_limited (429), search_unavailable, and
internal (500). This is not exhaustive — treat the
openapi.yaml error-code
enum as the complete, authoritative list, and parse unknown codes leniently (new codes are
additive, see Versioning). Every response carries an X-Request-Id header —
quote it when reporting an issue.
Endpoints
Identity
| Method & path | Scope | Purpose |
|---|---|---|
GET /v1/me | any | Key identity, owner, scopes, usage, credit balance |
GET /v1/me/profile | profile:read | Read your own profile |
POST /v1/me/enrich | self:enrich | Start a self-enrich run (target-locked; see billing note below) |
GET /v1/me/enrich/runs/{run_id} | self:enrich | Poll a self-enrich run |
Self-enrich billing. A self-enrich via the API is treated as an intentional, cost-aware
call: for a key whose owner is on a paid plan it is billable at 1 credit via the
enrichment_self marker. Post-hoc metering for this marker is live on production (it
debits real credits today) — what is not yet enabled is hard blocking, so an insufficient
balance does not fail the call yet, but it will return a 402 once enforcement is turned
on. Owners not on a paid plan are not charged but are subject to the 30-day cooldown.
Don't assume it is free.
Search
POST /v1/search (scope search:read) runs expert search and returns ranked matches.
{ "query": "battery electrochemistry researchers", "scope": "community",
"community_id": "…", "limit": 20 }scopeis one ofcommunity(needscommunity_id),all(your authorized communities plus public claimed profiles), orpublic(public claimed profiles only). Usecommunityif you want a single-community corpus with no public matches mixed in.- Scopes are fail-closed and fenced server-side to what your key is authorized for; an
unauthorized
community_idreturns403, never a silent empty result.
Async by design. A full search runs embed → retrieve → rerank and can take 35–60s+, longer
than most agent clients' request timeout. So the call waits only up to wait_seconds and then
returns one of:
200 { "status": "complete", "search_id", "matches": [...] }— finished in time.202 { "status": "pending", "search_id", "poll_after_seconds" }— still running; pollGET /v1/search/{search_id}untilcompleteorfailed.
Each match carries { profile_id, user_id, name, headline, organization, location, avatar_url, score, semantic_score, explanation, shared_communities }.
Community members
| Method & path | Scope | Purpose |
|---|---|---|
GET /v1/communities/{id}/members | profile:read | List the roster (keyset pagination) |
GET /v1/communities/{id}/members/{profile_id} | profile:read | Read one enriched member |
POST /v1/communities/{id}/members | member:write | Add people (bulk, per-item results) |
DELETE /v1/communities/{id}/members/{profile_id} | member:write | Community-scoped erasure (DSAR) |
List responses use keyset pagination: pass ?limit= (default 50, max 200) and ?after=
(an opaque cursor), and page until next_cursor is null — that is the only exhaustion
signal.
Enrichment (write)
| Method & path | Scope | Purpose |
|---|---|---|
POST /v1/communities/{id}/enrich | enrich:write | Start a Smart Enrich run on members |
GET /v1/communities/{id}/enrich/runs/{run_id} | enrich:read | Poll a community enrichment run |
The enrich body is all-optional and composable: depth (quick | standard | deep),
profile_ids, joined_since, include_already_enriched, and dry_run (preview the
selection and estimated credits without starting a run). See
Enrichment for what each depth tier does.
Idempotency
Only the two community write POSTs honor an optional Idempotency-Key header (1–255
printable chars): POST /v1/communities/{id}/enrich and POST /v1/communities/{id}/members.
A replay with the same key + same body returns the original stored response verbatim; the same
key with a different body returns 422.
Everything else ignores the header — including POST /v1/me/enrich, the
DELETE …/members/{profile_id} erasure, search, and the reads. In particular, retrying
POST /v1/me/enrich with the same key is not de-duplicated, so a retry after a timeout can
start a second self-enrich run; handle that on the client.
Versioning
/v1 is the stability contract. Only backward-compatible changes ship within it (new
endpoints/fields/enum values/error codes); clients must tolerate unknown fields. Breaking
changes ship only under a new major version (/v2), served side-by-side.