Payin SDK

Render the Payment Labs payment element on your own page to take payments, save cards for later, and sign buyers up to subscriptions.

📘

Upgrading from a 0.x release? Version 1.x renamed the Widget API to Element. See Upgrading from 0.x to 1.x.

@paymentlabs/paymentlabs-js is a TypeScript SDK that renders the Payment Labs payment element inside your own page. You build the page around it; the element collects and tokenizes the buyer's card, bank or wallet details and submits the charge, so raw card data never reaches your servers.

This guide takes you to a working payment first, then covers saving a card without charging it, subscriptions, billing addresses, the fee, theming, events and errors. It ends with a reference of the SDK's methods and types.

Before you start

  1. Your merchant ID, for example C-01JBX2Q8F4H6K8M0N2P4R6T8VW. It is the merchantId on every checkout you create, and Get Merchant returns it.
  2. A token on your server. Merchants use the same login as payors, Create Access Token; see Environments and Authentication. The token never goes to the browser.
  3. A checkout or plan offer, created from your backend. Creating it on the server keeps the amount out of the buyer's reach.

How it works

A payment involves your app, the SDK element and the Payment Labs API:

   YOUR APP                  PAYMENT LABS SDK              PAYMENT LABS
┌──────────────┐          ┌────────────────────┐       ┌────────────────┐
│ Product page │          │  Payment element   │       │  Checkout API  │
│ Cart & total │ ───────▶ │  Secure card fields│ ────▶ │  Tokenization  │
│ "Pay" button │          │  Events & results  │       │  Processing    │
└──────────────┘          └────────────────────┘       └────────────────┘
   You design                 The SDK collects             We move the money
   the experience             & tokenizes safely           and tell you what
                                                            happened
  1. Your backend creates a checkout (the intent to collect money or save a card) and gets back a checkout ID.
  2. Your frontend hands that ID to the SDK, which renders the payment element inside a container you choose.
  3. The buyer enters their details. The SDK tokenizes them securely (your servers never touch raw card data) and submits the transaction.
  4. The SDK tells you what happened through events — checkout-completed, checkout-failed, and so on — so you can react in your UI.

What the SDK does — and doesn't do

The SDK collects and processes the payment. The rest of the checkout page is yours.

The SDK handlesYou handle
Secure card / bank / wallet input fieldsProduct pages, cart, and order summary
PCI-compliant tokenization & submissionPricing, tax, and shipping logic
Payment, save-card, and subscription flowsOrder creation and fulfillment
Real-time events and result screensThe "Pay" button and surrounding UI
Theming to match your brandCreating the checkout on your backend
📘

The SDK never collects money on its own. It always works against a checkout that you created first through the Payment Labs API. That checkout decides the amount, the currency, and which payment methods appear.

Install

Add the SDK with your package manager of choice:

npm install @paymentlabs/paymentlabs-js
# or
yarn add @paymentlabs/paymentlabs-js
# or
pnpm add @paymentlabs/paymentlabs-js

Prefer a script tag? Load it from our CDN — PaymentLabs becomes available on window:

<script src="https://sdk.paymentlabs.io/payin/latest/paymentlabs.js"></script>
📘

The CDN URL serves the newest 1.x release, so it picks up fixes and features automatically, and never a breaking change. To hold one exact version, install from npm instead. See the SDK Versioning and Support Policy.

Quickstart: your first payment

The flow has three parts.

Step 1: Create a checkout (backend)

From your server, call Create Checkout with the amount, currency, and buyer. Send the returned checkout ID to your page.

Step 2: Render the element (frontend)

Initialize the SDK, then mount an element into a container on your page.

import { PaymentLabs } from "@paymentlabs/paymentlabs-js";

// 1. Create the SDK once, with your environment and merchant ID.
const sdk = PaymentLabs.create({
  environment: "sandbox", // switch to "production" when you go live
  merchantId: "C-01JBX2Q8F4H6K8M0N2P4R6T8VW",
});

// 2. Mount the payment element into a container on your page.
const element = await sdk.createElement("checkout", checkoutId, {
  containerId: "checkout-container", // <div id="checkout-container"></div>
});
<div id="checkout-container"></div>
<button id="pay-button" disabled>Pay</button>

Step 3: React to the result

The element tells you when it's ready, when it's working, and how it ended. Wire up a button to submit, and listen for the outcome.

