Quickstart
An admin creates a key in the app under Settings, Developers. The key is shown once, so store it somewhere safe. Then call the API with the key as a bearer token.
curl "https://app.venturefract.com/api/v1/deals?limit=2" \ -H "Authorization: Bearer vfk_your_key_here"
{
"data": [
{ "id": "6f1c…", "name": "Northlight AI", "stage": "Diligence", "round": "Series A", "ask": 6.5, "fund_id": "a2b9…", "created_at": "2026-09-14T09:12:03Z" },
{ "id": "9d03…", "name": "Harbor Robotics", "stage": "Screening", "round": "Seed", "ask": 3.0, "fund_id": "a2b9…", "created_at": "2026-09-20T16:41:55Z" }
],
"next_cursor": "OWQwMy4uLg"
}
The example is illustrative. Your responses contain your own records.
Authentication and limits
- Send
Authorization: Bearer vfk_…on every request. Keys belong to one firm and can only read that firm's data. - The API is read-only: only
GETrequests are accepted. - Each key is limited to 120 requests per minute. Over the limit you receive
429with aRetry-Afterheader. - You can create up to 10 active keys and revoke any of them instantly. A revoked key stops working at once.
- If the firm's plan no longer includes API access, requests return
403until it does.
Pagination and incremental sync
List endpoints return data and next_cursor. Pass the cursor back as ?cursor= to get the next page. When next_cursor is null you have everything.
limit: 1 to 200, default 50.updated_since: an ISO 8601 timestamp. Returns only records changed at or after it, which is the efficient way to keep a warehouse in sync.stage(deals only): filter by pipeline stage.- Records are returned in a stable order, so paging never skips or repeats a record that existed when you started.
Resources
| Endpoint | Returns |
|---|---|
GET /api/v1/me | The firm the key belongs to: id, name, base_currency. |
GET /api/v1/dealsGET /api/v1/deals/{id} | id, name, stage, round, sector, geo, ask, source, next_step, next_step_due, owner_membership_id, fund_id, company_id, website, one_liner, thesis_note, created_at, updated_at, archived_at |
GET /api/v1/companiesGET /api/v1/companies/{id} | id, name, sector, geo, health, created_at, updated_at, archived_at |
GET /api/v1/positionsGET /api/v1/positions/{id} | id, company_id, fund_id, round, invested_amount, invested_at, exited_at, pro_rata_right, information_right, board_right, created_at, updated_at |
GET /api/v1/fundsGET /api/v1/funds/{id} | id, name, vintage_year, status, strategy, currency, created_at, updated_at, archived_at |
Monetary amounts are in the units and currency your firm uses in the app.
Errors
Errors use one shape, with a request id you can quote if you need help.
{ "error": { "code": "unauthorized", "message": "This API key is not valid or has been revoked.", "request_id": "8c496e0d-6800-41a1-8db7-7b7a7d39f996" } }
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | A parameter is invalid, such as a limit over 200 or a malformed cursor. |
| 401 | unauthorized | The key is missing, invalid or revoked. |
| 403 | forbidden | The firm's plan does not include API access. |
| 404 | not_found | Unknown resource, or no such record in your firm. |
| 405 | method_not_allowed | The API is read-only. |
| 429 | rate_limited | Over 120 requests per minute for this key. |
Webhooks
Add an HTTPS endpoint under Settings, Developers and choose the events you want. We send a signed POST with a JSON body.
| Event | Sent when |
|---|---|
deal.created | A deal is added to the pipeline. |
deal.stage_changed | A deal moves to a different stage. Includes previous_stage. |
position.created | A portfolio position is added. |
update.submitted | A founder submits an update. |
{
"id": "evt_5c364e15362b419efa67bc53",
"type": "deal.stage_changed",
"created_at": "2026-10-06T12:59:46Z",
"data": { "id": "a8fe…", "name": "Northlight AI", "stage": "Screening", "previous_stage": "Sourcing", "fund_id": "8d4a…" }
}
- Respond with any 2xx within 8 seconds. Redirects are not followed.
- Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, then marked failed. That is 7 attempts over about 34 hours.
- The event
idis the same on every retry, so use it to ignore duplicates. - An endpoint that fails 20 deliveries in a row is paused. Resume it in the app.
- URLs must be HTTPS on the standard port with a public hostname. Addresses and hostnames that point to private networks are refused.
- Each request carries
VentureFract-Event,VentureFract-DeliveryandVentureFract-Signatureheaders.
Verifying signatures
Always verify a webhook before trusting it. The signature header looks like t=1791290970,v1=9f2c…. Compute an HMAC-SHA256, using your endpoint's signing secret, of the timestamp, a dot and the raw request body, and compare it to v1 in constant time. Reject anything with a timestamp more than five minutes old.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, header, rawBody) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Python
import hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Your signing secret is shown once when you create the endpoint. If it is ever exposed, rotate it from the endpoint's menu in the app.