Pandabase
Learn

Hosted Checkout

Embed or redirect to Pandabase's hosted checkout page.

Overview

Every checkout session returns a checkout_url that points to Pandabase's hosted checkout page. There are three ways to use it:

  1. Redirect: send the customer to the checkout URL
  2. SDK embed: use the Pandabase Checkout SDK to render checkout as a modal, drawer, overlay, or inline element
  3. Direct link: share the checkout URL directly

The checkout page is hosted at checkout.pandabase.io with two routes:

RoutePurpose
/pay/stores/{storeId}/sids/{sessionId}Full-page checkout (used for redirects)
/embed/stores/{storeId}/sids/{sessionId}Embed-optimized checkout (used by the SDK)

Redirect

Create a session on your backend and redirect the customer to its checkout_url.

const STORE_ID = process.env.PANDABASE_STORE_ID!;

app.post("/buy", async (req, res) => {
  const response = await fetch(
    `https://api.pandabase.io/v2/stores/${STORE_ID}/checkouts`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        items: [{ product_id: "prd_xxx", quantity: 1 }],
        return_url: "https://yoursite.com/thank-you",
      }),
    },
  );

  const { data } = await response.json();
  res.redirect(data.checkout_url);
});

After payment, the customer is redirected to your return_url. If no return_url is set, they see a confirmation screen on the checkout page.

The redirect only means the customer completed the flow. It does not guarantee payment success. Always use webhooks to confirm payments on your backend.

Checkout SDK

SDK v2 is in public preview. The API may change before general availability.

Add the Checkout SDK to your site with a script tag. It renders checkout in a sandboxed iframe that keeps card data separate from your page. You can customize its colors, fonts, and layout.

  • No framework required. A vanilla <script> and a global Pandabase object.
  • Four presentations. Modal, drawer, full-page overlay, or inline.
  • Themeable. Colors, radius, fonts, and light/dark, all via tokens.

Prerequisites

You need two things:

ValueWhere it comes from
storeIdYour store identifier (shp_…).
sessionIdA checkout session created server-side (cs_…). It carries the cart, amounts, and return_url. Single-use.

Always create the session from your backend, then pass its id to the SDK in the browser. Never embed API secrets in the page.

Installation

Add the SDK script to your page:

<script src="https://secure.pandabase.io/v2/sdk.js"></script>

This exposes a global Pandabase with one method, Pandabase.checkout(options), which returns an instance with open(), close(), and destroy().

Always load the SDK from https://secure.pandabase.io/v2/sdk.js. Never self-host, bundle, vendor, or proxy this script. We push security and PCI-scope updates to the hosted version continuously, so a self-hosted copy will go stale, break the iframe's origin checks, and is unsupported.

Quick start

Open checkout in a modal, which is the default presentation:

<button id="pay">Checkout</button>

<script src="https://secure.pandabase.io/v2/sdk.js"></script>
<script>
  const checkout = Pandabase.checkout({
    storeId: "shp_abc123",
    sessionId: "cs_xyz789",
    on: {
      payment_success: (e) => {
        window.location.href = e.returnUrl ?? "/thank-you";
      },
    },
  });

  document.getElementById("pay").addEventListener("click", () => checkout.open());
</script>

Presentation modes

Set mode to choose how checkout appears. Each mode has its own layout.

ModeAppearanceOpen with
modal (default)Compact centered card over a dimmed backdrop.open()
drawerPanel that slides in from the right, full height.open()
overlayFull-page blurred backdrop, wide two-column (summary + form).open()
inlineMounts directly into an element on your pageauto-mounts

Call open() to show checkout and close() to dismiss it.

const checkout = Pandabase.checkout({
  storeId: "shp_abc123",
  sessionId: "cs_xyz789",
  mode: "overlay", // or "modal" | "drawer"
});

checkout.open();
// later: checkout.close();

Inline

Inline mounts immediately into the container you provide, so no open() is needed.

<div id="checkout"></div>

<script src="https://secure.pandabase.io/v2/sdk.js"></script>
<script>
  Pandabase.checkout({
    storeId: "shp_abc123",
    sessionId: "cs_xyz789",
    mode: "inline",
    container: "#checkout", // CSS selector or an HTMLElement
  });
</script>

Theming

Pass an appearance object to match your brand. All colors accept any CSS color string; radius and fontSizeBase are numbers (px). The same tokens style both the checkout UI and the payment fields, so everything stays consistent.