const payButton = document.getElementById("pay-button");

// The form is rendered and interactive.
element.on("checkout-loaded", () => {
  payButton.disabled = false;
});

// Reflect progress so the buyer knows something's happening.
element.on("payment-processing", (isProcessing) => {
  payButton.disabled = isProcessing;
  payButton.textContent = isProcessing ? "Processing…" : "Pay";
});

// The payment succeeded. Confirm it on your server before fulfilling the order.
element.on("checkout-completed", () => {
  showSuccessScreen();
});

// Something went wrong — tell the buyer what to do next.
element.on("checkout-failed", ({ errorReason }) => {
  showError(errorReason);
});

// Hand control to the SDK when the buyer clicks Pay.
payButton.addEventListener("click", () => element.submitPayment());

The SDK rendered the form, collected and tokenized the card, ran the transaction, and reported the outcome. Before you fulfil the order, confirm the payment on your server: wait for the checkout-status-changed webhook or call Get Checkout and check that the status is PAID.

Saving a card without charging it

Sometimes you don't want to charge the buyer now, only let them add a payment method to their account, so they can choose it instead of typing it again on later checkouts.

To do this, create the checkout with an amount of 0. When the amount is zero, the checkout element becomes a save-card form:

  • The buyer enters their card exactly as usual.
  • On submit, the card is securely tokenized and saved — no money moves.
  • The SDK adapts its language automatically: progress reads "Your request is being processed" and success reads "Your request has been successfully completed" instead of talking about a payment.
  • The optional "save this card for later" checkbox is hidden, because saving the card is the purpose of the checkout.
  • The buyer must sign in or create an account, and only cards and bank accounts are offered.

How to do it

There is no special frontend flag. Your code is identical to a normal checkout — the only difference is that your backend created the checkout with amount: 0.

// Backend created this checkout with an amount of 0.
const element = await sdk.createElement("checkout", savecardCheckoutId, {
  containerId: "checkout-container",
});

const saveButton = document.getElementById("save-card-button");

element.on("checkout-loaded", () => {
  saveButton.disabled = false;
});

// Fired the same way — the card is now saved and reusable.
element.on("checkout-completed", () => {
  showCardSavedScreen();
});

saveButton.addEventListener("click", () => element.submitPayment());
<div id="checkout-container"></div>
<!-- Label the action for what it really is -->
<button id="save-card-button" disabled>Save card</button>

Label your button "Save card" or "Add payment method" rather than "Pay", to match the SDK's own wording.

To charge the saved card later from your server, follow Saved Cards and Merchant-Initiated Charges.

Subscriptions

Recurring billing follows the same shape as a one-time payment, with one difference: instead of a checkout ID, you pass a plan offer ID, and you create the element with the "subscription" type.

const element = await sdk.createElement("subscription", planOfferId, {
  containerId: "subscription-container",
});

element.on("subscription-loaded", () => {
  const offer = element.getSubscriptionOfferData();
  console.log("Plans available:", offer?.subscriptionPlans);
});

// The buyer picked a different plan from the offer.
element.on("subscription-plan-changed", (plan) => {
  updatePlanSummary(plan); // plan.name, plan.amount.formattedValue, plan.schedule…
});

element.on("subscription-completed", () => {
  showSubscriptionActiveScreen();
});

element.on("subscription-failed", ({ errorReason }) => {
  showError(errorReason);
});

// The chosen plan's checkout is on screen: only now can the buyer pay.
element.on("checkout-loaded", () => {
  const submitButton = document.getElementById("subscribe-button");
  submitButton.hidden = false;
  submitButton.addEventListener("click", () => element.submitPayment());
});

A plan offer can contain several plans (e.g. Monthly and Annual). The element renders them for the buyer to choose from, and fires subscription-plan-changed whenever the selection changes so you can keep your own summary in sync. When the buyer selects Continue to payment, the element shows that plan's checkout and fires checkout-loaded. An offer with one plan goes straight to its checkout.

The buyer must sign in to subscribe. When the subscription is created, subscription-completed fires; a plan with a free trial completes the same way, without a charge. Follow the subscription on your server with the subscription-status-changed webhook. Subscriptions covers plans, offers, billing and cancellation.

Collecting the billing address

