Migrate webhook signatures
Upgrade to Standard Webhooks v2 before V1 is deprecated.
V1 is being deprecated. All endpoints must move to V2. New endpoints are V2 by default; existing V1 endpoints will be auto-migrated after a 60-day notice period.
Endpoints run in V1 or V2.
| Mode | Signature | Status |
|---|---|---|
| V2 | Standard Webhooks v1 | Required. Default for endpoints after 2026-05-15. |
| V1 | Hex over ${timestamp}.${rawBody} (ms) + X-Pandabase-* | Deprecated. Migrate before the cutoff. |
Check your mode with GET /v2/stores/:storeId/webhooks/:webhookId (signatureVersion) or the dashboard.
A Standard Webhooks SDK rejects V1 deliveries. Both modes use the same
Webhook-* header names, but V1 uses a hex signature, a different signed
string, and a timestamp in milliseconds. A V2 verifier expects seconds
and rejects that timestamp as outside the 5-minute window. This can appear
as a "timestamp" or "stale" error.
Confirm signatureVersion is V2 before you point a Standard Webhooks SDK
(or any V2-only verifier) at the endpoint. Doing it the other way around drops
live events until you switch. See Troubleshooting.
Telling the modes apart on the wire
Check the Webhook-Signature format to identify the mode:
| Signature header starts with | Mode | Verify as |
|---|---|---|
v1, (then base64) | V2 | Standard Webhooks |
| bare hex (no prefix) | V1 | verifyV1 |
A receiver that supports both modes can use this prefix to choose a verifier. The authoritative answer for a
given endpoint is always its signatureVersion.
V1 → V2
Deploy a V2 verifier
We require webhook signatures to be verified using the raw request body and your webhook signing secret.
You can use the snippets from the verification guide, or install a
Standard Webhooks SDK
to verify deliveries.
Your endpoint is still on V1 until you complete the next step, so a
V2-only verifier will reject every live delivery in the gap between
deploying it and switching the version. Until you've switched and tested,
either keep your existing V1 verifier running, or accept the V2 signature
and fall back to verifyV1 when
Webhook-Signature is bare hex (see
Telling the modes apart). High-volume
endpoints should not deploy a V2-only verifier first.
Switch your webhook version
In your merchant dashboard, navigate to:
Your Store → Developers → WebhooksOpen the webhook dropdown menu, click Edit, and select the webhook version you want to enable.
Send a test event
After updating the version, return to:
Your Store → Developers → WebhooksOpen the dropdown menu and click Send test to verify that your endpoint is receiving and validating events correctly.
Roll back if needed
You can revert to a previous webhook version at any time.
Go back to Your Store → Developers → Webhooks, click Edit, and select the previous version.
Differences
| V1 | V2 | |
|---|---|---|
| Encoding | Hex | v1,<base64> (space-separated list) |
| Signed string | ${timestamp}.${rawBody} | ${Webhook-Id}.${timestamp}.${rawBody} |
Webhook-Timestamp | Milliseconds | Seconds |
Webhook-Id | ${webhookId}/${jobId} | Message id (matches payload.id) |
X-Pandabase-* headers | Sent | Not sent |
V1 replay protection
Moving from X-Pandabase-* to the V1 Webhook-* headers. V1 endpoints already send Webhook-* alongside the legacy headers, so you can move off X-Pandabase-* (which has no replay protection) without switching modes yet.
These V1 Webhook-* headers are not Standard Webhooks compatible — same
header names, but hex encoding (no v1, prefix), a ${timestamp}.${rawBody}
signed string, and a millisecond timestamp. Verify them with verifyV1
below, not a Standard Webhooks SDK. To use an off-the-shelf SDK, migrate the
endpoint to V2 first.
| Header | X-Pandabase-* | Webhook-* (V1) |
|---|---|---|
| Signature | X-Pandabase-Signature | Webhook-Signature |
| Timestamp | X-Pandabase-Timestamp (ms) | Webhook-Timestamp (ms) |
| Idempotency | X-Pandabase-Idempotency | Webhook-Id |
| Signed string | rawBody | ${timestamp}.${rawBody} |
| Replay protection | None | Reject events older than 5 minutes |
Same secret, same hex encoding.
import crypto from "node:crypto";
const TOLERANCE_MS = 5 * 60 * 1000;
export function verifyV1(
headers: Record<string, string>,
rawBody: string,
secret: string,
): boolean {
const ts = headers["webhook-timestamp"];
const sig = headers["webhook-signature"];
if (!ts || !sig) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
if (expected.length !== sig.length) return false;
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return false;
return Math.abs(Date.now() - Number(ts)) <= TOLERANCE_MS;
}Example deliveries
V2:
POST /webhooks/pandabase HTTP/1.1
Webhook-Id: evt_cm5x7k2a000001j0g8h3f9d2e
Webhook-Timestamp: 1715688123
Webhook-Signature: v1,7ZH9F8sZqxQk6vJtN5cBp0LmRwXyP3aT8eK1nDhU2gI=
{ "event": "PAYMENT_COMPLETED", "id": "evt_cm5x7k2a000001j0g8h3f9d2e", ... }V1:
POST /webhooks/pandabase HTTP/1.1
Webhook-Id: whk_abc/job_xyz
Webhook-Timestamp: 1715688123456
Webhook-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-Pandabase-Idempotency: whk_abc/job_xyz
X-Pandabase-Timestamp: 1715688123456
X-Pandabase-Signature: 0a4d55a8d778e5022fab701977c5d840bbc486d0
{ "event": "PAYMENT_COMPLETED", ... }Pitfalls
- Hash the raw body, not a re-serialized JSON object.
- Use
timingSafeEqual/hmac.compare_digest. Never==. - Reject stale timestamps. The signature alone doesn't stop replay.
- V2 timestamps are seconds, V1 are milliseconds.
- V2
Webhook-Signatureis a space-separated list — check eachv1,…entry. - Keep your server NTP-synced. >5 min drift rejects valid events.
Troubleshooting
Check your endpoint's signatureVersion. Standard Webhooks SDKs expect V2,
but V1 uses a hex signature, a different signed string, and a timestamp in
milliseconds. These differences cause verification to fail.
Switch the endpoint to V2, or use verifyV1 until
you migrate.
You likely have a verifier hard-coded to one mode while the endpoint's
signatureVersion changed (or you're mid-rollout across machines). Branch on
the Webhook-Signature prefix — v1, is V2, bare hex is V1 — or pin both
sides to the same mode. See
Telling the modes apart.
Check whether your handler is hashing a re-serialized body. Hash the raw bytes
exactly as received — JSON re-encoding changes whitespace and key order, which
changes the signature. In Express, use express.raw({ type: "application/json" }).
FAQ
Yes. V1 is being deprecated. Migrate before the cutoff or your endpoint will be auto-migrated.
After a 60-day notice period, emailed to your store contact. The deprecation date will be announced in the changelog.
Yes.
Yes during migration. signatureVersion is per-endpoint.