Pandabase.checkout({
  storeId,
  sessionId,
  theme: "auto", // "light" | "dark" | "auto" (default)
  appearance: {
    accent: "#6d28d9",
    accentForeground: "#ffffff",
    background: "#ffffff",
    foreground: "#0a0a0a",
    muted: "#f4f4f5",
    border: "#e5e5e5",
    summaryBackground: "#f5f3ff",
    radius: 10,
    fontFamily: '"Inter", ui-sans-serif, system-ui, sans-serif',
    summaryPosition: "left", // overlay only: "left" | "right" | "top"

    // Optional: ship a distinct dark palette, applied when the resolved
    // scheme is dark (system preference, `theme: "dark"`, or colorScheme).
    dark: {
      background: "#0b0b12",
      foreground: "#e0e7ff",
      muted: "#17171f",
      border: "#262633",
      summaryBackground: "#101019",
    },
  },
});

Appearance tokens

TokenTypeDescription
colorScheme"light" | "dark"Force a scheme regardless of system/theme.
accentcolorPrimary action color (buttons, focus, selected).
accentForegroundcolorText/icon on top of accent.
backgroundcolorPage/surface background.
foregroundcolorPrimary text.
mutedcolorSubtle/secondary surface.
mutedForegroundcolorSecondary text.
secondarycolorSecondary button/surface.
secondaryForegroundcolorText on secondary.
cardcolorCard surface.
cardForegroundcolorText on cards.
popovercolorDropdown/popover surface.
popoverForegroundcolorText in dropdowns/popovers.
ringcolorFocus-ring color.
bordercolorBorders, dividers, input outlines.
inputcolorInput field background.
dangercolorError/danger color.
summaryBackgroundcolorOrder-summary panel background.
radiusnumber (px)Corner radius for inputs/buttons/cards.
fontFamilystringSans font stack.
fontMonostringMonospace font stack.
fontSizeBasenumber (px)Base font size for payment fields.
summaryPosition"left" | "right" | "top"Summary placement (overlay only).
logostring (URL)Override the merchant logo in the summary.
darkobjectAny of the above (except colorScheme/summaryPosition), applied in dark scheme.

Scheme precedence: appearance.colorScheme → theme option → system preference. Tokens you omit fall back to the default light/dark theme.

Handling results & redirects

For inline, modal, and drawer, handle payment_success to send the buyer onward. Card payments that complete in-place do not auto-redirect.

Pandabase.checkout({
  storeId,
  sessionId,
  on: {
    payment_success: (e) => {
      window.location.href = e.returnUrl ?? "/thank-you";
    },
  },
});

Behavior depends on the payment type:

  • Redirect-based methods (Cash App, bank debits, some 3-D Secure) send the customer to another site and back; the embed auto-redirects them to your return_url.
  • In-page card success fires payment_success (with returnUrl) and leaves the navigation to you. The SDK never takes over the merchant's page.

SDK options

Pandabase.checkout(options) → instance
OptionTypeDefaultNotes
storeIdstring—Required.
sessionIdstring—Required.
mode"modal" | "drawer" | "overlay" | "inline""modal"
containerstring | HTMLElement—Required for inline. CSS selector or element.
theme"light" | "dark" | "auto""auto"
appearanceobject—See Theming.
localestring—BCP-47 (e.g. "en", "fr-FR"); localizes payment fields.
onobject—Event handlers, see Events.

SDK methods

MethodDescription
checkout.open()Open (modal/drawer/overlay). No-op for inline.
checkout.close()Close (modal/drawer/overlay). No-op for inline.
checkout.destroy()Remove listeners and all DOM created by the instance.

Events

Supply event handlers as on: { … }.

EventPayloadFires when
loaded{ sessionId }The checkout iframe loaded.
ready—The checkout UI is mounted.
payment_success{ orderId?, returnUrl? }Payment confirmed.
payment_processing—Async payment is pending.
payment_failed{ error? }Payment failed.
error{ error }Session/load error (incl. iframe failed to load).
close—The buyer closed the checkout.
Pandabase.checkout({
  storeId,
  sessionId,
  on: {
    loaded: (e) => console.log("loaded", e.sessionId),
    ready: () => console.log("ready"),
    payment_success: (e) => (window.location.href = e.returnUrl ?? "/done"),
    payment_processing: () => console.log("processing"),
    payment_failed: (e) => console.warn("failed:", e.error),
    error: (e) => console.error(e.error),
    close: () => console.log("closed"),
  },
});

Legacy flat callbacks (onPaymentSuccess, onClose, …) are still accepted for backwards compatibility, but the grouped on object is preferred.

Full example

<!doctype html>
<html>
  <body>
    <button id="buy">Buy now</button>

    <script src="https://secure.pandabase.io/v2/sdk.js"></script>
    <script>
      const checkout = Pandabase.checkout({
        storeId: "shp_abc123",
        sessionId: "cs_xyz789",
        mode: "overlay",
        theme: "auto",
        locale: "en",
        appearance: {
          accent: "#0284c7",
          radius: 10,
          summaryPosition: "left",
          dark: { background: "#071726", foreground: "#e0f2fe" },
        },
        on: {
          payment_success: (e) => {
            window.location.href = e.returnUrl ?? "/thank-you";
          },
          payment_failed: (e) => alert(e.error ?? "Payment failed"),
          error: (e) => console.error(e.error),
        },
      });

      document.getElementById("buy").addEventListener("click", () => checkout.open());
    </script>
  </body>