By default, the SDK renders the billing address fields for you inside the element — you don't have to do anything. But if you already collect the address elsewhere (say, a shipping form), you can turn the SDK's fields off and pass the address in at submit time.

Option A — Let the SDK collect it (default)

const element = await sdk.createElement("checkout", checkoutId, {
  containerId: "checkout-container",
});

await element.submitPayment(); // address comes from the SDK-rendered fields

The form starts compact: the buyer sees only Address. The other fields (Address 2, Country, State, City, Postal Code) show once the buyer picks a suggestion or enters the address manually.

To show every address field from the start, set the optional billingDetails.addressLayout to "full" (the default is "compact"):

const element = await sdk.createElement("checkout", checkoutId, {
  containerId: "checkout-container",
  billingDetails: {
    addressLayout: "full", // every address field from the start
  },
});

Option B — Provide it yourself

Set billingDetails.fields.address to false, then include the address in submitPayment():

const element = await sdk.createElement("checkout", checkoutId, {
  containerId: "checkout-container",
  billingDetails: {
    fields: { address: false }, // SDK won't render address fields
  },
});

await element.submitPayment({
  billingDetails: {
    address: {
      address1: "123 Main Street",
      address2: "Apt 4B", // optional
      city: "New York",
      state: "NY",
      postalCode: "10001",
      country: "US", // ISO 3166-1 alpha-2
    },
  },
});
⚠️

If you turn the fields off, you must pass the address. When address is false and you call submitPayment() without one, the call fails with "Address information is required but not provided".

fields.addressWho renders the addressWhere the data comes from
true (default)The SDKThe element's own fields
falseYouThe submitPayment() argument

Showing the fee

When a checkout carries a fee, the element shows the buyer the subtotal, the fee and the total above the payment methods. Each payment method can carry its own fee, so the figures follow the method the buyer selects. On a subscription they show in the plan card: what the buyer pays today, then each cycle. With no fee there is no breakdown: a one-time checkout shows no summary, a subscription shows only its plan card, and payableAmount equals amount.

To show the fee in your own layout instead, for example in an order summary beside the element, set hideFeeSummary and listen to payment-method-changed. It fires when the form shows and each time the buyer selects or clears a payment method. It can fire before checkout-loaded, so do not reset your summary there. On a subscription the plan card stays, without the fee lines.

const element = await sdk.createElement("checkout", checkoutId, {
  containerId: "checkout-container",
  hideFeeSummary: true,
});

element.on("payment-method-changed", ({ amount, payableAmount }) => {
  // No total yet: the methods cost different amounts and the buyer has not picked one.
  showOrderSummary({ subtotal: amount, total: payableAmount });
});
FieldWhat it is
paymentMethodpayment-card, bank-transfer or third-party-token. Set when a method is selected, or when the form offers only one.
amountThe subtotal: what the checkout asks for, before any fee.
payableAmountThe total with paymentMethod. Without a method, present only when every method pays the same.
recurringA subscription's cycle: its amount and payableAmount, figured the same way.

The fee is payableAmount minus amount. Apple Pay and Google Pay show their own total in the wallet sheet.

Theming

Match the element to your brand with a small set of seed colors. The SDK derives a full, accessible palette from them.

const element = await sdk.createElement("checkout", checkoutId, {
  containerId: "checkout-container",
  theme: {
    mode: "light", // "light" (default) or "dark"
    colors: {
      seed: {
        brand: "#5b3df5",    // your primary color — buttons, accents
        neutral: "#1f2937",  // text and neutral UI
        info: "#2563eb",
        positive: "#16a34a", // success
        negative: "#dc2626", // errors
        warning: "#d97706",
      },
      // Optional fine-grained overrides
      overrides: {
        "bg-main": "#ffffff",
      },
    },
  },
});

You can switch between light and dark at runtime from the SDK instance:

await sdk.updateTheme("dark");
⚠️

Switching the theme re-renders the secure fields. The card fields (cardholder name, number, expiry, CVC) live in isolated iframes that are rebuilt when the theme changes, so anything the buyer already typed into them is cleared. Switch the theme before the buyer starts entering card details.

Events

Events are how the element talks back to you. Register a handler with element.on(event, callback).

Checkout events

