Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.pandabase.io/llms.txt

Use this file to discover all available pages before exploring further.

This is a list of all event types Pandabase can send to your configured webhook endpoints. Each event is delivered as a POST request with a JSON payload.

Webhook structure

Every webhook delivery contains the event type, a unique event ID, a timestamp, and a data object with order, customer, and geo information.
{
  "event": "PAYMENT_COMPLETED",
  "id": "evt_cm5x7k2a000001j0g8h3f9d2e",
  "timestamp": "2026-03-07T12:00:00.000Z",
  "data": {
    "order": {
      "id": "ord_cm5x7k2a000001j0g8h3f9d2e",
      "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e",
      "status": "COMPLETED",
      "amount": 5000,
      "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": 5000
        }
      ]
    },
    "customer": {
      "id": "cus_cm5x7k2a000001j0g8h3f9d2e",
      "email": "buyer@example.com"
    },
    "geo": {
      "ip": "1.2.3.4",
      "country": "US",
      "city": "Miami",
      "region": "FL"
    }
  }
}

Webhook headers

Every delivery includes these headers for verification and deduplication:
HeaderDescription
X-Pandabase-SignatureHMAC-SHA256 hex digest of the JSON body, signed with your webhook secret
X-Pandabase-TimestampUnix timestamp (milliseconds) of when the delivery was sent
X-Pandabase-IdempotencyUnique delivery identifier — use this to deduplicate retried deliveries
Content-TypeAlways application/json
User-AgentPandabase (https://pandabase.io)

Verifying signatures

Always verify the X-Pandabase-Signature header to confirm the webhook was sent by Pandabase and hasn’t been tampered with.
import crypto from "crypto";

function verifyWebhook(
  rawBody: string,
  signature: string,
  secret: string,
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

// In your handler:
const signature = request.headers["x-pandabase-signature"];
if (!verifyWebhook(request.rawBody, signature, WEBHOOK_SECRET)) {
  return response.status(401).send("Invalid signature");
}
Always use constant-time comparison (e.g. timingSafeEqual, hmac.compare_digest) when verifying signatures to prevent timing attacks.

Retries

Failed deliveries are retried up to 5 times with exponential backoff starting at 1 second (1s, 2s, 4s, 8s, 16s). Your endpoint should return a 2xx status code to acknowledge receipt. Any other status code or a timeout (15 seconds) is treated as a failure and triggers a retry. Use the X-Pandabase-Idempotency header to deduplicate deliveries — the same event may be delivered more than once due to retries.

Event types

PAYMENT_PENDING

Fired when a customer initiates payment at checkout. The order has been created and a payment intent is awaiting confirmation.
At this stage, geo data (city, region, country) may be null — it is enriched asynchronously after checkout. Subsequent events will include full geo data.

PAYMENT_COMPLETED

Fired when a payment is successfully collected. This is the primary event to listen for when fulfilling orders. The order status will be PROCESSING (if fulfillment is in progress) or COMPLETED (if already fulfilled). The items array contains all purchased products, customFields includes any data the customer submitted at checkout, and metadata contains any developer-defined key-value pairs set when creating the checkout session.

PAYMENT_FAILED

Fired when a payment fails, is canceled by the customer, or expires without being completed. The order is moved to CANCELLED status. If a coupon was applied, its usage count is automatically restored.

PAYMENT_REFUNDED

Fired when a charge is refunded, whether initiated through the Pandabase dashboard or detected from an external refund. The order status is set to REFUNDED.

PAYMENT_DISPUTED

Fired when a customer opens a chargeback dispute on a payment. The order moves to CHARGEBACK status and the disputed amount plus a $20.00 dispute fee is deducted from the store’s available balance.

PAYMENT_DISPUTE_WON

Fired when a dispute is resolved in the merchant’s favor. The disputed amount and fee are restored to the store’s available balance and the order returns to COMPLETED status.

PAYMENT_DISPUTE_LOST

Fired when a dispute is resolved against the merchant. The deducted funds remain lost and the order stays in CHARGEBACK status.

Subscription events

Subscription events include the same order, customer, and geo fields as payment events, plus a subscription object with the current subscription state. They are fired alongside (not instead of) payment events — for example, a successful renewal triggers both PAYMENT_COMPLETED and SUBSCRIPTION_RENEWED.
{
  "event": "SUBSCRIPTION_RENEWED",
  "id": "evt_cm5x7k2a000001j0g8h3f9d2e",
  "timestamp": "2026-04-18T00:00:00.000Z",
  "data": {
    "order": {
      "id": "ord_cm5x7k2a000001j0g8h3f9d2e",
      "orderNumber": "cs_cm5x7k2a000001j0g8h3f9d2e",
      "status": "COMPLETED",
      "amount": 1999,
      "currency": "USD",
      "customFields": null,
      "metadata": null,
      "items": [
        {
          "productId": "prd_cm5x7k2a000001j0g8h3f9d2e",
          "variantId": null,
          "name": "Pro Plan",
          "quantity": 1,
          "amount": 1999
        }
      ]
    },
    "customer": {
      "id": "cus_cm5x7k2a000001j0g8h3f9d2e",
      "email": "buyer@example.com"
    },
    "geo": null,
    "subscription": {
      "id": "sub_cm5x7k2a000001j0g8h3f9d2e",
      "status": "ACTIVE",
      "billingInterval": "MONTHLY",
      "amount": 1999,
      "currency": "USD",
      "currentPeriodStart": "2026-04-18T00:00:00.000Z",
      "currentPeriodEnd": "2026-05-18T00:00:00.000Z",
      "nextChargeAt": "2026-05-18T00:00:00.000Z",
      "trialEnd": null,
      "cancelledAt": null
    }
  }
}
The subscription field is only present on SUBSCRIPTION_* events. Payment events (PAYMENT_COMPLETED, etc.) do not include it.

SUBSCRIPTION_CREATED

Fired when a new subscription is activated. This happens when:
  • A customer completes checkout for a subscription product (first payment collected)
  • A customer starts a free trial (card saved, no charge)
The order in the payload is the initial order that created the subscription.
For trial subscriptions, the initial order has an amount of $0.00. The first charge happens when the trial ends.

SUBSCRIPTION_RENEWED

Fired when a recurring payment is successfully collected. This event is sent on every billing cycle — weekly, monthly, or yearly depending on the subscription’s billing interval. The order in the payload is the renewal order for this billing period. Use this event to extend access, deliver license keys, or update entitlements.

SUBSCRIPTION_PAST_DUE

Fired when a renewal payment fails or requires additional authentication (3D Secure). The subscription moves to PAST_DUE status. This can happen when:
  • The customer’s card is declined
  • The card requires 3DS verification (customer is emailed an authentication link)
  • The payment method has expired
Failed payments are automatically retried up to 3 times over 3 days (24h, 48h, 72h). If all retries fail, the subscription is cancelled and a SUBSCRIPTION_CANCELLED event is fired.
Do not revoke access on SUBSCRIPTION_PAST_DUE — the customer may still authenticate or the retry may succeed. Wait for SUBSCRIPTION_CANCELLED before revoking.

SUBSCRIPTION_CANCELLED

Fired when a subscription is cancelled. This can happen when:
  • The merchant cancels the subscription (immediately or at period end)
  • The customer cancels from the customer portal (always at period end)
  • All automatic payment retries are exhausted
If cancelled at period end, the customer retains access until the current billing period ends. Check the order’s metadata or the subscription API for the endsAt date.

SUBSCRIPTION_PAUSED

Fired when a merchant pauses a subscription. No further charges are made until the subscription is resumed. The customer retains access during the pause.

SUBSCRIPTION_RESUMED

Fired when a merchant resumes a previously paused subscription. Billing resumes at the end of the current period (or immediately if the period has already ended).

Integrating subscription webhooks

Here’s an example of handling subscription events in a webhook handler:
app.post("/webhooks/pandabase", (req, res) => {
  const signature = req.headers["x-pandabase-signature"];
  if (!verifyWebhook(req.rawBody, signature, WEBHOOK_SECRET)) {
    return res.status(401).send("invalid signature");
  }

  const { event, data } = req.body;

  switch (event) {
    case "SUBSCRIPTION_CREATED":
      // grant initial access
      await grantAccess(data.customer.email, data.order.items);
      break;

    case "SUBSCRIPTION_RENEWED":
      // extend access for the next billing period
      await extendAccess(data.customer.email, data.order.items);
      break;

    case "SUBSCRIPTION_CANCELLED":
      // schedule access revocation (check endsAt via API)
      await scheduleRevocation(data.customer.email);
      break;

    case "SUBSCRIPTION_PAST_DUE":
      // optionally notify your system, but don't revoke access yet
      await flagSubscriptionAtRisk(data.customer.email);
      break;

    case "SUBSCRIPTION_PAUSED":
    case "SUBSCRIPTION_RESUMED":
      // update internal subscription state
      await syncSubscriptionStatus(data.customer.email, event);
      break;
  }

  res.status(200).send({ received: true });
});

Best practices

  1. Return 200 quickly. Process webhook logic asynchronously if it takes more than a few seconds. Pandabase times out after 15 seconds.
  2. Deduplicate with idempotency keys. Store the X-Pandabase-Idempotency header value and skip events you’ve already processed.
  3. Verify signatures. Always validate X-Pandabase-Signature before processing the payload.
  4. Use event filtering. Subscribe only to the events you need to reduce noise and processing overhead.
  5. Handle out-of-order delivery. In rare cases, events may arrive out of order (e.g. PAYMENT_COMPLETED before PAYMENT_PENDING). Use the timestamp field and order status to handle this gracefully.
  6. Don’t revoke on PAST_DUE. For subscriptions, only revoke access on SUBSCRIPTION_CANCELLED. The PAST_DUE state is temporary — the payment may still succeed via retry or 3DS authentication.