Validate Generic webhook signatures

Verify signed Generic webhook requests with the one-time secret, reject replays safely, and handle retries without processing a Delivery twice.

A Generic Notifier sends JSON to a public HTTPS receiver you control. PointWarden signs every Test Delivery and production Delivery Attempt so your receiver can reject requests that were changed or did not come from someone holding the Notifier's signing secret.

Understand the signed request

Generic requests use Content-Type: application/json. Every JSON envelope has version: 1 and Workspace data. Production envelopes contain stable Delivery and Notification Event IDs, plus Monitor and Incident data. A Test Delivery instead carries its own Test Delivery ID in delivery.id, uses a diagnostic event object with type test, and omits Monitor and Incident data. These test values do not create a Delivery or Notification Event.

This fictional incident-opening example shows the complete production envelope. PointWarden sends it as compact JSON; the indentation below is only for reading. Verify the signature against the exact raw body bytes received over HTTP, not a pretty-printed or reconstructed copy.

Illustrative incident-opening request body
{
  "delivery": {
    "id": "delivery_example"
  },
  "event": {
    "confirmedAt": "2026-08-20T03:02:00.000Z",
    "id": "event_example",
    "observedAt": "2026-08-20T03:00:00.000Z",
    "occurredAt": "2026-08-20T03:00:00.000Z",
    "type": "incident-opened"
  },
  "incident": {
    "confirmedAt": "2026-08-20T03:02:00.000Z",
    "detectedAt": "2026-08-20T03:00:00.000Z",
    "id": "incident_example",
    "recoveredAt": null,
    "recoveryObservedAt": null
  },
  "monitor": {
    "id": "monitor_example",
    "name": "Example Monitor"
  },
  "version": 1,
  "workspace": {
    "id": "workspace_example",
    "name": "Example Workspace"
  }
}
FieldProduction meaning
version1. Reject or isolate unsupported versions before processing.
delivery.idStable Delivery ID shared by every Delivery Attempt for this Delivery.
event.idStable Notification Event ID.
event.typeincident-opened or recovery-confirmed.
event.occurredAt, event.observedAt, event.confirmedAtISO 8601 event timestamps.
workspace.id, workspace.nameWorkspace identity and display name.
monitor.id, monitor.nameMonitor identity and display name.
incident.id and Incident timestampsIncident identity with detectedAt, confirmedAt, recoveryObservedAt, and recoveredAt values. Recovery values are null until known.
HeaderMeaning
X-PointWarden-TimestampUnix time in seconds. Use the exact header value in the signed message.
X-PointWarden-Signaturev1= followed by a 64-character lowercase hexadecimal HMAC-SHA-256 digest.
X-PointWarden-EventProduction requests contain the Notification Event ID. Test Deliveries contain test, a diagnostic marker rather than a Notification Event ID.
X-PointWarden-DeliveryProduction requests keep the same Delivery ID across retry attempts. Test Deliveries contain their Test Delivery ID, not a Delivery ID.

Verify before parsing the JSON

  1. Preserve the raw body

    Read the HTTP body as bytes before a JSON parser changes spacing, escaping, or property order. Re-serializing parsed JSON produces different bytes and makes a valid signature fail.

  2. Build the signed message

    Join the timestamp header, one period, and the exact raw body with no added whitespace or newline:

    timestamp + "." + rawBody

  3. Calculate and compare

    Calculate HMAC-SHA-256 with the full signing secret, encode the digest as lowercase hexadecimal, add v1=, and compare it with X-PointWarden-Signature using a constant-time comparison.

Verification examples

Choose your receiver's language. Each example uses its standard cryptography API, validates a five-minute timestamp window, and compares the signature without leaking timing information. Pass the untouched body bytes, the two signature headers, and the secret from your secret manager. Parse the JSON only after the function returns true.

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

const MAX_AGE_SECONDS = 5 * 60;
const SIGNATURE_PATTERN = /^v1=([0-9a-f]{64})$/;

export function verifyPointWarden(
  rawBody,
  timestamp,
  signature,
  secret,
) {
  const match = SIGNATURE_PATTERN.exec(signature ?? "");
  const timestampSeconds = /^\d+$/.test(timestamp ?? "")
    ? Number(timestamp)
    : Number.NaN;
  const now = Math.floor(Date.now() / 1000);

  if (
    !timestamp ||
    !match ||
    !Number.isSafeInteger(timestampSeconds) ||
    timestampSeconds < now - MAX_AGE_SECONDS ||
    timestampSeconds > now + MAX_AGE_SECONDS
  ) {
    return false;
  }

  const expected = createHmac("sha256", secret)
    .update(timestamp, "utf8")
    .update(".", "utf8")
    .update(rawBody)
    .digest();
  const received = Buffer.from(match[1], "hex");

  if (received.length !== expected.length) {
    return false;
  }
  return timingSafeEqual(received, expected);
}

Reject replays without breaking retries

A Delivery can have more than one Delivery Attempt. Each Delivery Attempt is one HTTP request. A retry creates a new Delivery Attempt for the same Delivery.

PointWarden uses at-least-once Delivery. A production retry keeps the same Delivery ID but carries a fresh timestamp and signature. Return a 2xx response after successful processing. PointWarden may retry 408, 425, 429, 5xx, timeout, and network failures; other non-2xx responses and redirects end that Delivery permanently.

Rotate a secret safely

Rotate the signing secret if it is exposed or unavailable. Rotation immediately invalidates the previous secret, pauses the Notifier, returns it to Unverified, and cancels unfinished Deliveries for the old configuration. Store the replacement shown once, update the receiver, send a successful Test Delivery, and then enable the Notifier again.

Return to Configure a Notifier for target and subscription setup. Then verify and enable the Notifier. To understand stable Delivery IDs and attempts, Inspect Delivery evidence.