Pulse Webhooks — Endpoints, Delivery Log & Scheduler

Jessica

Jessica

Last updated on Aug 18, 2026

Pulse Webhooks push platform events to HTTPS endpoints owned by the platform team and by firms: CRM syncs, marketing automation, monitoring. This page is the platform-side control center — every endpoint across every tenant, the delivery log with resend, and the scheduler switch that turns the delivery engine on.

💡 Tip — Who can use this Platform administrators, at Webhooks in the admin navigation (after Connect Keys). Firms manage their own endpoints from their portal's Settings; this page oversees everything.

Quick reference

Step Action
1 Open the Webhooks page
2 Enable the delivery scheduler
3 Create and manage endpoints
4 Read the delivery log and resend

Step 1: Open the Webhooks page

In the admin portal sidebar, click Webhooks (directly after Connect Keys). The page has three zones: the Delivery Scheduler card, the Webhook Endpoints manager (platform endpoints), and the Recent Deliveries log with filters.

Admin Webhooks page overview

Step 2: Enable the delivery scheduler

Deliveries are processed by a background job that ticks every 30 seconds. On a fresh environment it starts Paused — click Enable on the Delivery Scheduler card and confirm it shows Active with the 30-seconds schedule. While paused, nothing is lost: events accumulate in the outbox and flow when re-enabled. A platform-wide kill switch (WEBHOOKS_ENABLED environment variable) can pause the entire webhook system without touching any endpoints — the page and nav also disappear while it is off.

Delivery scheduler card showing Active

⚠️ Warning — The scheduler bakes in the internal API key The cron job calls the processor with the environment's internal key. Set the production INTERNAL_API_KEY before enabling the scheduler — if you rotate the key later, disable and re-enable the schedule so it picks up the new value.

Step 3: Create and manage endpoints

Create Endpoint opens the shared dialog: HTTPS URL, description, and event subscriptions grouped by domain (Lifecycle, Billing, Engagement, Messaging, Platform — messaging events are marked high-volume). Platform endpoints receive every subscribed event across all organizations; firm endpoints created in firm settings receive only their own clients' events. Each endpoint row shows its subscription count, status, consecutive failures, last delivery outcome, and actions: Edit, Reveal/Rotate Secret, Send Test, and enable/disable.

Endpoints table with delivery log

📝 Note — Auto-disable protects everyone An endpoint that fails 25 consecutive deliveries is disabled automatically and its owners are notified (platform admins, or the firm's admins for firm endpoints). Re-enabling resets its failure budget. Dead-lettered deliveries (rows that exhausted the retry ladder) stay in the log for inspection and one-click Resend.

Step 4: Read the delivery log and resend

Recent Deliveries lists every delivery attempt with filters by endpoint and status (pending / succeeded / failed / dead). Use Resend on a failed or dead row to requeue it with a fresh retry ladder — handy after a client fixes their receiving service. Deliveries are at-least-once; consumers deduplicate on the X-Pulse-Delivery header.

❗ Important — Consumers must verify signatures Every delivery is signed: the X-Pulse-Signature header is t=<unix-seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, "<t>.<rawBody>"), alongside X-Pulse-Event, X-Pulse-Delivery, and X-Pulse-Timestamp. Consumers should verify against the endpoint's secret and reject timestamps older than 5 minutes:

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyPulseSignature(secret, header, rawBody, toleranceSec = 300) {
  const m = /^t=(\d+),v1=([0-9a-f]+)$/.exec(header);
  if (!m) return false;
  const [, t, v1] = m;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSec) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}