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](https://brandassets.lucidusfortis.com/help/admin-webhooks/step-1-overview.png)

## 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](https://brandassets.lucidusfortis.com/help/admin-webhooks/step-2-scheduler-card.png)

**⚠️ 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](https://brandassets.lucidusfortis.com/help/admin-webhooks/step-3-endpoints-and-log.png)

**📝 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:

```js
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));
}
```
