# How To Verify Webhook Signatures With BetterShield Ultra?

Check a custom Ultra webhook's timestamp and raw-body signature, store its signing secret, and replace the secret when needed.

Use **[BetterShield Ultra](/ultra/)** to verify a custom webhook before your receiver processes it. Checking the signature and timestamp helps you refuse altered, stale or incorrectly signed messages.

_**Note:** Use BetterShield Ultra 1.0.9 or newer with the free BetterShield plugin installed and active. Your receiver must be able to read the raw request body._

This guide covers **'Other service (webhook)'** using standard JSON or NDJSON. Native chat destinations, syslog and collector presets such as Splunk, Datadog and Elastic use their own delivery arrangements.

<span id="step-1-save-the-destinations-signing-secret"></span>

## Step 1: Save Your Signing Secret

Open **'BetterShield › Ultra › Alert channels'** and [add a custom webhook destination](/docs/alert-destinations/). When you save a new supported destination, copy its **'Signing secret'** into the receiver's private configuration.

The secret is shown once. Store it before leaving the screen, and keep it outside source control and browser code.

An existing destination may say its deliveries are not signed yet. Select **'Make a Signing Secret'**, then store the newly shown value at the receiver.

Each destination has its own secret. A connector's access key is a separate credential.

<figure class="doc-shot">
  <img src="/docs/screenshots/webhook-signing-1-0-9.png" alt="Custom webhook destination confirms that each delivery is signed and offers Make a New Signing Secret." width="980" height="615" loading="lazy" decoding="async">
  <figcaption>Ultra 1.0.9 after a signing secret has been created. The example destination is off, and its secret is not shown.</figcaption>
</figure>

<span id="step-2-verify-the-raw-delivery"></span>

## Step 2: Verify and Test Deliveries

In your receiver's request handler, verify the headers and exact body bytes before processing the message. Use the secret saved from **'BetterShield › Ultra › Alert channels'**.

### Step 2.1. Check the Headers

Read these headers:

- `X-BetterShield-Timestamp`: the signing time in Unix seconds.
- `X-BetterShield-Signature`: `v1=` followed by the hexadecimal HMAC-SHA256 signature.

Reject missing or malformed headers. Refuse a timestamp more than five minutes away from your receiver's current time, including one too far in the future. Keep both servers' clocks accurate.

### Step 2.2. Verify the Raw Body

Keep the exact request bytes. Do not parse and reserialize JSON, normalize whitespace or remove NDJSON line endings before verification.

Compute HMAC-SHA256 using the full signing-secret string as the key and this message:

```text
timestamp + "." + raw request body
```

Compare equal-length signature bytes in constant time. For a Node.js receiver, use this function with the raw body as a Buffer and the two header values as strings:

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

export function verifyDelivery(rawBody, timestamp, signature, secret, now = Date.now()) {
  if (!/^\d+$/.test(timestamp) || !/^v1=[a-f0-9]{64}$/.test(signature)) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || Math.abs(now / 1000 - seconds) > 300) return false;
  const expected = createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest();
  const received = Buffer.from(signature.slice(3), 'hex');
  return received.length === expected.length && timingSafeEqual(received, expected);
}
```

Process the message only after this returns true. The same verification applies to supported alert, full-activity and checkpoint deliveries.

<span id="step-3-test-acceptance-and-refusal"></span>

### Step 2.3. Test Acceptance and Refusal

With synthetic input at your receiver, confirm these results:

- An unchanged body with a matching signature and current timestamp passes.
- Changing one byte or its whitespace without signing again fails.
- A correctly signed message older than five minutes or too far in the future fails.
- Missing headers and a signature made with another destination's secret fail.

Return to **'BetterShield › Ultra › Alert channels'**, save the destination, then select **'Send a Test Message'**. Inspect the actual receiver's response as well as WordPress's delivery result.

If you must process each event only once, keep a receiver-side record of deliveries already processed. A valid signature alone does not prevent repeat processing within the freshness window.

<figure class="doc-shot">
  <img src="/docs/screenshots/webhook-verifier-tests.png" alt="Example verifier report passes nine synthetic-input checks, including refusal of stale, altered and incorrectly signed bodies." width="944" height="225" loading="lazy" decoding="async">
  <figcaption>Output from the guide’s verifier using synthetic input. This tests the example code, not an external webhook delivery.</figcaption>
</figure>

<span id="step-4-replace-a-lost-or-exposed-secret"></span>

## Step 3: Replace a Signing Secret

In **'BetterShield › Ultra › Alert channels'**, select **'Make a New Signing Secret'** for the saved destination. Read the consequence, then confirm **'Yes, Make a New One'**.

The old secret stops working immediately. Give the new secret to the receiver and test again. Deliveries can be refused during the changeover.

A [Hub workspace webhook](/docs/hub-alert-destinations/) has its own destination and secret. Do not reuse an Ultra site's secret for that separate connection.

<figure class="doc-shot">
  <img src="/docs/screenshots/webhook-secret-rotation.png" alt="Signing-secret rotation confirmation warns that the old secret stops working immediately." width="980" height="621" loading="lazy" decoding="async">
  <figcaption>The released rotation confirmation. Update the receiver when you replace the secret.</figcaption>
</figure>

## Final Outcome

You have a destination secret and a verifier that accepts authentic current input while refusing altered, stale or incorrectly signed input. Confirm a real test delivery at your receiver before relying on it.

<figure class="doc-shot">
  <img src="/docs/screenshots/webhook-verifier-tests.png" alt="Example verifier report passes nine synthetic-input checks, including refusal of stale, altered and incorrectly signed bodies." width="944" height="225" loading="lazy" decoding="async">
  <figcaption>Output from the guide’s verifier using synthetic input. This tests the example code, not an external webhook delivery.</figcaption>
</figure>

## Related

- [Ultra destinations](/docs/alert-destinations/)
- [Log checkpoints](/docs/activity-log-checkpoints/)
- [Alert channels](/docs/alert-channels/)

This is how, with the help of BetterShield Ultra's webhook signatures, you can verify deliveries before processing them.

Getting Stuck? Feel free to contact our [Dedicated Support Team](/support/).
