Webhooks · 4 endpoints
Webhooks
On the Team plan, change events can be pushed to your URL instead of polled. Up to 10 webhooks an account, for every firm or for a list of firms.
Subscribe a public https URL to change events. The secret is shown once. A new webhook receives events from now on.
urlstring · body · requiredevent_typesstring[] · bodyfirmsstring[] · bodycurl -X POST "https://api.vcinfo.io/v1/webhooks" \
-H "X-API-Key: $VCINFO_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://hooks.example.com/vcinfo","event_types":["portfolio.added","fund.raised"]}'const res = await fetch("https://api.vcinfo.io/v1/webhooks", {
method: "POST",
headers: { "X-API-Key": process.env.VCINFO_KEY, "Content-Type": "application/json" },
body: JSON.stringify({"url":"https://hooks.example.com/vcinfo","event_types":["portfolio.added","fund.raised"]}),
});
const data = await res.json();import os, requests
res = requests.post(
"https://api.vcinfo.io/v1/webhooks",
headers={"X-API-Key": os.environ["VCINFO_KEY"]},
json={"url":"https://hooks.example.com/vcinfo","event_types":["portfolio.added","fund.raised"]},
)
res.raise_for_status()
data = res.json()201{
"id": "wh_3f9a1c7e2b4d6a80",
"url": "https://hooks.example.com/vcinfo",
"event_types": [
"portfolio.added",
"fund.raised"
],
"firm_ids": null,
"secret": "whsec_...",
"note": "Save the secret: it signs each delivery and is shown only once."
}Your webhooks, with their failure counts. active is 0 once ten deliveries in a row failed.
curl "https://api.vcinfo.io/v1/webhooks" \
-H "X-API-Key: $VCINFO_KEY"const res = await fetch("https://api.vcinfo.io/v1/webhooks", {
headers: { "X-API-Key": process.env.VCINFO_KEY },
});
const data = await res.json();import os, requests
res = requests.get(
"https://api.vcinfo.io/v1/webhooks",
headers={"X-API-Key": os.environ["VCINFO_KEY"]},
)
res.raise_for_status()
data = res.json()200{
"webhooks": [
{
"id": "wh_3f9a1c7e2b4d6a80",
"url": "https://hooks.example.com/vcinfo",
"event_types": [
"portfolio.added",
"fund.raised"
],
"firm_ids": null,
"active": 1,
"failures": 0,
"created_at": "2026-11-05T09:30:00.000Z"
}
]
}Send a clearly fake event with a real signature, so you can build and check your receiver.
idstring · path · requiredcurl -X POST "https://api.vcinfo.io/v1/webhooks/wh_3f9a1c7e2b4d6a80/test" \
-H "X-API-Key: $VCINFO_KEY"const res = await fetch("https://api.vcinfo.io/v1/webhooks/wh_3f9a1c7e2b4d6a80/test", {
method: "POST",
headers: { "X-API-Key": process.env.VCINFO_KEY },
});
const data = await res.json();import os, requests
res = requests.post(
"https://api.vcinfo.io/v1/webhooks/wh_3f9a1c7e2b4d6a80/test",
headers={"X-API-Key": os.environ["VCINFO_KEY"]},
)
res.raise_for_status()
data = res.json()200{
"delivered": true,
"status": 200
}Delete a webhook. Returns 204 with no body.
idstring · path · requiredcurl -X DELETE "https://api.vcinfo.io/v1/webhooks/wh_3f9a1c7e2b4d6a80" \
-H "X-API-Key: $VCINFO_KEY"const res = await fetch("https://api.vcinfo.io/v1/webhooks/wh_3f9a1c7e2b4d6a80", {
method: "DELETE",
headers: { "X-API-Key": process.env.VCINFO_KEY },
});
if (!res.ok) throw new Error(`VCinfo ${res.status}`);import os, requests
res = requests.delete(
"https://api.vcinfo.io/v1/webhooks/wh_3f9a1c7e2b4d6a80",
headers={"X-API-Key": os.environ["VCINFO_KEY"]},
)
res.raise_for_status()Deliveries
Each delivery is a POST with up to 100 events as JSON, so loop over events. Answer with a 2xx within 10 seconds. A batch that fails is sent again on the next run, two minutes later, and ten failures in a row switch the webhook off. Deliveries are at least once: use the event id to skip repeats.
200{
"events": [
{
"id": "evt_01J9ZK4Q2M",
"type": "portfolio.added",
"firm": {
"slug": "sequoia-capital",
"domain": "sequoiacap.com"
},
"company": "traversal.com",
"payload": {
"company": "Traversal",
"domain": "traversal.com"
},
"occurred_at": "2026-11-02T08:15:00.000Z"
}
]
}Verifying the signature
The vcinfo-signature header is t=TIMESTAMP,v1=HEX: the HMAC-SHA256 of TIMESTAMP.BODY with your secret. Check it against the raw body before parsing, and reject timestamps more than five minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Zapier, Make and n8n
Webhooks work with any tool that gives you a URL to receive a POST. Create the receiver in your tool, register its URL with POST /v1/webhooks, then call POST /v1/webhooks/{id}/test so the tool learns the shape of the data.
- Zapier: the Webhooks by Zapier trigger with Catch Hook.
- Make: the Webhooks module with Custom webhook, then an Iterator on
events. - n8n: a Webhook node with the POST method and its production URL, then Split Out on
body.events.
Anyone who learns the URL can post to it, so check the signature in a code step, or treat each event only as a prompt to look at its source.