docs

@xpay/sdk-react

The React SDK. XPayProvider, hooks, and components built on top of @xpay/sdk-js.

The XPay React SDK is a thin wrapper around @xpay/sdk-js that exposes a provider, hooks, and a component for the payment element. The provider creates an Elements instance from a clientSecret, the hooks expose the live session state and action methods, and the components mount payment UI.

pnpm add @xpay/sdk-js @xpay/sdk-react
# Both packages are required. @xpay/sdk-react peer-depends on @xpay/sdk-js, react ^18 || ^19, react-dom ^18 || ^19.

This page is the reference for every export. For walkthroughs, see Drop-in and Elements under Integrate.

Quick start

The recommended pattern is loadXPay at module level (or in a small loader file), then create a Checkout Session on your server, hold the returned clientSecret in state, and render <XPayProvider> once you have it. useCheckout() then reads the live session inside any child component.

lib/xpay.ts
"use client";

import { loadXPay } from "@xpay/sdk-js";

// Module-level: load once, share across all components.
export const xpayPromise = loadXPay(process.env.NEXT_PUBLIC_XPAY_PUBLISHABLE_KEY!);
app/checkout/page.tsx
"use client";

import { useState } from "react";
import { XPayProvider, useCheckout, PaymentElement } from "@xpay/sdk-react";
import { xpayPromise } from "@/lib/xpay";

export default function CheckoutPage() {
  const [clientSecret, setClientSecret] = useState<string | null>(null);
  const [creating, setCreating] = useState(false);

  const startCheckout = async () => {
    setCreating(true);
    const res = await fetch("/api/create-checkout", { method: "POST" });
    const { clientSecret } = await res.json();
    setClientSecret(clientSecret);
    setCreating(false);
  };

  if (!clientSecret) {
    return (
      <button onClick={startCheckout} disabled={creating}>
        {creating ? "Starting…" : "Pay now"}
      </button>
    );
  }

  return (
    <XPayProvider xpay={xpayPromise} options={{ clientSecret }}>
      <CheckoutForm />
    </XPayProvider>
  );
}

function CheckoutForm() {
  const state = useCheckout();

  if (state.type === "loading") return <p>Loading…</p>;
  if (state.type === "error") return <p>{state.error.message}</p>;

  const { checkout } = state;
  if (checkout.status.type === "expired") return <p>This checkout has expired.</p>;
  if (checkout.status.type === "complete") return <p>Payment complete. Thank you.</p>;

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const result = await checkout.confirm({
      customerDetails: { email: "customer@example.com" },
    });
    if (result.type === "error") {
      alert(result.error.message);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button type="submit" disabled={!checkout.canConfirm}>
        Pay {checkout.currency} {(checkout.amountTotal / 100).toFixed(2)}
      </button>
    </form>
  );
}

The clientSecret lives in state, so XPayProvider only mounts once and /api/create-checkout is called exactly once per checkout. If you create the session earlier in your flow (a click on a product page, a server component, a route param), pass the resulting string in any way you like: props, search params, context. The provider only needs the string.

useCheckout() updates reactively after every promo code, quantity change, or fee recalculation. There's no need for an onChange callback in React; the component re-renders with the latest checkout data on its own.

Exports at a glance

ExportKindWhat it is
XPayProvidercomponentWraps your checkout UI and provides context to all child components.
useCheckouthookReturns the disjoint union state and the merged session-plus-actions object.
useXPayhookReturns the underlying XPayInstance (or null).
useElementshookReturns the underlying Elements instance (or null).
useConfirmPaymenthookConvenience hook that exposes confirmPayment from the active checkout.
PaymentElementcomponentMounts the payment-method selector with the card form.
CheckoutButtoncomponentA pre-built button that opens the drop-in checkout modal on click.
CheckouttypeCheckoutSession & CheckoutActions. The merged shape useCheckout returns on success.
UseCheckoutResulttypeThe disjoint union returned by useCheckout.
PaymentElementProps, CheckoutButtonPropstypeComponent prop types.

The package re-exports a few core types from @xpay/sdk-js for convenience: CheckoutSession, CheckoutActions, PaymentMethodInfo, Appearance, ActionResult, XPayError, ConfirmPaymentOptions, PaymentElementChangeEvent, ElementsOptions. Import additional types directly from @xpay/sdk-js when you need them.

<XPayProvider>