EventFires whenPayload
checkout-loadedThe form is rendered and readyThe checkout details
payment-method-changedThe form shows, and each time the buyer selects or clears a payment methodPaymentMethodChangedPayload — see Showing the fee
payment-processingSubmission starts/stopsboolean (true = busy)
checkout-processingThe charge is underway—
checkout-completedPayment (or card save) succeeded—
checkout-failedPayment failed, or could not be confirmed{ errorReason?, extendedErrorReason?, unresolved? }
checkout-expiredThe checkout's time window passed—
sdk-errorAnything other than a declined payment went wrongSdkErrorPayload — see SDK errors
payment-failedDeprecated, never fires. Use sdk-error.—

If the checkout has redirectLinks, the element follows them. After checkout-completed, checkout-failed or checkout-expired, it sends the browser to the matching URL. Leave redirectLinks out of checkouts you render with the SDK unless you want that navigation.

unresolved: true is not a decline. The payment has not been confirmed as succeeded or failed, and the checkout is UNKNOWN. Do not ask the buyer to pay again and do not cancel the order; check the checkout on your server.

Subscription events

EventFires whenPayload
subscription-loadedThe offer is rendered and ready—
subscription-plan-changedThe buyer selects a different planSubscriptionPlan
checkout-loadedThe chosen plan's checkout is on screen. submitPayment() can only succeed after it, so keep your pay button hidden until it firesThe checkout details
payment-method-changedThe plan's checkout shows, and each time the buyer selects or clears a payment methodPaymentMethodChangedPayload — see Showing the fee
subscription-processingThe subscription is being set up—
payment-processingSubmission starts/stopsboolean
subscription-completedThe subscription was created—
subscription-failedSubscription setup failed{ errorReason?, extendedErrorReason? }
subscription-expiredThe offer's time window passed—
sdk-errorAnything other than a declined payment went wrongSdkErrorPayload
// Register many at once
["checkout-loaded", "checkout-completed", "checkout-failed"].forEach((event) =>
  element.on(event, (data) => analytics.track(event, data)),
);

Handle payment failures

When a payment fails, you get an errorReason (the category) and sometimes an extendedErrorReason (the specific cause). Use the category to decide what to tell the buyer, and the extended reason to be more specific when it is present. Both are described in Checkout Statuses and Failure Reasons.

element.on("checkout-failed", ({ errorReason, extendedErrorReason, unresolved }) => {
  if (unresolved) {
    show("We are confirming your payment. Please don't pay again.");
    return;
  }
  switch (errorReason) {
    case "MISSING_INSTRUMENT":
      show("Please choose a payment method to continue.");
      break;
    case "INVALID_INSTRUMENT":
      show("Those payment details look off. Please double-check them.");
      break;
    case "CHARGE_INSTRUMENT_ERROR":
      show("Your card was declined. Try another card or contact your bank.");
      break;
    case "APPLICATION_ERROR":
      show("Something went wrong on our side. Please try again shortly.");
      break;
    default:
      show("Payment couldn't be completed. Please try again.");
  }

  if (extendedErrorReason) console.debug("Decline detail:", extendedErrorReason);
});

Every errorReason and extendedErrorReason value, with its meaning, is listed in Checkout Statuses and Failure Reasons. New extendedErrorReason values can be added in minor releases, so fall back to errorReason for values you do not recognise.

The element can also report UNEXPECTED_ERROR when it stopped waiting for the result before the checkout reached a final status. Treat it like unresolved: check the checkout on your server.

SDK errors (sdk-error)

checkout-failed tells you a payment was declined. Everything else that can go wrong is reported on one event, sdk-error: the payment form failing to load, the API rejecting the charge, the session expiring, a wallet being unavailable, a 3D Secure step failing, or the payment status never being confirmed.

element.on("sdk-error", (error) => {
  // error.code      stable code, e.g. "CHARGE_REJECTED" — switch on this
  // error.category  "load" | "authentication" | "validation" | "payment" | "three_d_secure"
  //                 | "processing" | "wallet" | "address_autocomplete" | "internal"
  // error.severity  "fatal" | "recoverable" | "degraded"
  // error.message   fixed English sentence for the code — for your logs, not for buyers
  analytics.track("payment_sdk_error", error);

  if (error.code === "PAYMENT_STATUS_UNKNOWN") {
    // The payment may still go through. Don't cancel the order — check its status later.
  }
});

severity tells you how much it matters:

