JOGL Network Docs

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-gateway

Authenticate 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 202 while 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}/enrich run enriches is billed one marker for the run's depth tier (enrichment_quick, enrichment_standard, or enrichment_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": "…" } }
StatusCodes
400bad_request, invalid_request
401unauthorized
402insufficient_credits, coverage_required, community_over_quota (the body also names the payer)
403forbidden
404not_found
409conflict
422unprocessable, owner_consent_required
429rate_limited
500internal
502search_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 & pathScopePurpose
GET /v1/meanyKey identity, owner, scopes, usage, credit balance
GET /v1/me/profileprofile:readRead your own profile
POST /v1/me/enrichself:enrichStart a self-enrich run (target-locked; see billing note below)
GET /v1/me/enrich/runs/{run_id}self:enrichPoll 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.

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 }
  • scope is required and is one of community (needs community_id), all (your authorized communities plus public claimed profiles), or public (public claimed profiles only). Use community if you want a single-community corpus with no public matches mixed in.
  • all needs 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, all returns 422 unprocessable and searches nothing — it does not fall back to the public corpus. Use public for that.
  • Scopes are fail-closed and fenced server-side to what your key is authorized for; an unauthorized community_id returns 403, never a silent empty result.
  • limit defaults to 20 and is clamped to a maximum of 50.
  • Team composition (include_teams) is not available over the API and is rejected with 400.

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; poll GET /v1/search/{search_id} until its status is complete or failed (while it runs the poll reports running). Search jobs are fenced to the key that started them and expire, so an unknown, foreign, or expired id is a 404.

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 & pathScopePurpose
GET /v1/communities/{id}/membersprofile:readList the roster (keyset pagination)
GET /v1/communities/{id}/members/{profile_id}profile:readRead one enriched member
POST /v1/communities/{id}/membersmember:writeAdd people (bulk, per-item results)
DELETE /v1/communities/{id}/members/{profile_id}member:writeCommunity-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 & pathScopePurpose
POST /v1/communities/{id}/enrichenrich:writeStart a Smart Enrich run on members
GET /v1/communities/{id}/enrich/runs/{run_id}enrich:readPoll 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.

On this page