Wraps the part of your app that runs the checkout. Creates an Elements instance from options.clientSecret and provides three React contexts: the XPayInstance, the Elements, and the loading-state-aware checkout state.

<XPayProvider xpay={xpayPromise} options={{ clientSecret, appearance, locale }}>
  {children}
</XPayProvider>
PropTypeDescription
xpayXPayInstance | Promise<XPayInstance | null> | nullThe instance from loadXPay(). Pass the Promise; the provider awaits it on the client.
options.clientSecretstring | Promise<string>The Checkout Session client secret. Required to create an Elements instance and load the session.
options.appearanceAppearanceOptional. Branding overrides merged with the session's server-side branding.
options.locale"en" | "ar"Optional. Defaults to "en".
childrenReactNodeThe subtree that calls useCheckout, useXPay, useElements, useConfirmPayment.

The provider handles SSR gracefully: loadXPay returns null on the server, and the provider waits for the client to load the SDK before creating the Elements instance.

useCheckout()

The primary hook. Returns a disjoint union; narrow by state.type before reading session data or calling actions.

type UseCheckoutResult =
  | { type: "loading" }
  | { type: "error"; error: { message: string } }
  | { type: "success"; checkout: Checkout };

type Checkout = CheckoutSession & CheckoutActions;
const state = useCheckout();
if (state.type === "loading") return <Spinner />;
if (state.type === "error") return <p>{state.error.message}</p>;
const { checkout } = state;

After narrowing to success, checkout carries every session field plus every action method.

Session fields

The session-data fields on checkout come straight from CheckoutSession. See the @xpay/sdk-js reference for the full schema. Most-used fields:

FieldTypeDescription
idstringSession ID.
amountTotalnumberTotal in minor units.
amountSubtotalnumberSubtotal in minor units.
currencystringCurrency code.
merchantNamestringYour business name.
livemodebooleanLive vs test mode.
statusSessionStatus{type:"open"} | {type:"expired"} | {type:"complete", paymentStatus}
canConfirmbooleanWhether the session is ready to confirm.
paymentMethodsPaymentMethodInfo[]Available methods on this session.
lineItemsCheckoutLineItem[]Line items.
totalDetailsCheckoutTotalDetailsSubtotal, tax, shipping, discount, fee breakdown.
discountsCheckoutDiscount[]Applied promotion codes.

Action methods

Action methods come from CheckoutActions. All return a Promise<ActionResult> (a tagged union of success or error) except where noted.

MethodDescription
confirm(options?: Omit<ConfirmPaymentOptions, "elements">)Confirms the payment. Handles 3DS challenges and BNPL redirects internally.
applyPromotionCode(code: string)Applies a promo code; the session re-renders with the new total.
removePromotionCode()Removes the applied code.
updateLineItemQuantity({ lineItem, quantity })Updates a line item's quantity.
submit()Validates fields. Returns { error?, selectedPaymentMethod? }.
fetchUpdates()Re-fetches the session from the server.
changeAppearance(appearance: Appearance)Updates branding at runtime; returns void.
on(event, handler)Listens for "change", "error", or a custom event. Returns void.
getElements()Returns the underlying Elements instance for low-level access.

The hook listens for the underlying change and loaderror events itself, so when promo codes are applied, line items update, or fees recalculate, the component re-renders with the latest data without you wiring an effect.

Promo codes and quantities (example)

function LineItems() {
  const state = useCheckout();
  if (state.type !== "success") return null;
  const { checkout } = state;

  return (
    <ul>
      {checkout.lineItems?.map((item) => (
        <li key={item.id}>
          <span>{item.description}</span>
          <button
            onClick={() =>
              checkout.updateLineItemQuantity({
                lineItem: item.id,
                quantity: item.quantity + 1,
              })
            }
          >
            +
          </button>
          <button
            onClick={() =>
              checkout.updateLineItemQuantity({
                lineItem: item.id,
                quantity: Math.max(1, item.quantity - 1),
              })
            }
          >
            -
          </button>
        </li>
      ))}
    </ul>
  );
}