SeverityMeaning
fatalThe flow stopped. The buyer can't continue without reloading the page.
recoverableThe buyer can fix something and try again.
degradedA feature is unavailable (e.g. address suggestions), but paying still works.

The payload never contains sensitive data — no card or bank details, names, emails, addresses, tokens, or server responses. Besides code, category, severity, message and timestamp, it can only carry:

FieldWhen
paymentMethod"payment-card", "bank-transfer", "saved-instrument", "apple-pay" or "google-pay"
httpStatusAn API call failed with this status
reasonThe 3D Secure step that failed (e.g. "authentication-denied")
fieldsIds of the invalid fields — never their values
checkoutId, transactionIdIds to quote when contacting support

submitPayment() also resolves with the same code on failure, if you prefer await to events:

const result = await element.submitPayment();
if (!result.success) {
  console.log(result.code); // e.g. "CHARGE_VALIDATION"
}

Error codes

CodeSeverityWhen
PAYMENT_FORM_LOAD_FAILEDfatalThe card form failed to load
CHECKOUT_RELOAD_FAILEDfatalThe checkout couldn't be loaded for a signed-in buyer
SUBSCRIPTION_OFFER_LOAD_FAILEDfatalThe subscription offer couldn't be loaded
SAVED_METHODS_LOAD_FAILEDdegradedThe buyer's saved payment methods couldn't be loaded
SESSION_EXPIREDfatalThe session expired (HTTP 401) — the page must be reloaded
SESSION_FORBIDDENfatalThe session was refused (HTTP 403)
VERIFICATION_CODE_REQUEST_FAILEDrecoverableThe sign-in code or password-reset email couldn't be sent
NO_PAYMENT_METHOD_SELECTEDrecoverablePayment was submitted with no method selected
FIELD_VALIDATIONrecoverableFields are invalid — fields lists them
CHARGE_VALIDATIONrecoverableThe API rejected fields (HTTP 400) — fields lists them
BILLING_ADDRESS_REQUIREDfatalAddress fields are disabled and submitPayment() got no address
CHARGE_REJECTEDfatalThe charge request was rejected
CHECKOUT_LOCKEDfatalThe checkout is already being paid (HTTP 423)
CHARGE_NO_RESPONSEfatalThe charge request returned nothing
THREE_DS_FAILEDrecoverableA 3D Secure step failed — the outcome still follows on checkout-failed or checkout-completed
THREE_DS_TIMEOUTrecoverableThe 3D Secure challenge timed out
PAYMENT_STATUS_UNKNOWNfatalThe payment status couldn't be confirmed — it may still complete
WALLET_UNAVAILABLEdegradedApple Pay / Google Pay couldn't be offered
WALLET_MERCHANT_VALIDATION_FAILEDrecoverableApple Pay merchant validation failed
WALLET_PAYMENT_FAILEDrecoverableThe wallet payment couldn't be completed
WALLET_CANCELLEDrecoverableThe buyer closed the wallet sheet
ADDRESS_AUTOCOMPLETE_UNAVAILABLEdegradedAddress suggestions are off — the buyer can still type the address
RETRY_FAILEDfatalThe form couldn't be rebuilt for another attempt
UNEXPECTED_ERRORfatalAnything else

Good to know

  • Codes are stable. New codes may be added; existing ones are never renamed or removed. Always keep a default branch for codes you don't recognise.
  • No duplicates. The same error is reported once, and an unavailable feature (a wallet, address suggestions) once per element.
  • Your listener can't break the payment. If an sdk-error listener throws, the error is ignored and the payment flow carries on.
  • Older SDK versions. SDK versions released before sdk-error existed throw Invalid event type from element.on("sdk-error", …). If you support them, wrap the call in try.
  • payment-failed is deprecated. Use sdk-error instead.

Framework guides

The SDK is framework-agnostic. The pattern is always the same: create the SDK, create the element into a container, listen for events, call submitPayment(). Here it is in the most common setups.

React

import { useEffect, useRef, useState } from "react";
import { PaymentLabs, PaymentElement } from "@paymentlabs/paymentlabs-js";

