Pandabase
Webhooks

Overview

Listen for store events and trigger actions in your application.

Pandabase sends each event to your webhook endpoint using an HTTP POST request.

Use webhooks to fulfill orders, update subscriptions, or respond to other store activity.

See the event reference for the full list of supported events.

Payload

{
  "event": "PAYMENT_COMPLETED",
  "id": "evt_cm5x7k2a000001j0g8h3f9d2e",
  "timestamp": "2026-03-07T12:00:00.000Z",
  "data": {
    "order": {
      "id": "ord_cm5x7k2a000001j0g8h3f9d2e",
      "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e",
      "status": "COMPLETED",
      "amount": 2999,
      "currency": "USD",
      "customFields": { "discord": "johndoe#1234" },
      "metadata": { "campaign": "spring_sale", "ref": "partner_abc" },
      "items": [
        {
          "productId": "prd_cm5x7k2a000001j0g8h3f9d2e",
          "variantId": null,
          "name": "Pro Plan",
          "quantity": 1,
          "amount": 2999
        }
      ]
    },
    "customer": {
      "id": "cus_cm5x7k2a000001j0g8h3f9d2e",
      "email": "buyer@example.com"
    },
    "geo": { "ip": "1.2.3.4", "country": "US", "city": "Miami", "region": "FL" }
  }
}

Amounts are integers in cents.

Signature modes

ModeFormatStatus
V2Standard Webhooks v1Required. Default for new endpoints.
V1Hex Webhook-Signature + legacy X-Pandabase-*Deprecated. Migrate.

This page documents V2.

The verifiers below — including any Standard Webhooks SDK — only validate V2 endpoints. V1 sends the same Webhook-* header names with hex/millisecond values, so a V2 verifier rejects every V1 delivery. Check your endpoint's signatureVersion first; if it's V1, follow the migration guide.

Headers

HeaderValue
Webhook-IdMessage id. Matches payload.id. Deduplicate on this.
Webhook-TimestampUnix timestamp in seconds. Reject if older than 5 minutes.
Webhook-Signaturev1,<base64> HMAC-SHA256 of ${Webhook-Id}.${Webhook-Timestamp}.${rawBody}.

Retries

2xx is success. Anything else — non-2xx, 15s timeout, connection error — retries per the Standard Webhooks schedule over ~75 hours, ±10% jitter.

AttemptDelayElapsed
1—00:00:00
25s00:00:05
35m00:05:05
430m00:35:05
52h02:35:05
65h07:35:05
710h17:35:05
814h31:35:05
920h51:35:05
1024h75:35:05

If all 10 attempts fail, the store owner receives an email, limited to one per endpoint every 6 hours.

Verification

Hash the raw request body. Whitespace and key order change the bytes.

SDK

import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.WEBHOOK_SECRET!);

app.post(
  "/webhooks/pandabase",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      const event = wh.verify(req.body.toString(), req.headers as any);
      // event is the parsed JSON body, signature already verified

      switch (event.event) {
        case "PAYMENT_COMPLETED":
          console.log(`Payment completed for order ${event.data.order.id}`);
          break;
        case "PAYMENT_REFUNDED":
          console.log(`Refund issued for order ${event.data.order.id}`);
          break;
      }

      res.status(200).send("OK");
    } catch {
      res.status(401).send("Invalid signature");
    }
  },
);

Manual

import crypto from "node:crypto";

const TOLERANCE_MS = 5 * 60 * 1000;

function verifyWebhook(
  headers: Record<string, string>,
  rawBody: string,
  secret: string,
): boolean {
  const msgId = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatureHeader = headers["webhook-signature"];
  if (!msgId || !timestamp || !signatureHeader) return false;

  // Webhook-Signature is a space-separated list of `vN,<base64>` entries.
  // We only need to match one.
  const signatures = signatureHeader
    .split(" ")
    .filter((s) => s.startsWith("v1,"))
    .map((s) => s.slice(3));

  const signedPayload = `${msgId}.${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("base64");

  const valid = signatures.some(
    (sig) =>
      sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)),
  );
  if (!valid) return false;

  const skewMs = Math.abs(Date.now() - Number(timestamp) * 1000);
  return skewMs <= TOLERANCE_MS;
}

Best practices

  • Return a 2xx response promptly, then process the event asynchronously.
  • Verify every request. Reject stale timestamps.
  • Deduplicate on Webhook-Id.
  • Subscribe only to events you need.
  • Don't rely on delivery order — use the order status field.

Last updated on

On this page