function PromoInput() {
  const state = useCheckout();
  const [code, setCode] = useState("");
  const [error, setError] = useState("");

  if (state.type !== "success") return null;
  const { checkout } = state;

  const apply = async () => {
    setError("");
    const result = await checkout.applyPromotionCode(code);
    if (result.type === "error") setError(result.error.message);
  };

  return (
    <div>
      <input value={code} onChange={(e) => setCode(e.target.value)} />
      <button onClick={apply}>Apply</button>
      <button onClick={() => checkout.removePromotionCode()}>Remove</button>
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}

Listening for unsolicited errors

change events drive React state automatically. The error event covers errors that fire outside any merchant action (e.g. session expired during a fee recalculation, BIN detection failure):

useEffect(() => {
  if (state.type !== "success") return;
  state.checkout.on("error", (err) => {
    console.error("[XPay] unsolicited error:", err.code, err.message);
  });
}, [state]);

useXPay() and useElements()

Lower-level hooks that return the underlying instances. Useful when you need to call methods that aren't exposed through useCheckout (for example, mounting an extra element programmatically).

function useXPay(): XPayInstance | null;
function useElements(): Elements | null;

Both return null while the SDK is loading or before a clientSecret has been provided. Both must be called inside <XPayProvider>.

useConfirmPayment()

Convenience hook around useCheckout. Returns the same confirm function plus an isConfirming flag. Useful when you want a tiny API for a leaf component that only needs to submit:

const { confirmPayment, isConfirming } = useConfirmPayment();

If the checkout isn't ready yet, confirmPayment() returns { type: "error", error: { message: "Checkout not ready" } } immediately rather than throwing.

<PaymentElement />

Renders the payment-method selector and card form. Must be inside <XPayProvider> with a session.

<PaymentElement
  onChange={(e) => setReady(e.complete)}
  onReady={() => console.log("element ready")}
/>
PropTypeDescription
onReady() => voidOptional. Fires when the element's iframe is initialized.
onChange(event: PaymentElementChangeEvent) => voidOptional. Fires on payment-method selection and field changes.
onLoaderStart() => voidOptional. Fires synchronously when the iframe is created.
onLoadError(event: ElementsLoadErrorEvent) => voidOptional. Fires when the element fails to load.
classNamestringOptional. CSS class for the container <div>.
idstringOptional. ID for the container <div>.

The component renders a single <div> and mounts the underlying PaymentElement (from @xpay/sdk-js) into it. On unmount, the element is torn down automatically.

<CheckoutButton />

Pre-built button that opens the drop-in checkout modal on click. Wraps xpay.checkout({ mode: "modal" }) so you don't have to wire it yourself.

<CheckoutButton
  clientSecret="cs_test_..."
  checkoutOptions={{
    onComplete: (result) => router.push(`/orders/${result.paymentIntentId}`),
    onClose: () => console.log("Customer closed checkout"),
  }}
>
  Pay Now
</CheckoutButton>
PropTypeDescription
clientSecretstringRequired. The Checkout Session client secret.
childrenReactNodeButton label. Defaults to "Pay".
checkoutOptionsOmit<CheckoutOptions, "clientSecret" | "mode">Optional. Callbacks (onComplete, onClose, onReady, onConfirmed, onError), appearance, locale.
classNamestringOptional. CSS class for the button.
disabledbooleanOptional. Disables the button.

The button is also disabled automatically when the SDK isn't loaded yet (useXPay() returns null). Must be inside <XPayProvider>.

Common patterns

Theme-aware appearance

Sync XPay's color mode with your app's theme using changeAppearance at runtime:

function ThemeAwareCheckout() {
  const state = useCheckout();
  const { theme } = useTheme();

  useEffect(() => {
    if (state.type === "success") {
      state.checkout.changeAppearance({ colorMode: theme as "light" | "dark" });
    }
  }, [theme, state]);

  return <PaymentElement />;
}

Pre-validating before confirming

submit() validates all fields before confirm(). Useful when you want to gate the confirm step behind another UI step (a confirmation dialog, a Terms checkbox, etc.):

const handlePay = async () => {
  const { error, selectedPaymentMethod } = await checkout.submit();
  if (error) return setError(error.message);

  const ok = await showConfirmDialog(selectedPaymentMethod);
  if (!ok) return;

  const result = await checkout.confirm();
  if (result.type === "error") setError(result.error.message);
};

Redirect after success

By default confirm() returns the result to your code. Pass redirect: "always" to send the customer to your afterCompletion.redirect.url after success:

await checkout.confirm({
  customerDetails: { email },
  redirect: "always",
});
// On success, the page navigates away. Any code below only runs on error.

You can override the server-side returnUrl from the client by passing it explicitly: redirect: "always", returnUrl: "https://example.com/custom".

Where to next

On this page