/

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.

POST/v1/webhooksTeam#

Subscribe a public https URL to change events. The secret is shown once. A new webhook receives events from now on.

Results none

ParameterDescription
urlstring · body · required
https, on a public host and the default port.
event_typesstring[] · body
Every deliverable type by default. See Event types.
firmsstring[] · body
Up to 500 slugs or domains. Leave out for every firm; company events go only to webhooks that follow every firm.
curl -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"]}'
Response201
{
  "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."
}
GET/v1/webhooks#

Your webhooks, with their failure counts. active is 0 once ten deliveries in a row failed.

Results none

curl "https://api.vcinfo.io/v1/webhooks" \
  -H "X-API-Key: $VCINFO_KEY"
Response200
{
  "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"
    }
  ]
}
POST/v1/webhooks/{id}/test#

Send a clearly fake event with a real signature, so you can build and check your receiver.

Results none

ParameterDescription
idstring · path · required
The webhook's id.
curl -X POST "https://api.vcinfo.io/v1/webhooks/wh_3f9a1c7e2b4d6a80/test" \
  -H "X-API-Key: $VCINFO_KEY"
Response200
{
  "delivered": true,
  "status": 200
}
DELETE/v1/webhooks/{id}#

Delete a webhook. Returns 204 with no body.

Results none

ParameterDescription
idstring · path · required
The webhook's id.
curl -X DELETE "https://api.vcinfo.io/v1/webhooks/wh_3f9a1c7e2b4d6a80" \
  -H "X-API-Key: $VCINFO_KEY"

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.

Response200
{
  "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));
}

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.