Pandabase
Webhooks

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.

ModeSignatureStatus
V2Standard Webhooks v1Required. Default for endpoints after 2026-05-15.
V1Hex 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 withModeVerify as
v1, (then base64)V2Standard Webhooks
bare hex (no prefix)V1verifyV1

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 → Webhooks

Open 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 → Webhooks

Open 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

V1V2
EncodingHexv1,<base64> (space-separated list)
Signed string${timestamp}.${rawBody}${Webhook-Id}.${timestamp}.${rawBody}
Webhook-TimestampMillisecondsSeconds
Webhook-Id${webhookId}/${jobId}Message id (matches payload.id)
X-Pandabase-* headersSentNot 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.

HeaderX-Pandabase-*Webhook-* (V1)
SignatureX-Pandabase-SignatureWebhook-Signature
TimestampX-Pandabase-Timestamp (ms)Webhook-Timestamp (ms)
IdempotencyX-Pandabase-IdempotencyWebhook-Id
Signed stringrawBody${timestamp}.${rawBody}
Replay protectionNoneReject 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-Signature is a space-separated list — check each v1,… entry.
  • Keep your server NTP-synced. >5 min drift rejects valid events.

Troubleshooting

FAQ

Last updated on

On this page