We will assume you have a general understanding of programming. This guide is intended for developers, and we expect you to have a good understanding of REST APIs in general. If you’re not a developer, please skip this section.

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: drop the Pandabase Checkout SDK into your page 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:

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

Redirect

The simplest integration. Create a session on your backend and redirect the customer to the 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. It’s fairly stable and safe for production use, but the API may still see minor changes before general availability.

The Pandabase Checkout SDK drops checkout into any website with a single script tag. Checkout renders inside a sandboxed iframe, so card data never touches your page, and is fully themeable to match your brand.

  • 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:

Value Where it comes from
storeId Your store identifier (shp_…).
sessionId A 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.

Mode Appearance Open with
modal (default) Compact centered card over a dimmed backdrop .open()
drawer Panel that slides in from the right, full height .open()
overlay Full-page blurred backdrop, wide two-column (summary + form) .open()
inline Mounts directly into an element on your page auto-mounts

These three open imperatively. 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

Token Type Description
colorScheme "light" | "dark" Force a scheme regardless of system/theme.
accent color Primary action color (buttons, focus, selected).
accentForeground color Text/icon on top of accent.
background color Page/surface background.
foreground color Primary text.
muted color Subtle/secondary surface.
mutedForeground color Secondary text.
secondary color Secondary button/surface.
secondaryForeground color Text on secondary.
card color Card surface.
cardForeground color Text on cards.
popover color Dropdown/popover surface.
popoverForeground color Text in dropdowns/popovers.
ring color Focus-ring color.
border color Borders, dividers, input outlines.
input color Input field background.
danger color Error/danger color.
summaryBackground color Order-summary panel background.
radius number (px) Corner radius for inputs/buttons/cards.
fontFamily string Sans font stack.
fontMono string Monospace font stack.
fontSizeBase number (px) Base font size for payment fields.
summaryPosition "left" | "right" | "top" Summary placement (overlay only).
logo string (URL) Override the merchant logo in the summary.
dark object Any of the above (except colorScheme/summaryPosition), applied in dark scheme.

Scheme precedence: appearance.colorSchemetheme 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) bounce the buyer off-site and back; the embed auto-redirects them to your return_url. Nothing extra to do.
  • 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
Option Type Default Notes
storeId string Required.
sessionId string Required.
mode "modal" | "drawer" | "overlay" | "inline" "modal"
container string | HTMLElement Required for inline. CSS selector or element.
theme "light" | "dark" | "auto" "auto"
appearance object See Theming.
locale string BCP-47 (e.g. "en", "fr-FR"); localizes payment fields.
on object Event handlers, see Events.

SDK methods

Method Description
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: { … }.

Event Payload Fires 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.tsimport { 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.tsximport { 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"}