</html>

React

Load the SDK script once, then use a hook to manage the checkout lifecycle.

// hooks/use-checkout.ts
import { useEffect, useRef, useCallback } from "react";

declare global {
  interface Window {
    Pandabase?: {
      checkout: (opts: PandabaseCheckoutOptions) => PandabaseCheckout;
    };
  }
}

type CheckoutMode = "modal" | "drawer" | "overlay" | "inline";

interface PandabaseCheckoutOptions {
  storeId: string;
  sessionId: string;
  mode?: CheckoutMode;
  container?: string | HTMLElement;
  theme?: "light" | "dark" | "auto";
  appearance?: Record<string, unknown>;
  locale?: string;
  on?: {
    loaded?: (e: { sessionId: string }) => void;
    ready?: () => void;
    payment_success?: (e: { orderId?: string; returnUrl?: string }) => void;
    payment_processing?: () => void;
    payment_failed?: (e: { error?: string }) => void;
    error?: (e: { error: unknown }) => void;
    close?: () => void;
  };
}

interface PandabaseCheckout {
  open: () => void;
  close: () => void;
  destroy: () => void;
}

const SDK_URL = "https://secure.pandabase.io/v2/sdk.js";

let sdkLoaded = false;
let sdkPromise: Promise<void> | null = null;

function loadSdk(): Promise<void> {
  if (sdkLoaded) return Promise.resolve();
  if (sdkPromise) return sdkPromise;

  sdkPromise = new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = SDK_URL;
    script.onload = () => {
      sdkLoaded = true;
      resolve();
    };
    script.onerror = () => reject(new Error("Failed to load Pandabase SDK"));
    document.head.appendChild(script);
  });

  return sdkPromise;
}

export function useCheckout() {
  const checkoutRef = useRef<PandabaseCheckout | null>(null);

  const open = useCallback(async (opts: PandabaseCheckoutOptions) => {
    await loadSdk();

    if (checkoutRef.current) {
      checkoutRef.current.destroy();
    }

    checkoutRef.current = window.Pandabase!.checkout({
      mode: "modal",
      theme: "auto",
      ...opts,
    });

    checkoutRef.current.open();
  }, []);

  const close = useCallback(() => {
    checkoutRef.current?.close();
  }, []);

  useEffect(() => {
    return () => {
      checkoutRef.current?.destroy();
    };
  }, []);

  return { open, close };
}
// components/buy-button.tsx
import { useState } from "react";
import { useCheckout } from "@/hooks/use-checkout";

export function BuyButton() {
  const [loading, setLoading] = useState(false);
  const checkout = useCheckout();

  async function handleClick() {
    setLoading(true);

    try {
      const res = await fetch("/api/create-checkout", { method: "POST" });
      const { storeId, sessionId } = await res.json();

      checkout.open({
        storeId,
        sessionId,
        mode: "overlay",
        on: {
          payment_success: (e) => {
            window.location.href = e.returnUrl ?? "/thank-you";
          },
          payment_failed: (e) => console.error("failed:", e.error),
        },
      });
    } finally {
      setLoading(false);
    }
  }

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? "Loading..." : "Buy Premium Plan — $29.99"}
    </button>
  );
}

For inline mode in React, pass a ref as the container:

import { useRef, useEffect } from "react";
import { useCheckout } from "@/hooks/use-checkout";

interface InlineCheckoutProps {
  storeId: string;
  sessionId: string;
}

export function InlineCheckout({ storeId, sessionId }: InlineCheckoutProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const checkout = useCheckout();

  useEffect(() => {
    if (!containerRef.current) return;

    checkout.open({
      storeId,
      sessionId,
      mode: "inline",
      container: containerRef.current,
      on: {
        payment_success: (e) => console.log("paid:", e.orderId),
      },
    });
  }, [storeId, sessionId]);

  return <div ref={containerRef} />;
}

Notes & limits

  • summaryPosition (left/right) only applies to the overlay mode, which is wide enough for two columns; other modes stack the summary on top.
  • Cleanup: call destroy() if you remove the checkout (e.g. on an SPA route change) to detach listeners and DOM.
  • Multiple instances: each checkout() call is independent; don't open two modals at once.

Return URL

Set a return_url when creating the session to redirect customers back to your site after payment. It applies to the full-page checkout (/pay/ route) and to redirect-based methods in the embed. For in-page card success, the SDK hands the returnUrl to your payment_success handler so you control the navigation.

{
  "items": [{ "product_id": "prd_xxx", "quantity": 1 }],
  "return_url": "https://yoursite.com/thank-you"
}

Last updated on

On this page