export function Checkout({
  checkoutId,
  onSuccess,
  onFailure,
}: {
  checkoutId: string;
  onSuccess: () => void;
  onFailure: (reason: string) => void;
}) {
  const elementRef = useRef<PaymentElement | null>(null);
  const [ready, setReady] = useState(false);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let element: PaymentElement | undefined;

    (async () => {
      const sdk = PaymentLabs.create({
        environment: "sandbox",
        merchantId: "C-01JBX2Q8F4H6K8M0N2P4R6T8VW",
      });

      element = await sdk.createElement("checkout", checkoutId, {
        containerId: "checkout-container",
      });
      elementRef.current = element;

      element.on("checkout-loaded", () => setReady(true));
      element.on("payment-processing", setLoading);
      element.on("checkout-completed", onSuccess);
      element.on("checkout-failed", ({ errorReason }) => onFailure(errorReason));
    })();

    return () => element?.destroy();
  }, [checkoutId, onSuccess, onFailure]);

  return (
    <div>
      <div id="checkout-container" />
      {ready && (
        <button onClick={() => elementRef.current?.submitPayment()} disabled={loading}>
          {loading ? "Processing…" : "Pay"}
        </button>
      )}
    </div>
  );
}

Vue

<template>
  <div>
    <div id="checkout-container"></div>
    <button v-if="ready" @click="submit" :disabled="loading">
      {{ loading ? "Processing…" : "Pay" }}
    </button>
  </div>
</template>

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { PaymentLabs, PaymentElement } from "@paymentlabs/paymentlabs-js";

const props = defineProps<{ checkoutId: string }>();
const emit = defineEmits<{ success: []; failure: [reason: string] }>();

const element = ref<PaymentElement | null>(null);
const ready = ref(false);
const loading = ref(false);

onMounted(async () => {
  const sdk = PaymentLabs.create({
    environment: "sandbox",
    merchantId: "C-01JBX2Q8F4H6K8M0N2P4R6T8VW",
  });

  element.value = await sdk.createElement("checkout", props.checkoutId, {
    containerId: "checkout-container",
  });

  element.value.on("checkout-loaded", () => (ready.value = true));
  element.value.on("payment-processing", (p) => (loading.value = p));
  element.value.on("checkout-completed", () => emit("success"));
  element.value.on("checkout-failed", ({ errorReason }) => emit("failure", errorReason));
});

onUnmounted(() => element.value?.destroy());

const submit = () => element.value?.submitPayment();
</script>

Vanilla JavaScript (module import)

<div id="checkout-container"></div>
<button id="pay-button" disabled>Pay</button>

<script type="module">
  import { PaymentLabs } from "@paymentlabs/paymentlabs-js";

  const sdk = PaymentLabs.create({
    environment: "sandbox",
    merchantId: "C-01JBX2Q8F4H6K8M0N2P4R6T8VW",
  });

  const element = await sdk.createElement("checkout", "your-checkout-id", {
    containerId: "checkout-container",
  });

  const payButton = document.getElementById("pay-button");
  element.on("checkout-loaded", () => (payButton.disabled = false));
  element.on("checkout-completed", () => alert("Payment complete"));
  element.on("checkout-failed", ({ errorReason }) => alert(`Failed: ${errorReason}`));
  payButton.addEventListener("click", () => element.submitPayment());
</script>

CDN (script tag)

<!DOCTYPE html>
<html>
  <body>
    <div id="checkout-container"></div>
    <button id="pay-button" disabled>Pay</button>

    <script src="https://sdk.paymentlabs.io/payin/latest/paymentlabs.js"></script>
    <script>
      (async () => {
        // PaymentLabs is available globally from the CDN.
        const sdk = PaymentLabs.create({
          environment: "production",
          merchantId: "C-01JBX2Q8F4H6K8M0N2P4R6T8VW",
        });

        const element = await sdk.createElement("checkout", "your-checkout-id", {
          containerId: "checkout-container",
        });

        const payButton = document.getElementById("pay-button");
        element.on("checkout-loaded", () => (payButton.disabled = false));
        element.on("payment-processing", (busy) => {
          payButton.disabled = busy;
          payButton.textContent = busy ? "Processing…" : "Pay";
        });
        element.on("checkout-completed", () => alert("Payment complete"));
        element.on("checkout-failed", (e) => alert(`Failed: ${e.errorReason}`));

        payButton.addEventListener("click", () => element.submitPayment());
      })();
    </script>
  </body>
</html>

API reference

PaymentLabs.create(config)

Creates an SDK instance. Call it once and reuse it.

