ShiftxPay
Accept payments

Checkout SDK

Mount the embedded checkout with @shiftx-mu/lite-checkout.

@shiftx-mu/lite-checkout mounts the payment surface directly in the buyer's browser. The card is entered only inside the PSP's own iframe (the Peach embedded widget) — the SDK and the gateway never see a PAN. The SDK resolves the checkout session, mounts the right surface, and on submit confirms with the PSP and then finalizes the payment server-side.

Before you start

The SDK consumes a checkout session that your server must create first.

  1. Your backend calls POST /v1/checkout-sessions with your secret API key and receives { id, session_secret, ... }.
  2. Your page receives that id (cs_...) and session_secret (cs_secret_...) and passes them to mountCheckout.

The secret API key never reaches the browser. The session_secret is the bearer of authority for a single session and nothing else. See Quickstart for the server-side step.

Install

pnpm add @shiftx-mu/lite-checkout
import { mountCheckout } from "@shiftx-mu/lite-checkout";

Drop-in script (no bundler)

The IIFE bundle exposes a global ShiftxPay. Always pin a Subresource Integrity hash for the CDN-hosted bundle so a tampered file cannot execute; take the hash from the published release (for example openssl dgst -sha384 -binary lite-checkout.global.js | openssl base64 -A).

<div id="payment-element"></div>
<button id="pay">Pay</button>
<script
  src="https://cdn.lite.shiftxpay.com/lite-checkout.global.js"
  integrity="sha384-REPLACE_WITH_PUBLISHED_HASH"
  crossorigin="anonymous"
></script>
<script>
  ShiftxPay.mountCheckout({
    baseUrl: "https://api.lite.shiftxpay.com",
    sessionId: "cs_...",
    sessionSecret: "cs_secret_...",
    container: "#payment-element",
    onSuccess: (r) => alert("Paid: " + r.paymentId),
  }).then((c) => {
    document.getElementById("pay").onclick = () => c.submit();
  });
</script>

Full example

import { mountCheckout, CheckoutError } from "@shiftx-mu/lite-checkout";

const controller = await mountCheckout({
  baseUrl: "https://api.lite.shiftxpay.com",
  sessionId: "cs_...",          // from POST /v1/checkout-sessions
  sessionSecret: "cs_secret_...", // from POST /v1/checkout-sessions
  container: "#payment-element",
  returnUrl: "https://shop.example.com/checkout/return",
  fetchTimeoutMs: 30000,
  onReady: () => {
    // The PSP surface has mounted; enable the Pay button.
    document.querySelector<HTMLButtonElement>("#pay")!.disabled = false;
  },
  onSuccess: ({ paymentId, status }) => {
    window.location.href = `/checkout/done?payment=${paymentId}`;
  },
  onProcessing: ({ status }) => {
    // status is "processing" or "requires_action" — keep the buyer waiting and
    // rely on the outbound webhook to confirm the final state.
    showSpinner(`Payment ${status}…`);
  },
  onError: (err) => {
    if (err.kind === "client") {
      showMessage("Please check your card details and try again.");
    } else {
      showRetry("Something went wrong. Please try again.");
    }
  },
});

// Wire your own Pay button to the controller. With the Peach embedded widget
// the widget owns its own pay button and submit() is a no-op, so a single
// shared button works across every surface.
document.querySelector("#pay")!.addEventListener("click", () => {
  void controller.submit();
});

// Tear down when the view unmounts (SPA route change, modal close, etc.).
function onUnmount() {
  controller.destroy();
}

Configuration

mountCheckout(config) returns Promise<CheckoutController>.

FieldTypeRequiredDescription
baseUrlstringyesGateway origin, no trailing slash. https://api.lite.shiftxpay.com.
sessionIdstringyesCheckout session public id (cs_...) from POST /v1/checkout-sessions.
sessionSecretstringyesSession secret (cs_secret_...) that authorizes this one session.
containerstring | HTMLElementyesA CSS selector or element to mount the PSP surface into.
returnUrlstringnoWhere the buyer returns after a redirect-based method or a 3-D Secure challenge. See 3-D Secure & redirects.
fetchTimeoutMsnumbernoPer-request network timeout for each gateway call. Default 30000.
onReady() => voidnoFired once the PSP surface has mounted.
onSuccess(r: CheckoutSuccess) => voidnoFired when the payment is completed.
onProcessing(r: CheckoutPending) => voidnoFired when the payment is processing or requires_action.
onError(err: CheckoutError) => voidnoFired on any failure. The argument is always a CheckoutError.

The active surface is not a client choice — it is driven by the session's render hints, which come from the merchant's connector configuration. See Checkout surfaces.

CheckoutController

MemberTypeDescription
submit()() => Promise<void>Confirms with the PSP, then finalizes server-side. A no-op for the Peach embedded widget, which owns its own pay button.
destroy()() => voidTears down the mounted PSP surface. Call on unmount.

CheckoutSuccess / CheckoutPending

TypeFieldDescription
CheckoutSuccesspaymentId: stringThe payment public id (pay_...).
CheckoutSuccessstatus: stringThe payment status, typically succeeded.
CheckoutPendingstatus: "processing" | "requires_action"The interim payment state.

Error handling

Every failure is reported as a CheckoutError. Branch on err.kind to decide what to tell the buyer.

kindMeaningWhat to do
clientDeterministic — bad input, declined card, cancelled, expired session.Ask the buyer to check their details; retrying the same input will not help.
serverGateway-side fault (5xx).Transient — offer a retry.
networkConnectivity failure reaching the gateway or a PSP script.Transient — offer a retry.
timeoutThe gateway did not respond within fetchTimeoutMs.Transient — offer a retry.

CheckoutError also carries .status: the HTTP status code when the error came from an HTTP response, and undefined for network and timeout. The SDK retries the idempotent confirm call automatically for server, network, and timeout failures; client (4xx) failures are never retried because the gateway will return the same result.

onError: (err) => {
  switch (err.kind) {
    case "client":
      showMessage(err.message); // safe to surface — e.g. "Your card was declined."
      break;
    case "server":
    case "network":
    case "timeout":
      showRetry("We couldn't reach the gateway. Please try again.");
      break;
  }
};

CheckoutError extends Error, so existing handlers typed as (error: Error) => void remain compatible.

On this page