Outbound webhooks

Register endpoints in Settings → Developer. Each event is POSTed as JSON with a stable id — retries always carry the same event id, so deduplicate on it.

Event types

  • client.created
  • lead.created
  • lead.stage_changed
  • proposal.created
  • proposal.accepted
  • invoice.issued
  • invoice.paid
  • payment.received
  • project.completed
  • support.ticket_created
  • business.created
  • proposal.sent
  • proposal.rejected
  • invoice.overdue
  • support.ticket_status_changed
  • marketing.campaign_created
  • marketing.campaign_activated
  • marketing.campaign_paused
  • marketing.campaign_completed
  • marketing.lead_captured
  • marketing.attribution_captured
  • marketing.campaign_submitted
  • marketing.campaign_failed
  • marketing.provider_sync_completed

Payload & headers

POST /your/endpoint
Content-Type: application/json
X-DIGISUBS-Event-Id: evt_9f2c41a83b7d
X-DIGISUBS-Event-Type: invoice.paid
X-DIGISUBS-Signature: t=1790000000,v1=5f8a…

{
  "id": "evt_9f2c41a83b7d",
  "type": "invoice.paid",
  "createdAt": "2026-08-27T12:00:00.000Z",
  "data": { "invoiceId": "inv_abc", "number": "INV-2026-0042" }
}

Signature verification

HMAC-SHA256 of `${t}.${rawBody}` with your endpoint secret. Reject timestamps older than 5 minutes (replay protection). During secret rotation the header carries two v1= values — accept if either matches.

// Node.js
import { createHmac, timingSafeEqual } from "crypto";

function verify(header, rawBody, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=", 2)),
  ); // NOTE: collect ALL v1 values in production code
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (age > toleranceSec) return false; // replay
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

Retries & dead-lettering

  • Any non-2xx response (or timeout after 10s) schedules a retry.
  • Backoff: 30s, 60s, 120s, 240s — 5 attempts total.
  • After the final failure the delivery is dead-lettered; you can inspect the full delivery log (attempts, status codes, durations) in Settings → Developer.
  • Respond 2xx quickly and process asynchronously — slow handlers hit the timeout.