{"openapi":"3.1.0","info":{"title":"ServiceGraph API","version":"0.1.0","description":"Search the ServiceGraph catalog of US professional-services firms. Anonymous /v1/search (IP-rate-limited) returns brief firm cards. Authenticate with email + OTP (free) for full /v1/get/{apex} data. Quota-exhausted authed callers fall back to the anon shape with `quota_exceeded: true`."},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{}},"paths":{"/.well-known/oauth-authorization-server":{"get":{"description":"OAuth 2.0 Authorization Server Metadata (RFC 8414) — discovery endpoint for MCP clients.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/v1/auth/be/{*}":{"get":{"parameters":[{"schema":{"type":"string"},"in":"path","name":"*","required":true}],"responses":{"200":{"description":"Default Response"}}},"post":{"parameters":[{"schema":{"type":"string"},"in":"path","name":"*","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/auth/register":{"post":{"description":"OAuth 2.0 Dynamic Client Registration (RFC 7591). Public clients only — no client_secret is issued.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"client_name":{"type":"string","maxLength":200},"redirect_uris":{"type":"array","items":{"type":"string"},"maxItems":10},"grant_types":{"type":"array","items":{"type":"string"}},"response_types":{"type":"array","items":{"type":"string"}},"scope":{"type":"string"},"token_endpoint_auth_method":{"type":"string"}}}}}},"security":[],"responses":{"200":{"description":"Default Response"}}}},"/v1/auth/authorize":{"get":{"description":"OAuth 2.1 authorization endpoint. Validates the request, requires a signed-in session (redirects to /signin if absent), then issues a single-use authorization code bound to the client + PKCE challenge.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/v1/auth/token":{"post":{"description":"OAuth 2.1 token endpoint. Supports authorization_code and refresh_token grants.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/v1/auth/revoke":{"post":{"description":"OAuth 2.0 token revocation (RFC 7009). Accepts an access or refresh token.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/v1/me/":{"get":{"description":"Identity, plan, and today's usage for the authed user.","responses":{"200":{"description":"Default Response"}}}},"/v1/me/accounts":{"get":{"description":"Lists the auth providers linked to the caller (e.g. \"google\", \"credential\"). Useful for showing the \"Connected accounts\" section on /profile.","responses":{"200":{"description":"Default Response"}}}},"/v1/datasets":{"get":{"description":"List of available datasets. Tile-sized payload (id, label, description, unlock price, TTL, row count) suitable for an index page. Call /v1/datasets/:id for the full schema (field specs + filters) of one dataset.","responses":{"200":{"description":"Default Response"}}}},"/v1/datasets/{id}":{"get":{"description":"Full schema for one dataset: brief field specs (free, returned in search), detail field specs (returned only when the caller has an active unlock for the row), allowed filters, per-unlock price and TTL. Drives the dataset search page and reference docs.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/datasets/{id}/search":{"get":{"description":"Search a single dataset, returning brief rows + per-row unlock state. Dataset id comes from the URL — `kind:` predicates in the `filter` query param are rejected with 400 (the URL is authoritative). Empty `filter` is allowed (returns all rows of the dataset). To see the dataset's allowed filters and brief field shape, call /v1/datasets/:id.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/datasets/{id}/{apex}":{"get":{"description":"Fetch one row from a dataset. Always returns the brief; the detail block is present only when the caller has an active unlock for (user, dataset_id, apex). Idempotent — never charges. To unlock, POST to /v1/datasets/:id/unlocks.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string","minLength":3,"maxLength":253},"in":"path","name":"apex","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/datasets/{id}/unlocks":{"post":{"description":"Unlock detail data for one or more rows in a dataset. Atomic: either all uncached apexes in the request are unlocked + charged, or none are (402 if balance < total cost). Already-unlocked rows return was_cached=true with no additional charge. Returns brief + detail data for every requested row.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["apexes"],"properties":{"apexes":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/datasets/{id}/fields":{"get":{"description":"Filter field catalog for one dataset: name + kind + operators + description for every field the dataset's /search will accept. Default response omits value lists; pass include_values=1 to expand them inline (cheap for short enums, slow for service_provided). For longer enums prefer /v1/datasets/:id/values/:field. Also returns the DSL grammar string so an agent can prime a single session with one call.","parameters":[{"schema":{"type":"string"},"in":"query","name":"q","required":false},{"schema":{"type":"string","enum":["0","1"]},"in":"query","name":"include_values","required":false},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/datasets/{id}/values/{field}":{"get":{"description":"Enumerate distinct values for one filter field, scoped to rows in this dataset. Use this to populate value pickers (state dropdowns, industry typeaheads, …). Pagination via limit + offset; substring search via q.","parameters":[{"schema":{"type":"string","maxLength":100},"in":"query","name":"q","required":false},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","minimum":0,"default":0},"in":"query","name":"offset","required":false},{"schema":{"type":"string"},"in":"path","name":"id","required":true},{"schema":{"type":"string"},"in":"path","name":"field","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/datasets/{id}/check":{"get":{"description":"Validate a DSL filter string against this dataset. Returns {valid:true, normalized} on success, {valid:false, error} on parse or validation failure. Filter cannot reference `kind:` (the URL is authoritative) and can only use fields in this dataset's allowed filter list — useful for live syntax-checking in agent UIs.","parameters":[{"schema":{"type":"string","minLength":1},"in":"query","name":"filter","required":true},{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/datasets/{id}/translate-intent":{"post":{"description":"Translate a plain-English intent into a DSL filter for this dataset. Result includes the LLM-produced filter, a one-sentence reasoning, validity against the dataset's allowed fields, and a sanity-check row count.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["intent"],"additionalProperties":false,"properties":{"intent":{"type":"string","minLength":1,"maxLength":500},"model":{"type":"string","maxLength":80}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/me/credits":{"get":{"description":"Current credit balance for the authenticated user.","responses":{"200":{"description":"Default Response"}}}},"/v1/me/credits/transactions":{"get":{"description":"Paginated spend history for the authenticated user. Each row carries delta + balance_after + reason; unlock charges also surface the (dataset_id, apex, expires_at) tuple of the row the charge bought.","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","minimum":0,"default":0},"in":"query","name":"offset","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/v1/lists/":{"get":{"description":"Paginated list of the caller's firm lists. Pass `?contains_apex=foo.com` to also include `item_id` per list (uuid when the list contains the apex, null otherwise) — used by the \"Add to list\" modal to render one-click toggles.","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","minimum":0,"default":0},"in":"query","name":"offset","required":false},{"schema":{"type":"string","minLength":3,"maxLength":253},"in":"query","name":"contains_apex","required":false}],"responses":{"200":{"description":"Default Response"}}},"post":{"description":"Create a new (private) firm list. Lists are scoped to a single dataset — the dataset_id controls default columns, sort order, and per-row unlock pricing. Optionally seed with custom columns (the client picks a preset, the server stores it verbatim).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","dataset_id"],"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":2000},"dataset_id":{"type":"string","minLength":1,"maxLength":50,"description":"Dataset id this list is scoped to (e.g. pro_services, newsletter). Must match an id in /v1/datasets."},"columns":{"type":"array","maxItems":30,"items":{"type":"object","required":["key","label","type"],"additionalProperties":false,"properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["text","longtext","number","date","bool","url","select","multiselect","catalog"]},"options":{"type":"object"}}}}}}}}},"responses":{"200":{"description":"Default Response"}}}},"/v1/lists/fields":{"get":{"description":"Catalog field metadata — the set of public fields a user can reference in a `catalog`-type column. Used by the column-add UI to render a field picker without duplicating the registry on the client. Shape mirrors filter/fields.ts. Pass `dataset=<id>` to scope the result to the fields that dataset surfaces (its brief + detail fields) — so a list only offers columns relevant to its own kind. An unknown id returns the full set.","parameters":[{"schema":{"type":"string"},"in":"query","name":"dataset","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/v1/lists/memberships":{"get":{"description":"Batch list-membership lookup. For each apex, returns the lists (belonging to the caller) it appears in along with the per-item `values` (user-edited custom-column cells). A sidecar `lists` map carries the active column definitions for every list referenced, so callers can render `label: value` pairs without a follow-up fetch. Used to render list memberships on /search tiles + firm detail pages. Pass `apex` once for a single firm, or multiple times for a batch (max 200).","responses":{"200":{"description":"Default Response"}}}},"/v1/lists/{id}":{"get":{"description":"Full list contents: metadata, columns, items (with CH enrichment). Pass `include_deleted_columns=1` to also receive soft-deleted columns (used by the undo UI).","parameters":[{"schema":{"type":"string","enum":["0","1"]},"in":"query","name":"include_deleted_columns","required":false},{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}},"patch":{"description":"Update list metadata. `sort` is the table-sort state; null clears it (manual / position order). The server does not validate `sort.key` against the active column set — clients render position order when the referenced column is missing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000},"sort":{"oneOf":[{"type":"null"},{"type":"object","required":["key","direction"],"additionalProperties":false,"properties":{"key":{"type":"string","minLength":1,"maxLength":100},"direction":{"type":"string","enum":["asc","desc"]}}}]}}}}}},"parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}},"delete":{"description":"Hard-delete the list and all its columns/items.","parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/lists/{id}/items":{"post":{"description":"Bulk-add firms by apex. Apexes not in the catalog are still accepted (`in_catalog: false`). Apexes already in the list are returned in `duplicates` and not re-inserted.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["apexes"],"additionalProperties":false,"properties":{"apexes":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"string","minLength":3,"maxLength":253}}}}}}},"parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/lists/{id}/items/{item_id}":{"patch":{"description":"Update an item: values and/or position.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"values":{"type":"object"},"position":{"type":"integer","minimum":0}}}}}},"parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"id","required":true},{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"item_id","required":true}],"responses":{"200":{"description":"Default Response"}}},"delete":{"description":"Remove a firm from the list.","parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"id","required":true},{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"item_id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/lists/{id}/columns":{"post":{"description":"Add a user-defined typed column to a list. `key` is the stable identifier in items.values; `label` is the display name. select/multiselect columns require options.choices.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["key","label","type"],"additionalProperties":false,"properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["text","longtext","number","date","bool","url","select","multiselect","catalog"]},"options":{"type":"object"},"position":{"type":"integer","minimum":0}}}}}},"parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/v1/lists/{id}/columns/{col_id}":{"patch":{"description":"Rename, reorder, or update options on a column. Type changes are not supported — add a new column and migrate values manually.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"label":{"type":"string"},"options":{"type":"object"},"position":{"type":"integer","minimum":0}}}}}},"parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"id","required":true},{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"col_id","required":true}],"responses":{"200":{"description":"Default Response"}}},"delete":{"description":"Hard-delete a column. Removes the column row and strips its key from every item.values blob in the list, atomically. Cannot be undone — clients should confirm before calling.","parameters":[{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"id","required":true},{"schema":{"type":"string","pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"},"in":"path","name":"col_id","required":true}],"responses":{"200":{"description":"Default Response"}}}}},"servers":[{"url":"https://api.servicegraph.co"}],"security":[{"bearerAuth":[]}]}