Dealroom API consumer guide
How to build against the Dealroom next-gen REST API: authenticate, pick the right
endpoint, and discover filters from the live API instead of guessing.
This skill covers Programmatic (M2M) API usage. Application (PKCE) keys for browser
SPAs also exist (read-only, created from the same settings page), but this skill does not
cover that flow.
This skill holds only the durable parts (auth, endpoint judgment, discovery mechanism,
pointers). Endpoint shapes, the full filter catalog, and field lists live in the API
itself and the docs, which are the single source of truth. Always read those for
specifics rather than relying on memory.
Read this before building anything
Two failure modes cause almost every stuck integration. Avoid both:
- Don't guess endpoint paths, filter keys, or operator syntax. Hallucinated names
that "look right" return empty results or s. Discover filters at runtime with
GET /reference/filters?scope=<scope>
and confirm shapes against the OpenAPI spec.
- Don't default to aggregate endpoints. This is the most common mistake. Most
questions want records, not a computed statistic. See below.
This is an early-access API and can change without notice. If the live behavior conflicts
with this skill, trust the API and flag it: see
When the API disagrees with this skill.
Choosing the right endpoint
The most important decision. Pick by what the answer is, not by how analytical the
question sounds.
Rule of thumb: If the user wants to see things (a list of companies, the rounds
of one startup, who invested in X), use a transactional / list endpoint. If they
want a number or a chart of numbers computed across many rows (count, sum, average,
median, distribution, trend, cross-tab), use an aggregate endpoint. When in doubt,
start transactional.
List endpoints already return rich nested objects (funding summary, latest valuation,
tags, founders) and a
count, so you rarely need a separate aggregate just to
enrich or count a result set.
| The user wants | Use | Not |
|---|
| A list of companies / investors / people matching criteria | (or , , ) | aggregate |
| The "top N by funding / valuation / signal" | list + + | aggregate group_by |
| Everything about one entity | | aggregate |
| One entity's rounds / valuations / investors / portfolio / team | typed collections (see below): GET /data/companies/{id}/{funding-rounds,valuations,investors,team}
, /data/investors/{id}/{portfolio,funds}
| aggregate |
| All funding rounds matching criteria | | aggregate |
| All valuations matching criteria (cross-entity) | | aggregate |
| Fund vehicles investor firms have raised (cross-manager) | | |
| Points to plot on a map | GET /data/{companies,investors,universities}/geo
(slim dots; see below) | a full list call you then thin client-side |
| How many entities match (just the count) | the list call's () | an aggregate for a bare count |
| A count / sum / avg / median grouped by a dimension | GET /analytics/aggregate/{source}
| paging the list and reducing client-side |
| Several metrics at once (KPIs, leaderboards) | GET /analytics/aggregate/{source}/multi-metric
| many separate calls |
| A 2D matrix, stage transitions, or a per-year trend | GET /analytics/funding-analytics/{heatmap,round-transitions,funnel}
, | |
| Fuzzy name lookup ("find Stripe") | (all five collections; narrow with ; returns a flat array, each row carries its ) | a name filter |
| Ranked investor recommendations for a target company | GET /analytics/matching/investors
| hand-rolled portfolio-overlap queries |
| Companies / investors similar to a given one | GET /data/companies/{id}/similar
, /data/investors/{id}/similar
| building your own tag-overlap ranking |
Anti-patterns:
- Ranking entities via aggregate. groups by a dimension (country, year,
sector), not by entity. To rank companies, use the list endpoint with .
- Aggregating to count. A list response already returns .
- Aggregating one entity. Profile data lives on the typed-collection sub-resources
(see below).
- The reverse mistake: paging thousands of list rows to sum/average client-side. That
is exactly what
GET /analytics/aggregate/{source}
is for.
Relationship sub-resources are facet-scoped by entity type. They are
not on
(that path carries only the detail record plus
).
Read the entity's
/
/
/
flags
from the detail payload, then call the matching typed collection. The paths are static
and knowable:
- Companies (): , ,
, , , , ,
- Investors (): , ,
, ,
- People / founders / universities: ,
/data/founders/{id}/founded-companies
, /data/universities/{id}/alumni
,
and on universities / gov-ngo
The
collections rank by weighted tag overlap (force-sorted by score), accept
the full company/investor filter DSL to narrow the pool, and use offset pagination
capped at
.
Map points have their own slim lens. GET /data/{companies,investors,universities}/geo
returns one point per entity (id, name, coordinates) instead of the full list payload, takes
the same
as its list endpoint, and accepts
size_by=<numeric dimension>
(e.g.
,
,
,
,
,
,
) to return each point's
for proportional sizing.
also sorts descending, so a capped response keeps
the highest-value points; entities without usable coordinates are omitted. For per-area
counts (a choropleth rather than dots) use
GET /analytics/aggregate/companies?metric=count&group_by=map_area
instead. The older
generic
still exists (no
, higher limits) but prefer
the per-collection endpoints.
Setup
Steps 1-2 apply to both modes; run them on the first turn and skip what is already done.
Step 3 is only for app builds (see
Two ways to call the API).
- Generate an API key (the user must do this). Auth0 needs a logged-in browser, so
you cannot do it for them. Tell them: open
https://beta.dealroom.app/settings/api, click + Create key, choose
Programmatic (M2M), and copy both and (the secret is
shown only once). The API is in closed beta: if that page shows a waitlist sign-up
instead of + Create key, the account has no API access yet - the user should join
the waitlist and wait for the enablement email; there is no way around this gate.
- Store the credentials in . Copy and fill in
and . Optionally set
for server-side observability. Confirm is in .
- App builds only - copy a client snippet. Python:
cp <skill-path>/assets/snippets/dealroom.py ./
then pip install authlib requests python-dotenv
. Node/TS: copy plus
and (the ESM config the snippet needs), then
and verify with . Both read , send the headers, and
mint and refresh tokens automatically. For one-off conversational queries, skip this and
use curl (next section).
Two ways to call the API
Match the tool to the job. Do not scaffold a project or write a script just to answer a
question; do not hand-mint tokens in a loop inside a real program.
Conversational / ad-hoc - you answering a question now: run curl directly. This is the
default when the user asks you to look something up, explore, or sanity-check data. Mint one
token per session, reuse it across calls, and pass each filter with
so the
,
, and
metacharacters survive the shell. No files, no
project, no snippet.
bash
# Load credentials and mint a token ONCE per session (24h lifetime); reuse $TOKEN after.
# NOTE: the audience is NOT the API base URL - it stays the legacy Auth0 API
# identifier (https://api-next.beta.dealroom.co) even though requests go to
# api.beta.dealroom.app. See the environment table below.
set -a && . ./.env && set +a
TOKEN=$(curl -s https://accounts.beta.dealroom.co/oauth/token \
-H 'Content-Type: application/json' \
-d "{\"grant_type\":\"client_credentials\",\"client_id\":\"$DEALROOM_CLIENT_ID\",\"client_secret\":\"$DEALROOM_CLIENT_SECRET\",\"audience\":\"https://api-next.beta.dealroom.co\"}" \
| jq -r .access_token)
curl -s -G 'https://api.beta.dealroom.app/data/entities' \
--data-urlencode 'filter=and(organization_subtype[eq]:company,tag_id[in_any]:42|99)' \
--data-urlencode 'sort=-total_funding' --data-urlencode 'limit=5' \
-H "Authorization: Bearer $TOKEN" -H "X-Client-Id: $DEALROOM_CLIENT_ID" | jq
Building an app or anything repeated - code that outlives the session: use the snippet.
Copy
/
into the project. It reads
, sends the headers, and
re-mints the token on a
automatically - which raw curl will not do when the 24h token
expires mid-run. This is the right path inside any program, loop, or multi-query tool.
Pitfall either way: never put raw in a curl URL string. Use
per parameter (or the snippet's structured params). Bare brackets in the
URL are the single biggest cause of agents writing throwaway escape scripts.
Authentication
- Flow: OAuth2 client-credentials (machine-to-machine). Exchange /
for a Bearer token, then send it on every call. The snippets do this
for you.
- Two mandatory headers on every request:
Authorization: Bearer <token>
and
(missing it is a ). A custom is optional
and useful for server-side observability, but it is not part of authentication.
- Token lifetime: 24h. Cache and reuse; do not mint per call. Snippets refresh on
. A key deactivated in the UI is rejected on its next request even while its token is
still inside that 24h window, so a that survives one refresh means the key is dead,
not expired: stop and tell the user rather than re-minting in a loop.
- Mutations (POST / PATCH / PUT / DELETE) additionally need the relevant write/delete
permission on the key.
A missing token does not fail loudly - check that your auth applied
Read endpoints also serve
anonymous callers (that is how public ecosystem pages work), so
a request with no or a dropped
header returns
with a thinner row rather
than
. Two markers tell you which principal the API actually saw:
- - / / , alongside and
. Present only for non-M2M callers on capped resources.
- - a top-level array of
{ field, reason, unlock }
for fields redacted (nulled,
not omitted) at that tier, e.g. { "field": "website", "reason": "ACCOUNT_REQUIRED", "unlock": "signup" }
. Added only when something was actually redacted.
A of or a on a call you made with a key means your
credentials did not apply. Fix the headers; do not report the nulls as missing data. M2M
callers are exempt from field redaction and from the per-tier page-size and pagination-depth
caps below, so a correctly authenticated response has neither marker.
The default environment is
beta. For other environments, swap the base URL, Auth0
host, and audience per this table.
The OAuth2 is NOT the API base URL on
beta/production: the platform moved to
but the Auth0 API identifier kept
its legacy
value. Minting a token with the base URL as audience
fails.
| Environment | API base URL | Auth0 host | OAuth2 |
|---|
| Beta | https://api.beta.dealroom.app
| accounts.beta.dealroom.co
| https://api-next.beta.dealroom.co
|
| Production | | | https://api-next.dealroom.co
|
| Staging | https://api-next.staging.dealroom.dev
| accounts.staging.dealroom.dev
| https://api-next.staging.dealroom.dev
|
The former
api-next.beta.dealroom.co
/
hosts are retired
and no longer serve traffic - update any old base URLs to the
hosts. The
strings live on only as Auth0 audience identifiers.
API versioning
The API is
date-versioned (Stripe-style). Send an optional
header to pin behavior;
omit it to get the latest version (what new integrations
should do). Clients pinned to an older date keep their old request/response shapes via
server-side transforms until that version's sunset date, so existing code does not break
when the API moves.
There is no path prefix. Every namespace is served at the root of the API
host:
,
,
,
,
. Old
URLs from earlier integrations are permanently redirected (
, all
versions, no sunset), so they still work - but write new code against the root paths
and drop
from any base URL you find in existing code.
Every breaking change, deprecation, and addition is listed in the
changelog with the version
date and affected endpoints.
If you are returning to a project built against an earlier
version of this skill, or anything here looks stale, read the changelog first - it is
the fastest way to see what moved. This skill deliberately keeps no per-version change
list: the live changelog is the single source of truth for what changed when.
Constructing queries
Filter grammar
All list and aggregate endpoints take a
query parameter:
text
filter=key[op]:value # single
filter=and(key1[op]:val1,key2[op]:val2) # AND (comma-separated args)
filter=or(key1[op]:val1,key2[op]:val2) # OR
filter=and(tag_id[eq]:42,or(location[eq]:1234,location[eq]:5678)) # nested
Operators:
,
,
,
,
,
, and the multi-value
/
/
/
(pipe-separated, e.g.
).
/
apply only to junction filters (tags, growth stages). Booleans are strings (
/
). Relationship-path filters reach related entities with
(one hop) and
(two hops), e.g.
founder.gender[eq]:female
,
funding_round__investor.total_invested[gt]:1000000
.
Entity classification (the legacy
flags were removed):
is
or
;
is
,
,
,
or
; role flags
/
/
/
stack
on top. So "companies" is
organization_subtype[eq]:company
, "investment firms" is
organization_subtype[eq]:investor
, "people" is
. (The investor-firm
subtype was renamed from
to
;
now refers only to the investment
vehicle and is no longer a valid
value.)
The exact key list, operators, and value types per scope are not memorized here. Discover
them live (next section) or read the
Filters & Sorting reference.
Discover filters and resolve IDs (do not guess)
There are two ID families - never mix them up:
- Taxonomy IDs are numeric (locations, industries, tags, degrees, backgrounds), not
strings:
location[eq]:United+States
returns nothing; works.
- Entity IDs are UUIDs - every path param and every entity-reference filter
(, , , , and
relationship paths like ). An integer where a UUID is
expected fails validation or matches nothing.
Discover and resolve at runtime:
bash
GET /reference/filters?scope=companies # valid filter keys, operators, types, data status
GET /reference/filters/location/values?q=netherlands # resolve a location to its ID
GET /reference/filters/tag_id/values?q=climate # resolve a tag across ALL taxonomy types
GET /reference/filters/search?q=climate&scope=companies # one-shot value search across every filter key
Valid scopes:
,
,
,
,
,
,
. Cache resolved IDs in your app; taxonomy changes rarely.
Build filters from , not the displayed . returns tag
entries whose
is category-qualified (
,
,
, ...) but whose
is the bare
. The filter grammar
only accepts the bare form:
works;
tag_id:sector[eq]:2181301
throws
("Expected LBRACKET but got COLON"). Always construct filter
expressions from each entry's
.
Resolve tags without forcing a . A tag's category is not always what you expect -
"Climate Tech" is a
, not an
, so
…/values?q=climate&type=industry
returns
. Omit
to search every taxonomy at once; each result is labelled with its
own
. Only pass
to disambiguate. Note: do not trust
from these
value lookups (it can read
even for tags that match hundreds of entities on beta) -
confirm real counts with the list call's
.
Pagination, sorting, currency
- Pagination: offset-based ( / ). Some list responses also return
for keyset pagination; round-trip it opaquely.
skips the count for faster lists.
- Pagination depth is capped for non-M2M callers. above the caller's tier
ceiling (anonymous 750 / free 5,000 / premium 50,000, raisable per ecosystem) is a
PAGINATION_DEPTH_EXCEEDED
on both the offset and cursor paths - an error, not a silent
clamp. M2M keys are exempt. Page size, by contrast, clamps silently for non-M2M callers and
reports it via .
- Sorting: (prefix for descending, comma-separated).
- Currency: converts thresholds and amounts (default USD). Field
names stay base names (no suffix); every response has a top-level .
Limit maximums, sort columns, and response field lists vary by endpoint and are documented
in the OpenAPI spec, not here.
Live references
These are the single source of truth. Fetch the slice you need (
or
); do
not paste whole pages or the full spec into context.
| Need | Where |
|---|
| Enumerate namespaces / resources at runtime | lists the five namespaces; each namespace index (, , ...) lists its resources |
| Exact request/response shape for any endpoint | https://developers.beta.dealroom.co/openapi.yaml
(raw OpenAPI, YAML - slice with `yq '.paths |
| Browsable endpoint reference | the API Reference tab on https://developers.beta.dealroom.co
(the API host's redirects there; the old on-host Swagger UI at is no longer public) |
| Guides + concepts (filtering, aggregates, pagination, rate limits) | https://developers.beta.dealroom.co
|
| Full filter + sorting catalog | https://developers.beta.dealroom.co/references/filters-and-sorting
|
| Known limitations (stub / no-data endpoints + filters) | https://developers.beta.dealroom.co/concepts/known-limitations
|
| Changelog (breaking changes, deprecations, new features per version) | https://developers.beta.dealroom.co/changelog
|
| MCP server (Dealroom data as MCP tools for agent clients) | https://developers.beta.dealroom.co/agent-apis/mcp
|
Some advertised endpoints and filters are stubbed or not fully data-loaded yet, and the
set changes over time. Check the known-limitations page, the
extension in
the OpenAPI spec, or the
field from
GET /reference/filters?scope=<scope>
before relying on a surface in production.
Common errors
| Symptom | Cause and fix |
|---|
| Token expired (24h). The snippet auto-refreshes. |
| that persists after one refresh | The key was deactivated or deleted. Re-minting will not help; ask the user to check https://beta.dealroom.app/settings/api. |
| mentioning | The required client-id header is missing or does not match the token. |
| / | Filter key wrong for this scope. Call GET /reference/filters?scope=<scope>
. |
| / | Enum filter value outside the known set (, , , ...). Values match case-insensitively by display name or code; discover them via GET /reference/filters/{key}/values
. |
/ PAGINATION_DEPTH_EXCEEDED
| past the tier depth cap (non-M2M only). Narrow the filter and partition the query instead of paging deeper. |
| with nulled fields and a | The call was treated as anonymous or free. Your credentials did not apply - fix the headers. |
| Empty from a sane-looking filter | The value did not resolve to a real ID. Look it up via /reference/filters/{key}/values
. |
| Rate limit. Back off, honor , cache taxonomy lookups. |
| 15s query timeout. Narrow the filter or set . |
Early access: data caveat
This skill targets
, where data may be refreshed or partially
loaded. If a single result looks off, say so honestly rather than inventing an explanation,
and sanity-check the same query in the production Dealroom UI before debugging further.
When the API disagrees with this skill
This is an early-access API: endpoints, filter keys, fields, response shapes, and auth
details can change without notice. The live API and its docs are authoritative; this
skill is not. When reality and this skill conflict, trust the API and surface the gap.
Treat these as drift signals (not normal data issues):
- A path documented here returns / , or a method that worked is rejected.
- A filter key this skill names returns on a scope where it should work.
- The response envelope differs from what is described (e.g. renamed, fields
missing or restructured, shape changed).
- Valid credentials no longer authenticate (header names, audience, or token flow changed).
GET /reference/filters?scope=<scope>
or the published OpenAPI spec
(developers.beta.dealroom.co/openapi.yaml
) advertise endpoints/filters this skill
does not mention, or omit ones it does.
When you hit one:
- Do not paper over it with hardcoded values, guessed keys, or silent workarounds.
- Confirm against the source of truth:
GET /reference/filters?scope=<scope>
for filters,
the published OpenAPI spec (developers.beta.dealroom.co/openapi.yaml
) for paths and
shapes. A one-off / or empty result is usually data, not drift; a structural
mismatch is reproducible.
- If the live state genuinely diverges from this skill, stop and tell the user
plainly, for example: "The Dealroom API now behaves differently from what the
dealroom-early-access-api
skill describes (). I verified this against
and the published OpenAPI spec. The skill looks out of date." Then proceed using the live
behavior, and recommend the user update the skill (or open a PR to
) so it stays accurate.