Get started
Webhooks
Add an endpoint
In Capable, open Settings → Integrations → Add webhook. Enter an https:// URL and tick the events you want, or * for every event. Open Integrations
Capable then shows the endpoint’s signing secret, which starts with whsec_. It is shown once: store it with your other secrets. Use Test next to the endpoint to send it a webhook.test event.
Events
| Event | Sent when |
|---|---|
security.created | A security is recorded in the app, a SAFE or note is issued from a fundraise, or one is created through the API. |
security.updated | A security is corrected or its terms are modified, funds are received on a SAFE, or drafts are published to the cap table. |
security.exercised | An exercise is recorded, or an exercise request is completed. |
security.cancelled | A security is cancelled. |
stakeholder.created | A stakeholder is added in the app, from HRIS sync, or through the API. |
stakeholder.terminated | A stakeholder's termination is recorded. |
esign.completed | Every signer has signed a document sent for e-signature. |
approval.approved | A signed board or stockholder consent is recorded or attached, including consents read from an imported data room. |
valuation.recorded | A 409A valuation is recorded. |
import.committed | An imported cap table is saved to the company. |
webhook.test | You choose Test next to the endpoint. It goes to that endpoint only, whatever events it subscribes to. |
Payload
Each delivery is a JSON POST. data says what changed and who changed it; fetch the record from the API for its current state.
{
"id": "evt_9w2k4n7q1x5c",
"event": "security.exercised",
"companyId": "org_northwind8k2",
"createdAt": "2026-11-05T18:24:51.112Z",
"data": {
"entityType": "security",
"entityId": "sec_1v9ha3opt004",
"summary": "Exercised 2,000 of OPT-4 → CS-31",
"actor": "Maya Chen",
"meta": null
}
}| Field | Description |
|---|---|
id | Unique id of this delivery, starting with evt_. Use it to ignore a delivery you have already handled. |
event | The event name, also sent in the X-Capable-Event header. |
companyId | The company it happened in. |
createdAt | When Capable sent it, ISO 8601 in UTC. |
data.entityType | The kind of record, such as security, stakeholder or valuation. |
data.entityId | The record's id, such as a security id you can pass to GET /securities/{id}. |
data.summary | One line saying what happened, as shown in the activity log. |
data.actor | Who did it: a person's name, API for an API key, or System. |
data.meta | Extra details for some events, such as the certificate id and status of a security created through the API. Often null. |
Verify the signature
Every delivery has an X-Capable-Signature header: sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with the endpoint’s signing secret. Compute it yourself and compare in constant time before trusting the payload.
import crypto from "node:crypto";
import express from "express";
const app = express();
// Read the raw body: the signature covers the exact bytes Capable sent.
app.post("/hooks/capable", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.CAPABLE_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const given = req.get("X-Capable-Signature") ?? "";
const ok = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
console.log(event.event, event.data.entityId);
res.status(200).end();
});Delivery
- Capable sends each event once, right after the change, and waits up to 8 seconds for a response.
- Any 2xx status counts as delivered. There are no automatic retries, so return 200 quickly and do slow work afterwards.
- Integrations shows each endpoint’s last status, when it last received an event, and how many deliveries in a row have failed.
- A successful delivery resets the failure count. After 20 failed deliveries in a row, the endpoint is turned off and shows as Disabled; delete it and add it again to turn it back on.
- Deliveries are not guaranteed to arrive in order. Use
createdAt, and read the record from the API when order matters.