const sdk = PaymentLabs.create({
  environment: "sandbox", // "sandbox" | "production"
  merchantId: "C-01JBX2Q8F4H6K8M0N2P4R6T8VW",
});
FieldTypeDescription
environment"sandbox" | "production"Which Payment Labs environment to talk to
merchantIdstringYour merchant identifier

Returns a PaymentLabs instance.

sdk.createElement(type, id, options)

Creates and mounts a payment element. Returns Promise<PaymentElement>.

ParameterTypeDescription
type"checkout" | "subscription"The kind of element to render
idstringA checkout ID (for "checkout") or plan offer ID (for "subscription")
optionsElementOptionsSee below
interface ElementOptions {
  containerId: string;            // ID of the DOM element to render into (required)
  theme?: ThemeConfig;            // Brand colors and light/dark mode
  billingDetails?: BillingDetailsConfig; // Address collection behavior
  hideFeeSummary?: boolean;       // Hide the subtotal, fee and total; default false (see Showing the fee)
}

sdk.updateTheme(mode)

Switches the element between "light" and "dark". It re-renders the secure card fields (see Theming).

element.submitPayment(request?)

Submits the payment (or saves the card, for a zero-amount checkout). Returns a result object.

const result = await element.submitPayment();
// { success: boolean; instrumentId?: string; transactionId?: string; error?: string; code?: SdkErrorCode }

On failure, code is the same code the sdk-error event carries.

Pass request.billingDetails.address only when you've disabled the SDK's address fields — see Collecting the billing address.

element.on(event, callback)

Registers an event listener. See Events for the full list.

element.getCheckoutData()

Returns the current Checkout (or null). Most useful inside checkout-loaded.

element.getSubscriptionOfferData()

Returns the current SubscriptionOffer (or null). Most useful inside subscription-loaded.

element.destroy()

Tears down the element and cleans up listeners. Call this when your component unmounts to avoid leaks.

Type reference

The SDK ships full TypeScript definitions. The types you'll touch most often:

import {
  PaymentLabs,
  PaymentElement,
  Checkout,
  SubscriptionOffer,
  SubscriptionPlan,
  CheckoutErrorReason,
  ThemeConfig,
  SdkErrorPayload,
  SdkErrorCode,
} from "@paymentlabs/paymentlabs-js";

Checkout

Returned by getCheckoutData().

interface Checkout {
  id: string;
  buyerEmail: string;
  buyerId: string;
  merchantId: string;
  merchantName: string;
  alias: string;                 // your reference / description
  amount: { value: number; currency: string }; // value: 0 ⇒ save-card checkout
  payableAmount?: { value: number; currency: string }; // see Showing the fee
  payableAmounts?: Partial<Record<InstrumentType, { value: number; currency: string }>>;
  recurring?: {                  // a subscription's cycle, priced like the fields above
    payableAmount?: { value: number; currency: string };
    payableAmounts?: Partial<Record<InstrumentType, { value: number; currency: string }>>;
  };
  status: CheckoutStatus;
  currentTransactionStatus?: CheckoutTransactionStatus;
  errorTransaction?: {
    transactionId: string;
    errorReason: CheckoutErrorReason;
    extendedErrorReason?: CheckoutExtendedErrorReason;
    checkoutInstrumentId: string;
    instrumentType: InstrumentType;
  };
  enabledCheckoutInstruments: InstrumentType[];
  requiresFraudSession: boolean;
  expiresAt: string;             // ISO 8601
  links: { embeddable: string; checkout: string };
  redirectLinks?: {
    successRedirectUrl?: string;
    failureRedirectUrl?: string;
    expiredRedirectUrl?: string;
  };
  termsOfServiceUrl: string;
  privacyPolicyUrl: string;
}

type CheckoutStatus =
  | "PENDING"     // awaiting payment
  | "PROCESSING"  // payment in progress
  | "PAID"        // succeeded
  | "FAILED"      // failed
  | "EXPIRED"     // time window passed
  | "UNKNOWN"     // the charge was never confirmed; do not charge again
  | "CREATING" | "ACTIVE" | "TRIAL"; // subscription statuses; a checkout never has them

SubscriptionOffer & SubscriptionPlan

interface SubscriptionOffer {
  id: string;
  merchantId: string;
  userId: string;
  subscriptionPlanIds: string[];
  subscriptionPlans: SubscriptionPlan[];
  expiresAt: string;
  links: { embeddable: string; self: string };
}

