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
| Mode | Format | Status |
|---|---|---|
| V2 | Standard Webhooks v1 | Required. Default for new endpoints. |
| V1 | Hex 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
| Header | Value |
|---|---|
Webhook-Id | Message id. Matches payload.id. Deduplicate on this. |
Webhook-Timestamp | Unix timestamp in seconds. Reject if older than 5 minutes. |
Webhook-Signature | v1,<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.
| Attempt | Delay | Elapsed |
|---|---|---|
| 1 | — | 00:00:00 |
| 2 | 5s | 00:00:05 |
| 3 | 5m | 00:05:05 |
| 4 | 30m | 00:35:05 |
| 5 | 2h | 02:35:05 |
| 6 | 5h | 07:35:05 |
| 7 | 10h | 17:35:05 |
| 8 | 14h | 31:35:05 |
| 9 | 20h | 51:35:05 |
| 10 | 24h | 75: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
statusfield.
