{"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) \u2014 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 \u2014 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 \u2014 `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 \u2014 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, \u2026). 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 \u2014 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) \u2014 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 \u2014 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 \u2014 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) \u2014 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 \u2014 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 \u2014 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 \u2014 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":[]}]}