interface SubscriptionPlan {
  id: string;
  merchantId: string;
  merchantDisplayName: string;
  sort: string;                  // ordering/grouping hint, e.g. "RegularPlan#100"
  name: string;
  description?: string;
  enabledCheckoutInstruments: InstrumentType[];
  schedule: Schedule;
  amount: Money;
  trialPeriod?: Schedule;
  status: "ACTIVE" | "CANCELED";
  termsOfServiceUrl?: string;
  privacyPolicyUrl?: string;
}

interface Schedule {
  interval: "daily" | "weekly" | "monthly" | "yearly";
  intervalCount: number;         // e.g. interval "monthly" + count 2 = every 2 months
}

interface Money {
  value: number;                 // amount as a decimal value
  currency: string;              // ISO 4217
  formattedValue: string;        // e.g. "$9.99"
  digits: number;                // decimal digits for the currency
  wholeValue: number;            // value with no decimals (usually cents)
}

SdkErrorPayload

Passed to sdk-error listeners. See SDK errors for the codes.

interface SdkErrorPayload {
  code: SdkErrorCode;            // e.g. "CHARGE_REJECTED"
  category: "load" | "authentication" | "validation" | "payment" | "three_d_secure"
    | "processing" | "wallet" | "address_autocomplete" | "internal";
  severity: "fatal" | "recoverable" | "degraded";
  message: string;               // fixed English sentence for the code
  timestamp: string;             // ISO 8601
  paymentMethod?: "payment-card" | "bank-transfer" | "saved-instrument" | "apple-pay" | "google-pay";
  httpStatus?: number;
  reason?: string;               // 3D Secure step that failed
  fields?: string[];             // invalid field ids, never values
  checkoutId?: string;
  transactionId?: string;
}

ThemeConfig

interface ThemeConfig {
  mode?: "dark" | "light";       // defaults to "light"
  colors?: {
    seed?: {
      brand: string;
      neutral: string;
      info: string;
      positive: string;
      negative: string;
      warning: string;
    };
    overrides?: {
      "bg-main": string;         // main background override
    };
  };
}

BillingDetailsConfig & SubmitPaymentRequest

interface BillingDetailsConfig {
  fields?: {
    address?: boolean;           // false ⇒ you provide the address at submit time
  };
  addressLayout?: "compact" | "full"; // optional, default "compact"
}

interface SubmitPaymentRequest {
  billingDetails: {
    address?: {
      address1: string;          // required
      address2?: string;
      city: string;              // required
      state: string;             // required
      postalCode: string;        // required
      country: string;           // required, ISO 3166-1 alpha-2
    };
  };
}

Going live

A short checklist before you flip environment to "production":

  1. Choose how you take updates — read the SDK Versioning and Support Policy and decide between the CDN (automatic 1.x updates) and an exact npm version.
  2. Create checkouts server-side — never trust an amount that came from the browser.
  3. Confirm payments server-side — fulfil an order only after the checkout-status-changed webhook or Get Checkout shows PAID. Browser events can be lost when the buyer closes the tab.
  4. Handle every outcome — checkout-completed, checkout-failed, and checkout-expired should each lead somewhere sensible, and log sdk-error so you can see failures that never reach a decline.
  5. Show progress — use payment-processing to disable the button and signal work in flight.
  6. Clean up — call element.destroy() when your component unmounts.
  7. Register your domain for Apple Pay — send the domain that hosts the element to your Payment Labs contact. See Apple Pay and Google Pay.
  8. Test the edges — a decline, an unconfirmed payment, an expired checkout, and (if you use it) the zero-amount save-card flow. Use Credit Card Payin Test Values.

Errors and edge cases

  • Fulfil an order only after your server sees PAID. A checkout-completed event is not proof of payment.
  • checkout-failed with unresolved: true, or the UNEXPECTED_ERROR reason, means the result is not known yet. Do not ask the buyer to pay again; read the checkout on your server.
  • After a failure the element does not offer another attempt on the same checkout. To let the buyer try again, create a new checkout. See Checkout Statuses and Failure Reasons.
  • When you contact Payment Labs support about an element, quote the checkoutId and transactionId from the sdk-error payload.

Next steps


Did this page help you?