Developers · Webhooks
Webhooks
Register an HTTPS endpoint and Alethia POSTs a signed JSON payload whenever a subscribed event happens — a deal moves stage, an entity is updated, a shareholding is created. Manage subscriptions with the manage_webhooks capability.
Subscribable
Events
19 events across 6 resources. "ping" is reserved for the test endpoint and cannot be subscribed.
deal
entity
person
shareClass
shareholding
document
- "ping" is reserved for the test endpoint and cannot be subscribed.
- deal.stage_changed fires when a stage change made in-app (the pipeline board or the deal forms) commits — never from the public API: PATCH /api/v1/deals/{dealId} silently strips stage on every write (by design — only the in-app service layer enforces the IC-gate, locked-deal, and forward-move rules a stage change must pass). data carries the full deal record plus previousStage and newStage.
- Subscriptions with recordAccess: "open_only" (the default) never receive restricted records.
Payload
Two envelope shapes
Most events use a simple standard envelope. entity.* events run through a durable outbox with a versioned envelope, so a subscriber can detect gaps and out-of-order delivery.
Standard envelope — deals, people, share classes, shareholdings, documents
Standard envelope (deals, people, share classes, shareholdings, documents, and ping): { event: string, workspaceId: string, data: <the affected record / test payload>, timestamp: ISO-8601 }.
{
"event": "deal.created",
"workspaceId": "ws_9f2c1a",
"data": {
"id": "deal_6f2a19",
"name": "Example Deal",
"stage": "identified",
"...": "the rest of the deal record"
},
"timestamp": "2026-07-15T09:30:00.000Z"
}Durable-outbox envelope — entity.*
entity.* events are produced through a durable outbox with a versioned envelope: { schemaVersion: 1, id: <sha256 event id>, event, workspaceId, entity: { id, data: <snapshot | tombstone> }, timestamp: ISO-8601, revision: number, sequence: number }. entity.deleted carries a tombstone whose revision matches the envelope.
{
"schemaVersion": 1,
"id": "3f9a2b7c1d4e5f60a1b2c3d4e5f60718",
"event": "entity.updated",
"workspaceId": "ws_9f2c1a",
"entity": {
"id": "ent_51ac02",
"data": {
"id": "ent_51ac02",
"legalName": "Example Holdings Ltd",
"...": "a full snapshot, or a tombstone for entity.deleted"
}
},
"timestamp": "2026-07-15T09:30:00.000Z",
"revision": 4,
"sequence": 128
}Verify every delivery
Signature verification
Every delivery carries an X-Tithe-Signature header: HMAC-SHA256, hex, prefixed: sha256=<hex digest>.
- Take the RAW request body bytes exactly as received.
- Compute HMAC-SHA256 over the body using the subscription's signingSecret (whsec_…) as the key.
- Hex-encode and compare (constant-time) against the X-Tithe-Signature header after stripping the sha256= prefix.
const crypto = require("node:crypto");
function isValidSignature(rawBody, signatureHeader, signingSecret) {
const expected = crypto
.createHmac("sha256", signingSecret)
.update(rawBody) // the RAW bytes, before any JSON.parse
.digest("hex");
const received = (signatureHeader || "").replace(/^sha256=/, "");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}Reliability
Delivery & retries
Delivery headers, transport rules and the retry/backoff schedule.
Headers on every delivery: Content-Type: application/json, Content-Length, X-Tithe-Signature, X-Tithe-Event, User-Agent: Tithe-Webhooks/1.
- POST over HTTPS only; URLs resolving to private/loopback/link-local/metadata addresses are blocked at registration AND at delivery (DNS-pinned, rebinding-safe).
- 5-second per-attempt timeout; redirects are not followed; any 2xx status counts as delivered.
- Standard-envelope events: up to 5 attempts with backoff of 1, 5, 30, 120 and 360 minutes (cron-driven retry sweep).
- entity.* outbox events: up to 8 attempts, exponential backoff from 60 s capped at 6 h; payloads capped at 128 KiB.
- Delivery outcomes are denormalised onto the subscription (lastDeliveryAt / lastDeliveryStatus).
Before you trust it
Send a test event
POST /api/v1/webhooks/{subId}/test — Fires a synthetic "ping" event at the subscription's URL, signed with its signingSecret, and reports the outcome. The subscription must be enabled and structurally deliverable. Payload data: { message: "Test delivery from Alethia", subscriptionId }.
curl -X POST "https://app.alethiahq.com/api/v1/webhooks/sub_123/test" \
-H "Authorization: Bearer tk_{workspaceId}_{secret}"Next
Continue reading
Wire up your first subscription
Register an endpoint, send yourself a test ping, and you're ready to receive live events.