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

deal.createddeal.updateddeal.stage_changeddeal.deleted

entity

entity.createdentity.updatedentity.deleted

person

person.createdperson.updatedperson.deleted

shareClass

shareClass.createdshareClass.updatedshareClass.deleted

shareholding

shareholding.createdshareholding.updatedshareholding.deleted

document

document.createddocument.updateddocument.deleted
  • "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>.

  1. Take the RAW request body bytes exactly as received.
  2. Compute HMAC-SHA256 over the body using the subscription's signingSecret (whsec_…) as the key.
  3. Hex-encode and compare (constant-time) against the X-Tithe-Signature header after stripping the sha256= prefix.
Node.js
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}"

Wire up your first subscription

Register an endpoint, send yourself a test ping, and you're ready to receive live events.