Skip to content
GenPageHelp center

Verify webhook signatures

Check that a webhook message came from GenPage, was not changed on the way and is not an old message sent again, using your signing secret.

· For owners and admins

On this page

When a webhook has a Signing Secret, GenPage signs every message it sends. Your endpoint can then check that the message came from GenPage, was not changed on the way and is not an old message sent again. This article is for whoever builds the endpoint. You set the secret when you create or edit the webhook under Developers → Webhooks, see Webhooks.

The headers on each message

HeaderWhat it holds
webhook-idA unique id for the message. A repeat of the same message keeps its id, so you can use it to skip duplicates.
webhook-timestampWhen the message was sent, in seconds since 1 January 1970 (Unix time).
webhook-signaturev1, followed by the signature in base64. Sent only when the webhook has a secret.
X-Webhook-SignatureThe older signature of the body alone, in lowercase hex. Sent only when the webhook has a secret.

The first three follow the Standard Webhooks specification, so its free libraries can do the whole check for you.

Check a message

  1. Take the request body exactly as it arrived, as raw bytes, before anything parses it as JSON.
  2. Join three values with a full stop: the webhook-id, the webhook-timestamp and the raw body.
  3. Compute an HMAC-SHA256 of that text. The key is your Signing Secret exactly as you typed it, as UTF-8 text. Do not decode it.
  4. Encode the result in base64 and put v1, in front of it. Compare it with webhook-signature using a constant-time comparison. The header can hold several signatures separated by spaces; accept the message if any of them matches.
  5. Reject the message if webhook-timestamp is more than five minutes away from your server's clock. This stops someone from replaying a message they captured.
  6. To skip duplicates, remember the webhook-id values you have processed.

Use a Standard Webhooks library

The libraries check the signature and the time in one call. Your secret is plain text rather than a key that starts with whsec_, so pass it in as raw bytes:

// Node: npm install standardwebhooks
import { Webhook } from "standardwebhooks";

const wh = new Webhook(new TextEncoder().encode(process.env.GENPAGE_SECRET), { format: "raw" });
const payload = wh.verify(rawBody, request.headers); // throws if the check fails
# Python: pip install standardwebhooks
from standardwebhooks.webhooks import Webhook

payload = Webhook(GENPAGE_SECRET.encode()).verify(raw_body, request.headers)  # raises if the check fails

Test your code

Run your check against these values. The body is one line with no line break at the end. The timestamp is in the past, so skip the five-minute rule for this test.

Signing secret:      my-signing-secret
webhook-id:          msg_01K5TEST000000000000000000_7
webhook-timestamp:   1790142744
Body:                {"event":"form_submission","timestamp":"2026-09-23T05:52:24+00:00","form_id":"contact","submission_id":4821,"data":{"name":"Zo\u00eb M\u00fcller","website":"https:\/\/acme.io\/path"},"page_url":"https:\/\/acme.genpage.ai\/demo?x=1","ip_address":"203.0.113.7"}

webhook-signature:   v1,KlX4cBqzvZTus3rQZ1SvZDwiOl9RYmcXurt7AiC4GYQ=
X-Webhook-Signature: 79a9467b29d6d56263df5654a6ceab20a5bbdfff72a731300bb3dc303fc401f6

The older X-Webhook-Signature header

Endpoints built before September 2026 may check X-Webhook-Signature. It is the HMAC-SHA256 of the raw body alone, keyed with your secret, written as 64 lowercase hex characters with no prefix. It keeps working. It does not cover the time, though, so a captured message would still pass. Move to webhook-signature when you can.

Was this article helpful?