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.
Base URL
Endpoints are served under the api-gateway function on the app's Supabase project:
https://ohtxbovwsyktbjldbjao.supabase.co/functions/v1/api-gatewayAuthenticate with an API key (see Authentication). An
unauthenticated request to any path, known or not, returns 401 — routes are not
enumerable without a key. The CORS preflight is the one exception: an OPTIONS request is
answered 204 before authentication runs, so a browser client can preflight without a key.
Credits
The API draws on the same credit balances as the app. Which owner pays depends on the
operation and its community, not on the key. A community enrichment run is charged to the
target community's billing owner. Search and self-enrich fall back to your own billing
owner, but not unconditionally: when the operation names a community, the usage record carries
that community and the payer is resolved from it, so an organization that owns the community
can cover the charge through standing funding or a delegation budget before it reaches you. A
search that names no community — public or all scope — is not automatically yours
either: it is treated as a global search, and the wallet is resolved from your saved global
search preference if you have one, else your personal balance, else the earliest organization
you joined that has credits to spend. So an organization can pay for a keyless-of-community
search too. (This resolution needs a user behind the key: an organization-owned key logs no
user, so no wallet is pinned for it.) Three operations consume credits
today:
- Search — a search is billed at the in-app simple search rate once its result is
stored, whether that happens inside the initial call or after a
202while you poll. Failed or rejected searches are not billed. Metering is live: the post-hoc biller debits real credits from the completed search's usage record. - Self-enrich — see the note under Identity.
- Community enrichment — each profile a
POST /v1/communities/{id}/enrichrun enriches is billed one marker for the run's depth tier (enrichment_quick,enrichment_standard, orenrichment_deep), to the community's billing owner; see Enrichment (write).
Read GET /v1/me for your live balance, and Settings → Billing in the app for the current
rate card. Hard blocking on an insufficient balance is not enabled today; when it is turned
on, calls return 402 with one of the codes listed under
Error envelope.
Error envelope
Every non-2xx response is a uniform envelope:
{ "error": { "code": "not_found", "message": "…", "request_id": "…" } }| Status | Codes |
|---|---|
| 400 | bad_request, invalid_request |
| 401 | unauthorized |
| 402 | insufficient_credits, coverage_required, community_over_quota (the body also names the payer) |
| 403 | forbidden |
| 404 | not_found |
| 409 | conflict |
| 422 | unprocessable, owner_consent_required |
| 429 | rate_limited |
| 500 | internal |
| 502 | search_unavailable |
This list is not exhaustive — new error codes can be added over time (additive, see
Versioning), so parse unknown codes leniently. 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 operation. Post-hoc metering for this operation is live (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 a
30-day cooldown; a call inside the cooldown returns 429 rate_limited with an
available_at field. 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, "wait_seconds": 20 }scopeis required and is 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.allneeds at least one authorized community. Authorized means your direct memberships plus the communities owned by organizations you manage, archived ones excluded — so belonging to no community directly is not by itself the failing case; managing an organization that owns one is enough. When that combined set is empty, or an allowlist narrows it to empty,allreturns422 unprocessableand searches nothing — it does not fall back to the public corpus. Usepublicfor that.- Scopes are fail-closed and fenced server-side to what your key is authorized for; an
unauthorized
community_idreturns403, never a silent empty result. limitdefaults to 20 and is clamped to a maximum of 50.- Team composition (
include_teams) is not available over the API and is rejected with400.
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
(0–25, default 20) 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}until itsstatusiscompleteorfailed(while it runs the poll reportsrunning). Search jobs are fenced to the key that started them and expire, so an unknown, foreign, or expired id is a404.
Each match carries { profile_id, user_id, name, headline, organization, location, avatar_url, score, semantic_score, explanation, shared_communities }. shared_communities
is empty for scope: "public"; its ids are valid community_id values for the member read
below.
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.
Adding members takes { "members": [ … ], "role": "member" | "admin" } with at most 100
items per request. Each item identifies a person by email, linkedin_url, orcid, or a
name, optionally seeded with headline, bio, organization, and location. Results are
reported per item; a bulk call is never all-or-nothing. The owner role cannot be granted
through the API.
Erasure removes the person's membership, this community's enrichment data and embeddings, and
the community's source identifier for them. A claimed person's account is never deleted. For
an unclaimed placeholder that exists in no other community the handler also tries to remove
the placeholder's auth account; that step is best-effort, and the response reports the
outcome in auth_account_deleted (false when the account could not be removed, with the
erasure itself still complete).
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, workflow_key (currently only
smart_enrich), and dry_run. A dry run returns 200 with eligible_count,
total_members, and a credit estimate (estimated_max_credits_per_profile and
estimated_max_credits, both null when no depth was supplied or the tier is not priced
in the catalog) instead of starting anything. Despite the max in the field names, the
estimate is the exact flat per-tier charge: every enriched profile bills the tier rate
whatever optional steps ran for it. See Enrichment for what each
depth tier does. A run the community's billing owner cannot cover returns 402 once credit
enforcement (or the plan coverage gate) is armed; today, with enforcement off, the run starts
and is debited post-hoc like every other metered operation (see Credits).
Idempotency
Only the two community write POSTs honor an optional Idempotency-Key header (1–255
printable chars, otherwise 400): 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; the
same key while the first call is still in flight returns 409 conflict. Stored responses are
kept for 48 hours, and a 5xx outcome is not stored, so a retry after a server error runs
again.
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. When a version or
endpoint is scheduled for retirement, responses carry Deprecation, Sunset, and Link
headers ahead of time — watch for them.