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
- Your merchant ID, for example
C-01JBX2Q8F4H6K8M0N2P4R6T8VW. It is themerchantIdon every checkout you create, and Get Merchant returns it. - 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.
- A checkout or plan offer, created from your backend. Creating it on the server keeps the amount out of the buyer's reach.
- One-time payments: Create Checkout. See Hosted Checkout Page and Iframe for the fields.
- Subscriptions: Create Subscription Plan, then Create Plan Offer. See Subscriptions.
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
- Your backend creates a checkout (the intent to collect money or save a card) and gets back a checkout ID.
- Your frontend hands that ID to the SDK, which renders the payment element inside a container you choose.
- The buyer enters their details. The SDK tokenizes them securely (your servers never touch raw card data) and submits the transaction.
- 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 handles | You handle |
|---|---|
| Secure card / bank / wallet input fields | Product pages, cart, and order summary |
| PCI-compliant tokenization & submission | Pricing, tax, and shipping logic |
| Payment, save-card, and subscription flows | Order creation and fulfillment |
| Real-time events and result screens | The "Pay" button and surrounding UI |
| Theming to match your brand | Creating 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-jsPrefer 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 fieldsThe 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
addressisfalseand you callsubmitPayment()without one, the call fails with"Address information is required but not provided".
fields.address | Who renders the address | Where the data comes from |
|---|---|---|
true (default) | The SDK | The element's own fields |
false | You | The 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 });
});| Field | What it is |
|---|---|
paymentMethod | payment-card, bank-transfer or third-party-token. Set when a method is selected, or when the form offers only one. |
amount | The subtotal: what the checkout asks for, before any fee. |
payableAmount | The total with paymentMethod. Without a method, present only when every method pays the same. |
recurring | A 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
| Event | Fires when | Payload |
|---|---|---|
checkout-loaded | The form is rendered and ready | The checkout details |
payment-method-changed | The form shows, and each time the buyer selects or clears a payment method | PaymentMethodChangedPayload — see Showing the fee |
payment-processing | Submission starts/stops | boolean (true = busy) |
checkout-processing | The charge is underway | — |
checkout-completed | Payment (or card save) succeeded | — |
checkout-failed | Payment failed, or could not be confirmed | { errorReason?, extendedErrorReason?, unresolved? } |
checkout-expired | The checkout's time window passed | — |
sdk-error | Anything other than a declined payment went wrong | SdkErrorPayload — see SDK errors |
payment-failed | Deprecated, 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
| Event | Fires when | Payload |
|---|---|---|
subscription-loaded | The offer is rendered and ready | — |
subscription-plan-changed | The buyer selects a different plan | SubscriptionPlan |
checkout-loaded | The chosen plan's checkout is on screen. submitPayment() can only succeed after it, so keep your pay button hidden until it fires | The checkout details |
payment-method-changed | The plan's checkout shows, and each time the buyer selects or clears a payment method | PaymentMethodChangedPayload — see Showing the fee |
subscription-processing | The subscription is being set up | — |
payment-processing | Submission starts/stops | boolean |
subscription-completed | The subscription was created | — |
subscription-failed | Subscription setup failed | { errorReason?, extendedErrorReason? } |
subscription-expired | The offer's time window passed | — |
sdk-error | Anything other than a declined payment went wrong | SdkErrorPayload |
// 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)
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:
| Severity | Meaning |
|---|---|
fatal | The flow stopped. The buyer can't continue without reloading the page. |
recoverable | The buyer can fix something and try again. |
degraded | A 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:
| Field | When |
|---|---|
paymentMethod | "payment-card", "bank-transfer", "saved-instrument", "apple-pay" or "google-pay" |
httpStatus | An API call failed with this status |
reason | The 3D Secure step that failed (e.g. "authentication-denied") |
fields | Ids of the invalid fields — never their values |
checkoutId, transactionId | Ids 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
| Code | Severity | When |
|---|---|---|
PAYMENT_FORM_LOAD_FAILED | fatal | The card form failed to load |
CHECKOUT_RELOAD_FAILED | fatal | The checkout couldn't be loaded for a signed-in buyer |
SUBSCRIPTION_OFFER_LOAD_FAILED | fatal | The subscription offer couldn't be loaded |
SAVED_METHODS_LOAD_FAILED | degraded | The buyer's saved payment methods couldn't be loaded |
SESSION_EXPIRED | fatal | The session expired (HTTP 401) — the page must be reloaded |
SESSION_FORBIDDEN | fatal | The session was refused (HTTP 403) |
VERIFICATION_CODE_REQUEST_FAILED | recoverable | The sign-in code or password-reset email couldn't be sent |
NO_PAYMENT_METHOD_SELECTED | recoverable | Payment was submitted with no method selected |
FIELD_VALIDATION | recoverable | Fields are invalid — fields lists them |
CHARGE_VALIDATION | recoverable | The API rejected fields (HTTP 400) — fields lists them |
BILLING_ADDRESS_REQUIRED | fatal | Address fields are disabled and submitPayment() got no address |
CHARGE_REJECTED | fatal | The charge request was rejected |
CHECKOUT_LOCKED | fatal | The checkout is already being paid (HTTP 423) |
CHARGE_NO_RESPONSE | fatal | The charge request returned nothing |
THREE_DS_FAILED | recoverable | A 3D Secure step failed — the outcome still follows on checkout-failed or checkout-completed |
THREE_DS_TIMEOUT | recoverable | The 3D Secure challenge timed out |
PAYMENT_STATUS_UNKNOWN | fatal | The payment status couldn't be confirmed — it may still complete |
WALLET_UNAVAILABLE | degraded | Apple Pay / Google Pay couldn't be offered |
WALLET_MERCHANT_VALIDATION_FAILED | recoverable | Apple Pay merchant validation failed |
WALLET_PAYMENT_FAILED | recoverable | The wallet payment couldn't be completed |
WALLET_CANCELLED | recoverable | The buyer closed the wallet sheet |
ADDRESS_AUTOCOMPLETE_UNAVAILABLE | degraded | Address suggestions are off — the buyer can still type the address |
RETRY_FAILED | fatal | The form couldn't be rebuilt for another attempt |
UNEXPECTED_ERROR | fatal | Anything else |
Good to know
- Codes are stable. New codes may be added; existing ones are never renamed or removed. Always keep a
defaultbranch 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-errorlistener throws, the error is ignored and the payment flow carries on. - Older SDK versions. SDK versions released before
sdk-errorexisted throwInvalid event typefromelement.on("sdk-error", …). If you support them, wrap the call intry. payment-failedis deprecated. Usesdk-errorinstead.
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)
PaymentLabs.create(config)Creates an SDK instance. Call it once and reuse it.
const sdk = PaymentLabs.create({
environment: "sandbox", // "sandbox" | "production"
merchantId: "C-01JBX2Q8F4H6K8M0N2P4R6T8VW",
});| Field | Type | Description |
|---|---|---|
environment | "sandbox" | "production" | Which Payment Labs environment to talk to |
merchantId | string | Your merchant identifier |
Returns a PaymentLabs instance.
sdk.createElement(type, id, options)
sdk.createElement(type, id, options)Creates and mounts a payment element. Returns Promise<PaymentElement>.
| Parameter | Type | Description |
|---|---|---|
type | "checkout" | "subscription" | The kind of element to render |
id | string | A checkout ID (for "checkout") or plan offer ID (for "subscription") |
options | ElementOptions | See 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)
sdk.updateTheme(mode)Switches the element between "light" and "dark". It re-renders the secure card fields (see Theming).
element.submitPayment(request?)
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)
element.on(event, callback)Registers an event listener. See Events for the full list.
element.getCheckoutData()
element.getCheckoutData()Returns the current Checkout (or null). Most useful inside checkout-loaded.
element.getSubscriptionOfferData()
element.getSubscriptionOfferData()Returns the current SubscriptionOffer (or null). Most useful inside subscription-loaded.
element.destroy()
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 themSubscriptionOffer & 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":
- 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.
- Create checkouts server-side — never trust an amount that came from the browser.
- Confirm payments server-side — fulfil an order only after the
checkout-status-changedwebhook or Get Checkout showsPAID. Browser events can be lost when the buyer closes the tab. - Handle every outcome —
checkout-completed,checkout-failed, andcheckout-expiredshould each lead somewhere sensible, and logsdk-errorso you can see failures that never reach a decline. - Show progress — use
payment-processingto disable the button and signal work in flight. - Clean up — call
element.destroy()when your component unmounts. - Register your domain for Apple Pay — send the domain that hosts the element to your Payment Labs contact. See Apple Pay and Google Pay.
- 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. Acheckout-completedevent is not proof of payment. checkout-failedwithunresolved: true, or theUNEXPECTED_ERRORreason, 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
checkoutIdandtransactionIdfrom thesdk-errorpayload.
Next steps
Updated 1 day ago
