Skip to main content

Verify webhook signatures

Authenticate every webhook before using its contents. NextAppMarket signs the exact request body with an endpoint-specific secret.

Signature format​

The request includes these headers:

HeaderPurpose
X-Key-IdSelects a trusted signing key for this endpoint.
X-TimestampCanonical Unix timestamp in seconds.
X-SignatureBase64URL-encoded HMAC-SHA256, without padding.
X-Event-IdMust equal the event ID in the signed body.

The signed message is the timestamp, a literal period, and the raw body bytes:

signed_message = ASCII(timestamp) + b"." + raw_body
signature = BASE64URL_NO_PADDING(HMAC_SHA256(secret, signed_message))

Whitespace, key order, and a trailing newline are part of the signed body. Read the raw body before a JSON parser or framework middleware changes it.

Use the Python reference verifier​

Download verify_webhook.py and webhook-contract.json into the same directory. Install its schema-validation dependency:

python -m pip install 'jsonschema>=4.18,<5'

Call verify_webhook with the raw bytes, request headers, and your trusted key configuration:

import os
from verify_webhook import verify_webhook

keyring = {
os.environ["WEBHOOK_KEY_ID"]: {
"secret": os.environ["WEBHOOK_SECRET"],
"app_id": os.environ["PAYMENT_APP_ID"],
"environment": "production",
"endpoint_id": os.environ["WEBHOOK_ENDPOINT_ID"],
"revoked": False,
"not_after": None,
}
}

# raw_body must be the original request bytes.
event = verify_webhook(raw_body, request_headers, keyring)

raw_body and request_headers come from your HTTP framework. Load revoked and not_after from the key's current lifecycle state in your production key store. The static configuration above illustrates a single active key.

The verifier checks the timestamp window, constant-time signature comparison, UTF-8 JSON without duplicate object keys, header/body event-ID agreement, key binding, event schema, and verification-challenge expiry. Its default time tolerance is 300 seconds in either direction. Keep the receiver's clock synchronized.

It returns the validated event or raises WebhookError. Return a non-2xx response for invalid requests. The verifier does not write an inbox, grant an entitlement, or send an HTTP response; those steps belong to your application.

Connect verification to durable processing​

For a regular event, commit its original bytes, hash, identity, and pending work before acknowledging. If the same event ID is already stored with another body hash, quarantine the request for investigation. Keep business processing idempotent even if multiple events describe the same payment.

For an endpoint.verification event, return the validated data.challenge as described in endpoint verification.

The X-Event-Id header is checked against the signed body. It is not separately included in the HMAC message, so trusting the header alone would leave event identity unverified.

Verify your implementation locally​

The signature fixture includes a synthetic secret, exact body encoded as Base64, timestamp, expected headers, and a fixed verification time. Use that fixed time only in local tests. For real deliveries, use your server's current time.

Test rejection after changing one body byte, changing the event ID header, using another application's key, revoking a key, or moving the timestamp outside the accepted window. Also test that an identical retry does not repeat fulfillment.