# parsers.vc API — instructions for autonomous agents Venture capital data: startups, investors, funding rounds, valuations, people, news. Roughly 1.4M companies, 90k investors, 400k rounds. Updated continuously by crawlers. This file is the machine-readable contract. It is stable, versioned with the API, and safe to cache for 24 hours. The prose version for humans is . The rate card below is a copy, kept by hand. `GET /v2/pricing` is generated from the meter itself and costs nothing to call, so read that if the two ever disagree. ``` DATA API https://vcapi.parsers.vc/v2 AUTH API https://parsers.vc/api/v1 OPENAPI https://parsers.vc/api/v1/openapi.json THIS FILE https://parsers.vc/api/v1/agents.md ``` --- ## 1. Get a key You do this yourself. There is no sign-up form to send anyone to, no dashboard to walk them through, no support ticket. You make two HTTP calls; the only thing the person has to do is prove the mailbox is theirs, by reading you a six-digit code or by clicking a link. That confirmation is not a formality to route around. The key is issued in their name, spends their allowance, and they must be able to revoke it — which means they have to know it exists. **Four rules hold for the whole of this section.** Registrations that fail almost always fail on one of these rather than on the API. 1. **You make the calls, not the person.** Every request here is a plain `GET` with everything in the query string — no body, no custom method, no header you cannot set. If you can fetch a web page, you can register. Handing someone a URL to open is what you do after your own call has actually failed — or after your tooling refused to open a URL you composed, which is a different thing and has its own branch in 1.1 — not instead of trying. 2. **`request_id` comes back from the first call, and keeping it is your job.** It is the handle on the registration: it is not in the e-mail, the person never sees it unless you show it to them, and nothing will tell you it afterwards. Copy it verbatim into the very next message you write. There is a way to finish without it (1.2) and it is strictly worse — treat it as recovery from a mistake, not as permission to skip this. 3. **Never ask for the code before you have actually started a registration.** Otherwise you are collecting a secret against a clock — ten minutes — that is not even running yet, and the person reads you a code from an e-mail nobody sent. 4. **Say only what you actually did.** If you have not called `/auth/register`, no e-mail was sent, and telling someone to go and read one is a false report. If a call failed, say it failed and say what you are trying next. Never invent a `request_id` to keep going: it returns `410` and spends one of the three registrations that address is allowed in an hour. ### 1.0 First: do they already have one? Ask. A key looks like `pk_live_...` and they may already have one at . If so, skip to 1.4. ### 1.1 Start the registration Ask for their e-mail address, then make the call yourself — as a POST if your tooling allows one: ```http POST https://parsers.vc/api/v1/auth/register Content-Type: application/json {"email": "them@example.com"} ``` **If all you can do is open a URL**, this call is also a GET, and so is every other call in this file. Nothing here needs a request body, a custom method or a header you cannot set — so "I can only fetch URLs" is not a reason to delegate this to the person: ``` https://parsers.vc/api/v1/auth/register?email=them@example.com ``` ```json { "status": "sent", "request_id": "kQ7f...", "expires_in": 600, "next": "Ask the person for the 6-digit code ..." } ``` **Write `request_id` down before you write anything else** — put it in your reply as `request_id: kQ7f...`, or into whatever scratch memory you keep. You need it for the next call, it is not in the e-mail, and no endpoint will give it back to you. Losing it does not sink the registration — 1.2 can finish on the address instead — but it costs you the status endpoint and any registration that is not the newest one for that address. The response is `202` and looks the same whether or not the address was accepted — a refused address is not distinguishable from an accepted one, deliberately, so this endpoint cannot be used to find out who is registered. `request_id` is present only when a message really went out; if it is missing, the address was rejected (disposable domain, no MX record) or a rate limit was hit. Say so plainly and ask for a different address rather than retrying the same one. Now — and only now, with `request_id` in hand — tell the person: *"I have sent a code to them@example.com. Read it back to me, or open the link in the same message."* One e-mail carries both. **If your own request fails.** Try it once more as the GET URL above; a runtime that cannot POST often reports that as a connection failure rather than as a refusal. If that fails too, then hand the URL over — and ask for the right thing back: > *"Open this link and paste me back the whole line of text the page shows. It is JSON, and it > contains a `request_id` that I need in addition to the code that will arrive in your inbox."* The page prints exactly the JSON above and nothing else, so this costs them one copy-paste. Ask for the JSON, not just for the code — the address will get you out of it (1.2) but it is the second-best handle, and this way you are holding the first. **If your tools refuse the URL rather than fail on it.** Some runtimes — ChatGPT's browsing among them — will only open a URL that came out of a search result or out of the person's own message, and refuse one you assembled yourself, which is what a registration URL with their address in it is. The refusal is about where the URL came from, not about this host: retrying, rewording or switching to the POST form will not move it, and neither will anything on our side. Two things follow, and the second one matters more. Say which wall you hit. *"My browsing tool will not open a URL I built myself"* is true and actionable; *"parsers.vc is unreachable"* is neither, and it sends the person hunting a fault that is not there. Then stop trying to be the one who makes the calls, because you cannot finish this way — `verify` would be another URL you composed, so even a `request_id` and a code read out to you lead nowhere. Take the branch that needs no call from you at all: > *"I cannot make this request myself — my browsing tool only opens links it was given. Open this > one: https://parsers.vc/api/v1/auth/register?email=YOUR@ADDRESS (put your own address in it). > You will get an e-mail. Ignore the six-digit code, click **Open my account** in that same > message, and paste me the key the page shows."* They land in the cabinet already signed in, with a key minted and displayed ready to copy — no form to fill in. What comes back is a `pk_live_...` string; go to 1.4 and use it. The code branch is the one you cannot walk; the link branch was always theirs to walk, and it ends in the same place. ### 1.2 Finish it — two ways, both end with you holding a key **They read you the code.** One call and you are done: ```http POST https://parsers.vc/api/v1/auth/verify Content-Type: application/json {"request_id": "", "code": "123456"} ``` or, again, as a URL you can simply open: ``` https://parsers.vc/api/v1/auth/verify?request_id=&code=123456 ``` ```json { "api_key": "pk_live_...", "expires_at": "2026-10-05T09:41:00Z", "email": "them@example.com", "credits": {"limit": 500, "remaining": 500, "reset": "2026-10-01T00:00:00Z"}, "base_url": "https://vcapi.parsers.vc/v2/", "manage_url": "https://parsers.vc/account/" } ``` The code is good for **10 minutes and five attempts**. A wrong one comes back `401` with `attempts_left`; read it back to the person rather than guessing again. **If you lost the `request_id`, send the address instead.** Same call, same code, one field swapped — it resolves to the newest registration still open for that address: ``` https://parsers.vc/api/v1/auth/verify?email=them@example.com&code=123456 ``` This is the way out of the mess that comes from letting the person open the register URL themselves, and it is the only thing that recovers a code you cannot otherwise spend. It is still the worse handle: it only ever finds the most recent registration, so if two were started for one address the older one is unreachable, and `/auth/status` has no address form at all. Use it to recover, not as your normal path. Rule 2 stands — record the `request_id` and you never need this. **They would rather not share the code.** A reasonable choice — whoever holds it can mint a key in their name — and it costs you nothing. They open the link from the same e-mail, which signs them straight in, create a key at and paste it to you. Meanwhile: ```http GET https://parsers.vc/api/v1/auth/status?request_id= ``` `pending` → still waiting. `link_used` → they are in the cabinet; ask them for the key. `completed` → the code path already finished. `expired` → start again at 1.1. Poll this at most every 15 seconds, and stop after ten minutes. ### 1.3 What you now have **500 requests a month**, refilled on the first of each month, on an account that belongs to the person. The key itself **expires in 30 days** — it was minted inside a conversation, so it is short-lived on purpose. When it does expire, run 1.1 again against the same address: it resolves to the same account and the same allowance, not a second one. Anything more — a key that does not expire, a bigger allowance, seeing what was spent — is on , which they can now reach with the same e-mail. ### 1.4 Using the key ```http GET https://vcapi.parsers.vc/v2/startups?$top=5 X-Api-Key: pk_live_... ``` `Authorization: Bearer ` also works. If you cannot set a header at all, put the key in the query string as `token` — every endpoint on both hosts accepts it, and it is then a plain URL: ``` https://vcapi.parsers.vc/v2/startups?$top=5&token=pk_live_... ``` That URL carries a live secret. It belongs in the request and nowhere else: not in a summary you show, not in a message you send on, not in anything you write to disk. **One credit buys one record, not one request** — a page of 100 startups costs 100, a funding round costs 0.5, a news item 0.25, and a search costs 1 flat however many results come back. Errors are free. `GET https://vcapi.parsers.vc/v2/pricing` returns the whole rate card as JSON, also free; the human version is at . Every response carries the balance; you never need to spend a call to check it: ``` X-RateLimit-Limit: 500 X-RateLimit-Remaining: 487 X-RateLimit-Reset: 1791244800 (unix seconds) ``` Key handling: it is a secret and it belongs to the person, not to you. Do not print it into transcripts, logs, commits or issue trackers you do not control. --- ## 2. Two ways in Nearly every question about this dataset is one of two shapes, and each has its own entry point. Use them in this order — they are cheaper than the raw collections and they tell you what exists. - **"What is out there like X?"** → the categories API. Browse the taxonomy, count the facets, then pull the entities. Section 4. - **"What changed?"** → the signals API. A cursor-paged change feed with venture-specific event types. Section 5. The raw collections (section 6) are for when you already know exactly what you want. --- ## 3. Query conventions Collections take OData-style parameters: | parameter | meaning | |---|---| | `$top` | page size. Max 100 for API keys. | | `$skip` | offset. | | `$filter` | `Field eq 'value'`, plus `ne gt ge lt le like in notin`, combined with `and` / `or`. | | `$select` | which fields to return. **Use it.** Fewer fields is a faster query. | | `$expand` | pull related objects in the same request instead of a second call. | | `$orderby` | `Field`, `Field desc`. | | `$count` | return the count instead of the rows. | `in` takes a quoted list: `$filter=Tag in ('fintech','insurtech')`. Dates are ISO 8601 and UTC throughout. Money is USD, normalised, as an integer number of dollars. --- ## 4. Categories — "what is out there" Four endpoints, each usable on its own. Stop as soon as you have what you need. ### 4.1 The axes ```http GET /v2/categories # every dimension GET /v2/categories?entity=funds # only those that apply to investors ``` Returns, for each dimension: `id`, `title`, `description`, `kind`, `entities` it applies to, `filterField`, and — for closed dimensions — the complete `values` list. **Read this before guessing at a dimension or value name.** It costs one credit flat and saves you hundreds. Dimensions available today: | id | kind | startups | funds | rounds | |---|---|---|---|---| | `industry` | open | ✓ | ✓ | | | `country` | open | ✓ | ✓ | | | `region` | open | ✓ | ✓ | | | `city` | open | ✓ | ✓ | | | `stage` | closed | ✓ | | | | `next_stage` | closed | ✓ | | | | `investment_stage` | closed | | ✓ | | | `investor_type` | closed | | ✓ | | | `team_size` | closed | ✓ | ✓ | | | `operating_status` | closed | ✓ | ✓ | | | `exit_status` | closed | ✓ | | | | `founded_year` | bucketed | ✓ | ✓ | | | `fund_group` | open | | ✓ | | | `round_series` | open | | | ✓ | | `round_year` | bucketed | | | ✓ | `open` = free text out of the data, list it from the API. `closed` = fixed list, given to you in full by `/v2/categories`. `bucketed` = a number. Closed dimensions accept the label, the slug or the member name interchangeably: `Series A` = `series-a` = `SeriesA`. ### 4.2 The values on one axis ```http GET /v2/categories/industry?entity=startups&top=50 GET /v2/categories/city?entity=startups&country=Germany&q=ber ``` | parameter | default | meaning | |---|---|---| | `entity` | `startups` | `startups` \| `funds` \| `rounds` | | `q` | — | prefix search on the value; `*fin` searches anywhere | | `top` | 100 | max 500 | | `skip` | 0 | | | `counts` | `true` | `false` skips the count and is cheaper | | *any dimension id* | — | narrows the set first | That last row is the important one. `?country=Germany` on the `industry` endpoint returns *industries among German companies*, with counts for that subset — not industries and, separately, Germany. Any number of dimensions can be combined. Values come back most common first, with `value` (what to send back) and `label` (what to show). ### 4.3 Several axes at once ```http GET /v2/categories/facets?entity=startups&dimensions=industry,stage,country&country=Germany ``` One credit, one round trip, every dimension counted. This is the shape to use before you commit to a query: it tells you how large each slice is, so you do not spend a call discovering that there are four companies in it. Omit `dimensions` to get every closed dimension for that entity. Max 10 dimensions per request. A dimension is never constrained by its own selection — asking for `industry` while filtering on `industry=fintech` returns the alternatives, not an echo. ### 4.4 The entities themselves ```http GET /v2/categories/industry/fintech/startups?country=Germany&stage=series-a&$top=25&$select=Name,Website,TotalRaised ``` This is `/v2/startups` with the category conditions applied, so `$select`, `$expand`, `$orderby`, `$filter` and paging behave exactly as they do there. Extra dimensions on the query string stack with the one in the path. Path form: `/v2/categories/{dimension}/{value}/{startups|funds|rounds}`. --- ## 5. Signals — "what changed" A change feed over the whole database. Use it to watch a sector, a country, a portfolio, or a single company, without re-fetching anything. ### 5.1 Two clocks — read this before you write a poller Every signal has both: - **`detectedAt`** — when this database recorded the change. - **`occurredAt`** — when it happened in the market. The feed is ordered and paged by **`detectedAt`**, always. Venture data arrives late and out of order: a round announced three weeks ago gets recorded today. If you poll on the market date you will never see it, because it sorts into the past. `occurredAt` is filterable and reported, and it is the right field to *display*, but it is never the cursor. ### 5.2 The types ```http GET /v2/signals/types ``` | type | subject | object | amount | |---|---|---|---| | `round.recorded` | startup | round | ✓ | | `round.updated` | startup | round | ✓ | | `investor.joined_round` | fund | round | ✓ | | `valuation.recorded` | startup | valuation | ✓ | | `portfolio.company_added` | fund | startup | | | `portfolio.exit_recorded` | fund | startup | | | `startup.added` | startup | | | | `startup.updated` | startup | | | | `fund.added` | fund | | | | `fund.updated` | fund | | | | `team.person_added` | fund | person | | | `news.mention` | startup or fund | news | | Notes that matter for interpretation: - `round.recorded` is the highest-signal event: amount, series and announcement date together. - `investor.joined_round` usually arrives *after* the round it belongs to. Rounds are frequently recorded before the syndicate is known. Do not treat a round with one investor as final. - `portfolio.company_added` is often the first public trace of an unannounced investment: funds update their own site before anyone writes about it. - `startup.updated` says that something moved, not which field. Re-read the company to see what. - `valuation.recorded` is sparse. Valuations are disclosed far less often than round sizes. ### 5.3 The feed ```http GET /v2/signals?types=round.recorded&since=2026-09-01&minAmount=5000000&order=asc&limit=100 X-Api-Key: pk_live_... ``` | parameter | default | meaning | |---|---|---| | `types` | all | comma-separated. **Narrowing this is the single biggest cost saving.** | | `since` / `until` | — | bounds on `detectedAt`. ISO date or timestamp. | | `occurredSince` / `occurredUntil` | — | bounds on `occurredAt`. Never the cursor. | | `subject` | — | comma-separated company or fund ids. | | `minAmount` | — | USD. Excludes types that carry no amount. | | `cursor` | — | from `nextCursor`. | | `order` | `desc` | `asc` to follow the feed, `desc` to browse recent history. | | `limit` | 50 | max 200. | | `expand` | — | `subject`, `object`, or both — adds names, saves a lookup per row. | | *any dimension id* | — | same vocabulary as section 4: `?country=Germany&industry=fintech`. | Response: ```json { "signals": [{ "id": "8f2c...", "type": "round.recorded", "detectedAt": "2026-09-04T11:02:19Z", "occurredAt": "2026-08-28T00:00:00Z", "subject": {"type": "startup", "id": "...", "name": "...", "domain": "..."}, "object": {"type": "round", "id": "..."}, "amount": 12000000, "label": "Series A" }], "nextCursor": "MTc5MTI0...", "hasMore": true, "order": "asc" } ``` ### 5.4 How to follow it correctly 1. First call: `order=asc` with `since=`, no cursor. 2. Store `nextCursor`. Keep it, not a timestamp. 3. Next call: same parameters plus `cursor=`. Repeat while `hasMore` is true. 4. `id` is deterministic — derived from the change, not from when you read it. Re-reading an overlapping window is safe: deduplicate on `id`. Poll no more than hourly. The crawlers do not produce meaningful change faster than that, and each poll costs a quarter credit per signal returned. Cursors do not expire. --- ## 6. The collections For when you already know what you want. ``` GET /v2/startups GET /v2/startups/{id} GET /v2/startups/search?q=... GET /v2/startups/{id}/references GET /v2/startups/fundingrounds GET /v2/startups/{id}/fundingrounds GET /v2/startups/postvaluations GET /v2/startups/tags/search?q=... GET /v2/funds GET /v2/funds/{id} GET /v2/funds/search?q=... GET /v2/funds/{id}/portfolio GET /v2/persons GET /v2/persons/{id} GET /v2/news GET /v2/companies/search?q=... GET /v2/activityfeed ``` Filter fields worth knowing beyond the obvious ones — these are the ones a caller would not guess: | field | on | takes | |---|---|---| | `Tag` | startups, funds | industry tag | | `Country`, `Region`, `City` | startups, funds | address parts | | `LastFundingStage`, `NextFundingStage` | startups | stage | | `MembersCount` | startups, funds | a **headcount**, bucketed for you | | `MembersCountBucket` | startups, funds | a bucket ordinal, as categories returns them | | `Status` | startups | exit status of an investor relationship | | `InvestmentStage`, `InvestmentType` | funds | fund mandate | | `Group.Name` | funds | editorial grouping | | `Keywords` | startups, funds | full-text over the profile | | `Year`, `Investor`, `Startup` | rounds | round year, participant ids | Identity resolution: to turn a name or a domain into an id, use `/v2/companies/search?q=`. Do not guess ids and do not construct them. --- ## 7. Errors On the data API (`vcapi.parsers.vc`): | status | `error.code` | what to do | |---|---|---| | 400 | `unknown_dimension`, `unknown_value`, `unknown_signal_type`, `bad_cursor`, `bad_date` | The body says what is wrong and where to list the valid values. Fix the call. Do not retry it unchanged. | | 401 | — | Key missing, revoked or expired. A key from section 1 lasts 30 days; if it has run out, register again against the same address. | | 403 | — | The key is valid but not entitled to this endpoint. Do not retry, do not try adjacent endpoints. | | 429 | `credits_exhausted` | **Out of credits, not rate-limited.** Retrying never helps. Stop and tell the person. | | 5xx | — | Retry once after 30 seconds, then stop and report. | On registration (`parsers.vc/api/v1`): | status | `error.code` | what to do | |---|---|---| | 400 | `email_required`, `missing_fields` | You omitted a field. `verify` needs the code plus one of `request_id` or `email`. Fix and resend. | | 401 | `wrong_code` | Wrong code. `attempts_left` says how many remain out of five. Ask the person to read it again — do not guess. | | 410 | `invalid_or_expired` | No open registration answers to that `request_id` or address: it expired (10 minutes), was already used, or was never real. Start again at 1.1 — do not retry with a guessed id. | | 429 | `too_many_attempts` | Five wrong codes; this registration is dead. Start again at 1.1. | | 429 | — on register | Too many registrations from this address or network. Wait an hour. Do not switch addresses to get around it. | A `429` with `credits_exhausted` carries what you need to explain it: ```json { "error": {"code": "credits_exhausted", "message": "..."}, "credits": {"limit": 500, "remaining": 12, "required": 100, "plan": "free", "reset": "2026-10-01T00:00:00Z"}, "actions": [ {"type": "purchase_credits", "url": "https://parsers.vc/api_page/pricing/"}, {"type": "reduce_page_size", "parameter": "$top"}, {"type": "wait", "until": "2026-10-01T00:00:00Z"} ] } ``` Read `actions` rather than parsing the English. Compare `required` against `remaining` first: if the shortfall is just the page size, retry with a smaller `$top` instead of giving up. --- ## 8. Spending credits well ``` credits = max(1, ceil( base + records × per_record × (1 + 0.25 × expands) )) ``` | Per record | Endpoints | |---|---| | **1 flat** | `/search` and autocomplete, `/v2/categories` and its facets, tag and location lookups, totals — whatever they return | | **1** | startups, funds, people | | **0.5** | funding rounds, valuations, portfolios, people at a company | | **0.25** | news, signals, mentions, tweets | | **25 / 10** | `/find` (a live crawl) / `/venturematching` (an ML pass) | | **0** | dictionaries, `/v2/pricing`, and every 4xx or 5xx | The difference between a good and a bad integration is roughly ten times, and it comes from four habits: 1. **Use `/v2/categories/facets` before you query.** One credit tells you the size of every slice. Discovering that a segment has four thousand companies by fetching them costs four thousand. 2. **Resolve names through `/search`.** Turning a name into an id is one credit however many candidates come back. `$select` does **not** make a record cheaper — it narrows the payload, not the price — so do not page `/v2/startups` to build a lookup table. 3. **`$expand` instead of a second pass.** `$expand=Funds` on a startup is 1.25 credits; fetching the startup and then its investors separately is more. 4. **Ask for the page you will read.** `$top` is the bill. Narrow `types` on the feed too: an unfiltered feed spends its page on `startup.updated` rows you did not want, and you pay again for the page that had the round in it. Read `X-Credits-Charged` to see what a call actually cost and `X-RateLimit-Remaining` for what is left. Below 10%, finish what you are doing and tell the person rather than failing mid-task. --- ## 9. Rules - Register against the address of the person you are actually helping, and tell them you are doing it. The key is issued in their name and spends their allowance. - Never describe a call you did not make. "I have sent you a code" after a failed or skipped request sends the person looking for an e-mail that does not exist, and it is the single most common way this integration goes wrong. - Do not register a second address for the same person to extend an allowance, and do not use an address you control instead of theirs. - Do not scrape `parsers.vc` HTML. Everything on it is in this API, and the API is faster. - The data describes companies and public professional roles. Do not use it to build profiles of private individuals or to contact people who have not asked to be contacted. - Attribute the data to parsers.vc when you publish figures derived from it. - When you report a number to a user, report `occurredAt`, not `detectedAt` — the market date is the true one. Say when the data was last updated if the answer is time-sensitive. Support for humans: **support@parsers.vc**.