@paypercut/checkout-js
v1.4.0
Published
Lightweight JavaScript SDK for Paypercut Checkout
Readme
Paypercut Checkout JavaScript SDK
A lightweight, framework-agnostic JavaScript SDK for embedding Paypercut Checkout into your web application. Works seamlessly with vanilla JavaScript, TypeScript, React, Vue, Angular, and other modern frameworks.
Table of Contents
- Installation
- Elements
- Developer Assistance
- Complete TypeScript / ESM example
- Complete vanilla JavaScript example
- Card Element
- Split card fields
- Payment Element availability
- Express Checkout Element
- Confirming payments and handling next actions
- Errors, retries, and application return
- Events
- Localization, appearance, and layout
- Lifecycle and safe teardown
- Quick Start — Embedded Checkout
- API Reference — Embedded Checkout
- Customisation
- Events — Embedded Checkout
- Form Validation for Wallet Payments
- Types
- Usage Examples
- Security
- Troubleshooting
- Performance Optimization
- Best Practices
- FAQ
Installation
Via NPM
npm install @paypercut/checkout-jsVia Yarn
yarn add @paypercut/checkout-jsVia PNPM
pnpm add @paypercut/checkout-jsVia CDN
<!-- Pin an exact released version in production. -->
<script src="https://cdn.jsdelivr.net/npm/@paypercut/checkout-js@latest/dist/paypercut-checkout.iife.min.js"></script>
<!-- or UNPKG -->
<script src="https://unpkg.com/@paypercut/checkout-js@latest/dist/paypercut-checkout.iife.min.js"></script>Elements
The Elements API is the composable integration surface for merchant-owned
payment pages. One elements() group owns the shared amount, currency, mode,
locale, appearance, secure components, validation state, and Payment Method
creation attempt.
The lifecycle is deliberately explicit:
paypercut.elements(options)creates a group.elements.create(type)creates a visual Element; mounting it only renders secure collection UI.await elements.submit()validates and prepares the current group. It does not create a Payment Method or confirm a payment.await paypercut.createPaymentMethod({ elements, params? })creates an opaque Payment Method.- Send only the Payment Method ID and merchant-owned order data to your server.
- If the server returns an opaque client secret, call
confirmPayment()(or its advanced aliashandleNextAction()) in the browser.
Mounting an Element never creates a Payment Method and never creates or confirms a payment. The merchant server remains authoritative for the final amount, currency, order state, authorization, capture, and fulfillment.
Developer Assistance
The Developer Assistant is an optional configuration preview tool for developers and administrators. Use it for development or an authenticated, restricted preview. It ships separately so the default SDK ESM, CommonJS, and browser bundles contain none of its UI or supporting assets.
For TypeScript or ESM, enable it on the client created from the root package:
import { Paypercut } from '@paypercut/checkout-js';
const paypercut = Paypercut({
publishableKey: 'pk_test_replace_me',
developerAssistant: true,
});For a classic browser script, load the version-pinned root IIFE and use its
Paypercut global. The root script loads the matching optional artifact when it
is needed; do not add a second script tag for Developer Assistance:
<!-- Replace VERSION with the exact released SDK version you use. -->
<script src="https://unpkg.com/@paypercut/checkout-js@VERSION/dist/paypercut-checkout.iife.min.js"></script>
<script>
const paypercut = Paypercut({
publishableKey: 'pk_test_replace_me',
developerAssistant: true,
});
</script>Continue with the complete TypeScript / ESM
or complete vanilla JavaScript checkout
example; developerAssistant changes only whether the Developer Assistant is
available.
developerAssistant is a strict boolean. It defaults to false, is enabled only
when explicitly set to true, and rejects other values during Paypercut
initialization. The SDK does not authenticate developers or administrators and
this option is not an access-control boundary. Gate it with your own environment
check or authenticated feature flag. Enabling it on a public checkout makes the
controls visible to that checkout's visitors. Production use is not technically
blocked, but the SDK provides no production support guarantee for the Developer
Assistant and it should not be exposed to ordinary shoppers.
Developer Assistance requires a browser DOM and activates for compatible Card
and Express Checkout Elements. In a server-rendered application, initialize the
client and mount its Elements only from a client-side lifecycle. The Developer
Assistant mounts once, stays available across compatible component
reconfiguration or reload, and follows the owning Elements lifecycle. Call
elements.destroy() during page or component teardown as described in
Lifecycle and safe teardown; after the last
compatible enabled Element is destroyed, the Developer Assistant is removed.
Loading is best-effort. Missing browser or script-loading capability, a blocked
request, or an unavailable version-matched artifact disables only Developer
Assistance: checkout and payment flows continue, and the SDK emits one sanitized
developer_assistance_load_failed console warning. The Developer Assistant
collects no usage telemetry. Its only public application contract is the
developerAssistant boolean; there is no separate Developer Assistance API to
initialize or manage.
If the page enforces Content Security Policy, its script-src must allow the
origin serving the root SDK and its separately loaded, exact-version Developer
Assistance artifact. Keep the frame-src and connect-src permissions required
by the Card or Express Checkout integration as described in
Content Security Policy.
Complete TypeScript / ESM example
Use this merchant-owned markup:
<form id="payment-form">
<label>Billing name <input id="billing-name" autocomplete="cc-name" /></label>
<label>Billing email <input id="billing-email" type="email" autocomplete="email" /></label>
<div id="card-element"></div>
<button id="pay" type="submit" disabled>Pay</button>
</form>import {
Paypercut,
type ElementsSubmitError,
type PaypercutConfirmPaymentResult,
} from '@paypercut/checkout-js';
type MerchantResponse = {
clientSecret?: string;
redirectUrl?: string;
};
const paypercut = Paypercut({ publishableKey: 'pk_test_replace_me' });
const elements = paypercut.elements({
mode: 'payment',
amount: 4999,
currency: 'EUR',
locale: 'auto',
appearance: {
theme: 'light',
inputs: 'condensed',
labels: 'auto',
},
});
const card = elements.create('card');
card.on('ready', () => {
document.querySelector<HTMLButtonElement>('#pay')!.disabled = false;
});
card.on('error', ({ code, recoverable }) => {
console.error('Secure payment UI error', { code, recoverable });
});
card.mount('#card-element');
const form = document.querySelector<HTMLFormElement>('#payment-form')!;
form.addEventListener('submit', async (event) => {
event.preventDefault();
try {
await elements.submit();
const paymentMethod = await paypercut.createPaymentMethod({
elements,
params: {
billing_details: {
name: document.querySelector<HTMLInputElement>('#billing-name')!.value,
email: document.querySelector<HTMLInputElement>('#billing-email')!.value,
},
},
});
const response = await fetch('/api/payments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payment_method: paymentMethod.id }),
});
if (!response.ok) throw new Error('The server could not start the payment.');
const serverResult = (await response.json()) as MerchantResponse;
let result: PaypercutConfirmPaymentResult | undefined;
if (serverResult.clientSecret) {
result = await paypercut.confirmPayment({
clientSecret: serverResult.clientSecret,
});
}
if (
result &&
!['succeeded', 'processing', 'requires_capture'].includes(
result.checkoutSession.paymentObjectStatus ?? '',
)
) {
throw new Error(`Payment status: ${result.checkoutSession.paymentObjectStatus}`);
}
if (serverResult.redirectUrl) window.location.assign(serverResult.redirectUrl);
} catch (error) {
const failure = error as Partial<ElementsSubmitError>;
console.error('Payment attempt failed', {
code: failure.code ?? 'merchant_payment_failed',
recoverable: failure.recoverable ?? false,
});
}
});
window.addEventListener('pagehide', () => elements.destroy(), { once: true });The same named client is available from CommonJS:
const { Paypercut } = require('@paypercut/checkout-js');
const paypercut = Paypercut({ publishableKey: 'pk_test_replace_me' });Complete vanilla JavaScript example
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Paypercut card payment</title>
</head>
<body>
<form id="payment-form">
<label>Billing name <input id="billing-name" autocomplete="cc-name" /></label>
<div id="card-element"></div>
<button id="pay" type="submit" disabled>Pay</button>
<p id="payment-status" role="status"></p>
</form>
<script src="https://cdn.jsdelivr.net/npm/@paypercut/checkout-js@latest/dist/paypercut-checkout.iife.min.js"></script>
<script>
const status = document.querySelector('#payment-status');
const paypercut = window.Paypercut({ publishableKey: 'pk_test_replace_me' });
const elements = paypercut.elements({
mode: 'payment',
amount: 4999,
currency: 'EUR',
locale: 'auto',
});
const card = elements.create('card');
card.on('ready', () => {
document.querySelector('#pay').disabled = false;
});
card.on('error', ({ code }) => {
status.textContent = `Payment form error: ${code}`;
});
card.mount('#card-element');
document.querySelector('#payment-form').addEventListener('submit', async (event) => {
event.preventDefault();
status.textContent = 'Preparing payment…';
try {
await elements.submit();
const paymentMethod = await paypercut.createPaymentMethod({
elements,
params: {
billing_details: {
name: document.querySelector('#billing-name').value,
},
},
});
const response = await fetch('/api/payments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payment_method: paymentMethod.id }),
});
const serverResult = await response.json();
if (!response.ok) throw new Error('The server rejected the payment.');
if (serverResult.clientSecret) {
const result = await paypercut.confirmPayment({
clientSecret: serverResult.clientSecret,
});
status.textContent = `Payment status: ${result.checkoutSession.paymentStatus}`;
} else {
status.textContent = 'Payment Method created.';
}
} catch (error) {
status.textContent = error && error.code
? `Payment error: ${error.code}`
: 'Payment failed.';
}
});
window.addEventListener('pagehide', () => elements.destroy(), { once: true });
</script>
</body>
</html>Creating the group does not create a payment, setup, subscription, or Payment Method. Amounts are non-negative integers in the currency's minor unit and the currency is a three-letter ISO 4217 code.
Card Element
const card = elements.create('card');
card.mount('#card-element');
await elements.submit();
const paymentMethod = await paypercut.createPaymentMethod({ elements });
// Send paymentMethod.id to your server. Do not confirm a payment from browser input.When the same card may be used again, declare that purpose once while creating the Elements group:
const elements = paypercut.elements({
mode: 'payment',
amount: 4999,
currency: 'USD',
setupFutureUsage: 'off_session', // or 'on_session'
});
const card = elements.create('card');
card.mount('#card-element');
await elements.submit();
const paymentMethod = await paypercut.createPaymentMethod({
elements,
params: {
billing_details: {
name: 'Ada Lovelace',
email: '[email protected]',
address: { country: 'GB', postal_code: 'SW1A 1AA' },
},
},
});Omitting setupFutureUsage in payment mode prepares the Payment Method for the
current payment only. Use on_session when the customer is expected to be
present for later use, or off_session when the server may use it without the
customer present. This is an immutable group-level option;
createPaymentMethod() cannot override it. It prepares the instrument and
authentication context but does not create a mandate, attach a customer, or
guarantee that a future authorization will succeed.
params.billing_details belongs to the Payment Method creation attempt, not to
the Elements group or a durable Customer. It accepts name, email, phone,
and address (line1, line2, city, state, postal_code, country).
Country accepts an ISO 3166-1 alpha-2 code and is normalized to uppercase. The
call does not create or attach a Customer. An identical retry on the same group
shares the existing attempt; changing billing details after creation requires a
fresh Elements group.
For flows that are already known to prepare a card for later use, initialize the
group with setup or subscription mode:
const elements = paypercut.elements({
mode: 'subscription', // or 'setup'
amount: 4999,
currency: 'USD',
});
const card = elements.create('card');
card.mount('#card-element');
await elements.submit();
const paymentMethod = await paypercut.createPaymentMethod({ elements });Both modes derive setupFutureUsage: 'off_session' automatically and reject an
on_session override. They prepare an authenticated card Payment Method only:
they do not create a setup resource, subscription, mandate, customer attachment,
Checkout Session, or payment. amount and currency are still required by the
current public signature. Express Checkout is currently available only in
payment mode.
Split card fields
Use split fields when the merchant layout needs independent placement. All three
fields belong to the same Elements group. A field never exposes submit() and
its change event contains only PCI-safe state such as completeness and card
brand—never PAN, expiry, or CVC values.
const cardNumber = elements.create('cardNumber', {
// Defaults to true. Set false to hide every card-number badge and reclaim
// its input space.
showIcon: true,
});
const cardExpiry = elements.create('cardExpiry');
const cardCvc = elements.create('cardCvc');
cardNumber.mount('#card-number');
cardExpiry.mount('#card-expiry');
cardCvc.mount('#card-cvc');
cardNumber.on('change', ({ complete, empty, brand }) => {
console.log({ complete, empty, brand });
});
// Validation is explicit and creates no server-side payment resources.
await elements.submit();
// Payment Method creation uses the same public lifecycle as Card Element.
const paymentMethod = await paypercut.createPaymentMethod({
elements,
params: {
billing_details: {
name: document.querySelector<HTMLInputElement>('[autocomplete="cc-name"]')?.value,
},
},
});For reusable cards, set setupFutureUsage: 'on_session' | 'off_session' in the
paypercut.elements(...) options before creating these fields.
Keep the three mount points and any merchant-owned cardholder-name input inside
one semantic form. Use the standard cc-name token for that merchant-owned
field; Paypercut supplies the corresponding cc-number, cc-exp, and cc-csc
metadata inside the three secure inputs.
<form id="payment-form" autocomplete="on">
<label>
Cardholder name
<input name="cardholderName" autocomplete="cc-name" />
</label>
<div id="card-number"></div>
<div id="card-expiry"></div>
<div id="card-cvc"></div>
<button type="submit">Continue</button>
</form>The secure fields expose the standard browser saved-card autocomplete metadata. This is browser/password-manager integration, not Apple Pay or Google Pay. Browsers may intentionally decline to store or refill CVC, so CVC autofill must never be a checkout requirement.
One group accepts exactly one field of each split type. It rejects duplicate fields and rejects mixing split fields with the high-level Card Element; Express Checkout may coexist.
The SDK owns the non-sensitive visual shell around each secure input:
above or floating labels, focus and invalid borders, localized error messages,
and the card-number brand badges. appearance.inputs and appearance.labels
use the same condensed | spaced and auto | above | floating contract as the
Card Element. .Input, .Input:focus, .Input--invalid, .Label, and
.Error rules apply to split fields as well. Card-brand SVGs are embedded in
the SDK bundle, so showing them does not add an image-host CSP requirement.
Incomplete and invalid provider state is exposed through the PCI-safe change
event while the user edits. The visible error is delayed until blur and clears
on refocus or correction. This applies independently to card number, expiration
date, and security code.
elements.submit() validates the active payment-method source and displays its
field errors without tokenizing or creating server-side payment resources. A
successful validation is bound to the current source and input revision; card
edits, remounts, test presets, or group updates require another submit().
paypercut.createPaymentMethod({ elements, params? }) is the regular
Payment Method creation facade. params.billing_details is call-level Payment
Method data; it is not mutable group-level customer state. Calling creation
without a current successful validation rejects with
elements_submit_required.
Submission is single-flight and its terminal state belongs to the Elements
group, not to a particular iframe or field mount. Concurrent calls share one
attempt, and a successful result is returned again without creating another
Payment Method. If the SDK loses the authoritative result after submission has
started and still cannot verify it, the group rejects with
elements_submit_indeterminate and recoverable: false. Do not blindly create
a second Payment Method. Reconcile the order on your server; only then destroy
the old group and begin a deliberate new attempt.
Payment Element availability
This release does not export elements.create('payment'). The name is
reserved for a future multi-payment-method selector. Use Card Element for a
combined card form, split card fields for a custom card layout, and Express
Checkout Element for Apple Pay and Google Pay. A merchant asking for Card
Element will never receive additional payment methods because of server-side
configuration.
Confirming payments and handling next actions
Payment Method creation and payment confirmation are separate operations. Send the opaque Payment Method ID to your authenticated server. The server creates or confirms the order and may return an opaque Checkout Session client secret when browser participation is required:
const paymentMethod = await paypercut.createPaymentMethod({ elements });
const response = await fetch('/api/payments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payment_method: paymentMethod.id }),
});
if (!response.ok) throw new Error('Unable to start payment.');
const { clientSecret } = (await response.json()) as { clientSecret?: string };
if (clientSecret) {
const result = await paypercut.confirmPayment({ clientSecret });
const payment = result.checkoutSession;
if (
payment.paymentObjectStatus === 'succeeded' ||
payment.paymentObjectStatus === 'requires_capture'
) {
window.location.assign('/payment-complete');
} else if (payment.paymentObjectStatus === 'requires_payment_method') {
showMessage('Choose another payment method.');
} else if (payment.paymentObjectStatus === 'processing') {
showMessage('Your payment is processing.');
}
}The exact public signatures are:
paypercut.confirmPayment({ clientSecret: string }): Promise<PaypercutConfirmPaymentResult>;
paypercut.handleNextAction({ clientSecret: string }): Promise<PaypercutConfirmPaymentResult>;confirmPayment() is the primary facade: it retrieves authoritative state,
performs any supported browser action, confirms again when required, polls when
the server recommends polling, and returns the latest authoritative Checkout
Session state. handleNextAction() is an exact advanced alias backed by the
same coordinator. It is useful when integration code wants to name the
continuation step explicitly, but it does not accept a raw next-action payload.
Both methods currently accept exactly one input form: { clientSecret }.
The client secret is an opaque browser capability. Do not parse it, put it in a URL, or include it in logs or analytics. The current continuation supports payment-mode Checkout Sessions. Setup and subscription groups create Payment Methods, but server-side confirmation for those modes is not performed by this browser method in this release.
A server may require another action after initial Payment Method creation or after payment confirmation. Reuse the same Payment Method; do not collect and create it again merely because a later authentication step is required. Payment Method creation itself may also perform proactive card authentication. In either phase, frictionless authentication presents no dialog. A genuine challenge or out-of-band approval uses provider-owned UI; integrations must not replace, cover, restyle, or attempt to read that UI.
Errors, retries, and application return
Validation, Payment Method creation, and confirmation reject with an Error
that includes code and recoverable. Use code for your localized customer
copy and keep a generic fallback for newer codes:
type PublicElementsError = Error & { code?: string; recoverable?: boolean };
try {
await elements.submit();
const paymentMethod = await paypercut.createPaymentMethod({ elements });
await sendToMerchantServer(paymentMethod.id);
} catch (unknownError) {
const error = unknownError as PublicElementsError;
if (error.code === 'elements_payment_details_incomplete') {
showMessage('Complete your payment details.');
} else if (error.code === 'elements_action_canceled') {
showMessage('Authentication was canceled. You can try again.');
} else if (error.recoverable) {
showRetryButton();
} else {
showAnotherPaymentMethod();
}
}For validation errors, keep the current group mounted, let the customer correct
the fields, and call submit() again. For a recoverable confirmation
cancellation, timeout, or transient network failure, call confirmPayment()
again with the same client secret; it starts by reading authoritative state.
Never blindly recreate a Payment Method after
elements_submit_indeterminate.
During an out-of-band flow, the SDK listens for visibility, page-show, online,
and focus return and wakes its status check automatically. There is no public
manual-wake button or method. If a browser or WebView reloads the entire page,
obtain the same active client secret from your server and call
confirmPayment() again. A terminal requires_payment_method result means the
server requires a fresh payment-method attempt; processing means the server
still owns completion.
Elements events
Every on() call returns an unsubscribe function.
| Surface | Events | Public data |
| --- | --- | --- |
| Card Element | ready, resize, error | readiness, { height }, or { code, message, recoverable } |
| Split card field | ready, change, focus, blur, error | change exposes complete, empty, optional brand, and a safe validation error; never field values |
| Express Checkout | ready, availablepaymentmethodschange, resize, click, shippingaddresschange, shippingratechange, confirm, cancel, error | wallet availability, merchant-requested contact/shipping data, and an opaque Payment Method |
There are no group-level validation or completion events. Await
elements.submit() for validation, createPaymentMethod() for Payment Method
creation, and confirmPayment() for server-started payment continuation. The
Express Checkout confirm event is the wallet completion seam; call exactly one
of event.complete() or event.paymentFailed() after your server responds.
Localization, appearance, and layout
Pass locale: 'auto', a language code such as fr, or a regional code such as
fr-FR. Language matching is case-insensitive and accepts compact regional
forms such as BGBG. Unsupported values behave like auto: browser languages
are tried first, followed by en-GB. Current translated languages are
Bulgarian, Czech, Danish, Dutch, English, Finnish, French, German, Greek,
Hungarian, Italian, Norwegian, Polish, Portuguese, Romanian, Slovak, Spanish,
and Swedish. The hosted runtime supplies the catalog used by every Element in
the group; translations are not hardcoded into the SDK bundle.
locale, mode, currency, and setupFutureUsage are immutable. Recreate the
group to change them. Configure Appearance before the first Element mounts;
after that mount, Appearance is locked for the lifetime of the group by default.
Amount remains independently mutable:
const elements = paypercut.elements({
mode: 'payment',
amount: 4999,
currency: 'EUR',
locale: 'de-DE',
appearance: {
theme: 'dark',
inputs: 'spaced',
labels: 'above',
variables: {
colorPrimary: '#635bff',
borderRadius: '8px',
},
rules: {
'.Input:focus': { borderColor: '#635bff' },
'.Error': { color: '#b42318' },
},
},
});
elements.update({
amount: 5999,
});Appearance is locked after the first Element mounts by default. A later
elements.update({ appearance }) call ignores only Appearance, logs one
elements_appearance_locked warning, and still applies other valid properties
in the same update, such as amount.
appearanceUpdates: 'remount' is an explicit integration-preview escape hatch
and should not be used for routine theme changes on a live payment page. An
Appearance update then remounts the affected payment fields while retaining
their public Element objects and listeners. During the remount, submission can
return a recoverable elements_not_ready error and entered secure-field data is
cleared. Wait for the Element's next ready event before resuming submission. If
the remount fails, the last successfully applied Appearance remains active.
appearance.inputs controls Card and split-field density (condensed or
spaced); appearance.labels accepts auto, above, or floating.
theme, allow-listed variables, semantic rules, and disableAnimations
apply across the group. Express Checkout has its own button grid under the
Element's layout option (maxColumns, maxRows, and overflow).
Lifecycle and safe teardown
Element handles and groups cannot be reused after destroy():
const card = elements.create('card');
card.mount('#card-element');
// Remove one mounted surface.
card.destroy();
// Create a fresh handle if the same group is still valid.
const replacementCard = elements.create('card');
replacementCard.mount('#card-element');
// Final page/component cleanup. This destroys every Element in the group.
elements.destroy();Call elements.destroy() when the owning page or framework component unmounts.
For a later checkout attempt, create a new Elements group and new Element
handles. Destroying UI does not cancel, reverse, or alter a server-side payment;
always reconcile the order with your server before starting another attempt.
If your page enforces Content Security Policy, allow the SDK distribution origin and Paypercut-hosted frames and connections used by your integration. Do not copy internal frame URLs or action parameters into application code; contact Paypercut support for the current restrictive-CSP allowlist.
Express Checkout Element
The Express Checkout Element is one component. It checks wallet availability and renders the available Apple Pay and Google Pay buttons inside its hosted frame. It is not one Paypercut iframe per wallet and it is not a WooCommerce-specific widget.
import { Paypercut } from '@paypercut/checkout-js';
const paypercut = Paypercut({
publishableKey: 'pk_test_replace_me',
});
const elements = paypercut.elements({
mode: 'payment',
amount: 4999,
currency: 'EUR',
locale: 'en',
});
const expressCheckout = elements.create('expressCheckout', {
paymentMethods: {
applePay: 'auto',
googlePay: 'auto',
},
layout: {
maxColumns: 2,
maxRows: 1,
},
buttonHeight: 48,
emailRequired: true,
billingAddressRequired: true,
});
expressCheckout.on('ready', ({ availablePaymentMethods }) => {
const unavailable =
!availablePaymentMethods.applePay && !availablePaymentMethods.googlePay;
document.querySelector('#express-checkout')?.toggleAttribute('hidden', unavailable);
});
expressCheckout.on('click', (event) => {
// Resolve or reject promptly while the wallet's user gesture is active.
event.resolve({
lineItems: [{ name: 'Order total', amount: 4999 }],
});
});
expressCheckout.on('confirm', async (event) => {
try {
const response = await fetch('/api/payments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
payment_method: event.paymentMethod.id,
billing_details: event.billingDetails,
}),
});
if (!response.ok) throw new Error('The server rejected the wallet payment.');
const serverResult = (await response.json()) as { clientSecret?: string };
if (serverResult.clientSecret) {
const result = await paypercut.confirmPayment({
clientSecret: serverResult.clientSecret,
});
if (
!['succeeded', 'processing', 'requires_capture'].includes(
result.checkoutSession.paymentObjectStatus ?? '',
)
) {
throw new Error(`Payment status: ${result.checkoutSession.paymentObjectStatus}`);
}
}
event.complete();
} catch (error) {
event.paymentFailed({
code: 'merchant_checkout_failed',
message: error instanceof Error ? error.message : 'Payment failed.',
});
}
});
expressCheckout.on('error', ({ code, message, recoverable }) => {
console.error('Express Checkout error', { code, message, recoverable });
});
expressCheckout.mount('#express-checkout');The component owns wallet capability detection, button rendering, loading
states, resizing, secure collection, Payment Method creation, and any required
customer authentication. Do not call elements.submit() or
createPaymentMethod() for the Payment Method already supplied by its confirm
event. The merchant integration owns product/cart placement, shipping quotes,
provider-neutral address conversion, and server-side order/payment confirmation.
The native wallet stays pending until the adapter calls complete() or
paymentFailed().
Shipping is opt-in. Set shippingAddressRequired and provide shippingRates only
for physical fulfillment. When enabled, the Element emits address and rate change
events and requires each event to be resolved or rejected promptly. The final
confirm event returns the unredacted shippingAddress and selected
shippingRate. Integrations without shipping (for example, an EV charging
payment) should leave those options unset and receive no shipping callbacks.
Quick Start
This returns a checkout session ID like 01KB23M9EC960C50G3AH14FTTT.
1. Embed the Checkout
Use the checkout ID to initialize the checkout on your frontend:
import { PaypercutCheckout } from '@paypercut/checkout-js';
// Initialize the checkout
const checkout = PaypercutCheckout({
id: '01KB23M9EC960C50G3AH14FTTT', // Your checkout session ID from step 1
containerId: '#checkout', // CSS selector or HTMLElement
});
// Listen for payment events
checkout.on('success', () => {
console.log('Payment successful!');
});
checkout.on('error', () => {
console.error('Payment failed');
});
// Optional: Listen for when iframe finishes loading (useful for modal implementations)
checkout.on('loaded', () => {
console.log('Checkout loaded and ready');
});
// Render the checkout
checkout.render();That's it! The checkout is now embedded in your page.
2. Display Mode
The SDK supports one display mode:
| Mode | When to Use | Configuration |
| ------------ | ------------------------------------ | --------------------- |
| Embedded | Checkout is part of your page layout | Provide containerId |
API Reference
PaypercutCheckout(options)
Creates a new checkout instance.
Options
| Option | Type | Required | Default | Description |
| ------------------- | ------------------------ | -------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | string | Yes | — | Checkout session identifier (e.g., 01KB23M9EC960C50G3AH14FTTT) |
| containerId | string \| HTMLElement | Yes | — | CSS selector or element where iframe mounts |
| locale | string | No | 'auto' | Locale for checkout UI. Options: 'auto', 'en', 'en-GB', 'bg', 'bg-BG' |
| lang | string | No | 'auto' | Locale for checkout UI. Options: 'auto', 'en', 'en-GB', 'bg', 'bg-BG' |
| ui_mode | 'hosted' \| 'embedded' | No | 'embedded' | UI mode for checkout display |
| wallet_options | string[] | No | ['apple_pay', 'google_pay'] | Digital wallet options. Pass [] to disable all wallets |
| form_only | boolean | No | false | Show only payment form (no Pay Now button - use external button with submit()) |
| validate_form | boolean | No | false | This indicates that Google Pay/Apple Pay flow to proceed you need to confirm form validation. For EMBEDDED checkouts only) |
| appearance | object | No | undefined | Appearance configuration for the checkout UI |
| appearance.preset | 'modal' \| 'inline' | No | undefined | Controls checkout layout preset. 'modal' shows the order summary (TotalCard) and header inside the iframe — use when rendering in a modal/dialog. 'inline' hides the TotalCard — use when the merchant renders their own order summary outside the checkout. |
Examples
Basic initialization:
const checkout = PaypercutCheckout({
id: '01KB23M9EC960C50G3AH14FTTT',
containerId: '#checkout-container',
});With all options:
const checkout = PaypercutCheckout({
id: '01KB23M9EC960C50G3AH14FTTT',
containerId: '#checkout-container',
locale: 'en', // 'auto' | 'en' | 'en-GB' | 'bg' | 'bg-BG'
lang: 'en', // 'auto' | 'en' | 'en-GB' | 'bg' | 'bg-BG'
ui_mode: 'embedded', // 'hosted' | 'embedded'
wallet_options: ['apple_pay', 'google_pay'], // Can be empty array [] or contain one/both options
form_only: false, // Set true to hide Pay Now button (use external button),
validate_form: true, // Set true to require form validation before wallet payment
appearance: {
preset: 'modal', // 'modal' | 'inline' — 'modal' shows TotalCard + header, 'inline' hides them
},
});Disable wallet payments:
const checkout = PaypercutCheckout({
id: '01KB23M9EC960C50G3AH14FTTT',
containerId: '#checkout-container',
wallet_options: [], // No Apple Pay or Google Pay buttons
});Only Apple Pay:
const checkout = PaypercutCheckout({
id: '01KB23M9EC960C50G3AH14FTTT',
containerId: '#checkout-container',
wallet_options: ['apple_pay'], // Only Apple Pay, no Google Pay
});Form-only mode (external submit button):
const checkout = PaypercutCheckout({
id: '01KB23M9EC960C50G3AH14FTTT',
containerId: '#checkout-container',
form_only: true, // No Pay Now button inside checkout
});
// Use your own button to trigger payment
document.getElementById('my-pay-button').addEventListener('click', () => {
checkout.submit();
});Instance Methods
render()
Mounts and displays the checkout iframe.
checkout.render();destroy()
Destroys the instance and cleans up all event listeners. Call this when you're done with the checkout instance.
checkout.destroy();on(event, handler)
Subscribes to checkout events. Returns an unsubscribe function.
const unsubscribe = checkout.on('success', () => {
console.log('Payment successful!');
});
// Later, to unsubscribe
unsubscribe();once(event, handler)
Subscribes to a checkout event that automatically unsubscribes after the first emission. Returns an unsubscribe function.
checkout.once('loaded', () => {
console.log('Checkout loaded - this will only fire once');
});off(event, handler)
Unsubscribes from checkout events.
const handler = () => console.log('Payment successful!');
checkout.on('success', handler);
checkout.off('success', handler);isMounted()
Returns whether the checkout is currently mounted.
if (checkout.isMounted()) {
console.log('Checkout is visible');
}Customisation
Available since v1.2.5
Paypercut Checkout supports full visual customisation — you can match your brand colors, typography, corner radius, and even inject your logo. Customisation works in two complementary ways:
| Approach | When it applies | How |
| ----------------------------- | -------------------------------------------- | -------------------------------------------------- |
| appearance init option | At iframe creation (zero flash) | Pass in PaypercutCheckout({ appearance: { … } }) |
| checkout.updateAppearance() | After mount (dynamic, e.g. dark-mode toggle) | Call at any time post-render() |
Appearance at init time
Pass an appearance object when creating the checkout instance. These values are serialised into the iframe URL so the correct theme and CSS variables are applied before the first paint — no flash of unstyled content.
const checkout = PaypercutCheckout({
id: 'CHK_xxx',
containerId: '#checkout',
appearance: {
theme: 'dark', // 'light' | 'dark'
preset: 'modal', // 'inline' | 'modal'
variables: {
brandColor: '#ff5500', // CTA button background
fontFamily: 'Inter, sans-serif',
borderRadius: '12px',
logoUrl: 'https://cdn.example.com/logo.png',
},
},
});
checkout.render();preset
| Value | Effect |
| ---------- | ------------------------------------------------------------ |
| 'inline' | Compact form without the order summary card |
| 'modal' | Full form with order summary card + logo rendered at the top |
Dynamic updates — updateAppearance()
Call checkout.updateAppearance(appearance) at any point after render() to update the checkout's visual appearance without a full reload. The SDK sends an UPDATE_APPEARANCE postMessage to the iframe, which merges the incoming values on top of the current state.
// Example: dark-mode toggle on merchant page
document.getElementById('dark-toggle').addEventListener('change', (e) => {
checkout.updateAppearance({ theme: e.target.checked ? 'dark' : 'light' });
});
// Example: update brand color after merchant customiser changes
checkout.updateAppearance({
variables: {
brandColor: '#e63946',
brandColorContrast: '#ffffff',
},
});
// Example: inject logo after it's fetched asynchronously
checkout.updateAppearance({
variables: { logoUrl: 'https://cdn.example.com/logo.png' },
});Updates are merged — you only need to pass the fields you want to change. Fields omitted from the update are left as-is.
Note:
updateAppearance()requires the checkout to be mounted (render()must have been called first). Calling it beforerender()logs a warning and is a no-op.
AppearanceVariables reference
| Field | Type | Description |
| -------------- | -------------------- | -------------------------------------------------------------------------------------------- |
| brandColor | `#${string}` | Background color of the primary CTA (Pay Now) button. Must be a hex value, e.g. '#ff5500'. |
| fontFamily | string | CSS font-family value. Applied globally inside the checkout. E.g. 'Inter, sans-serif'. |
| borderRadius | string | CSS length value for element corner radii. Accepts px, rem, or em. E.g. '8px'. |
| logoUrl | string (https URL) | Your logo shown above the checkout header when preset: 'modal'. Must be https://. |
brandColor must be a 3- or 6-digit hex value (e.g. '#fff' or '#ffffff'). Invalid values log a warning and are silently dropped — the existing color is preserved.
TypeScript types
import type { Appearance, AppearanceVariables } from '@paypercut/checkout-js';
const appearance: Appearance = {
theme: 'dark',
variables: {
brandColor: '#6366f1',
fontFamily: 'Inter, sans-serif',
borderRadius: '10px',
},
};
checkout.updateAppearance(appearance);Events
Subscribe to events using the on() method. You can use string event names or the SdkEvent enum.
Event Reference
| Event | Enum | Description | Payload |
| ------------------ | -------------------------- | --------------------------------------------------------- | ----------------------------- |
| loaded | SdkEvent.Loaded | Checkout iframe has finished loading | void |
| processing | SdkEvent.Processing | Payment processing has started inside the hosted checkout | { checkoutId: string } |
| success | SdkEvent.Success | Payment completed successfully | PaymentSuccessPayload |
| error | SdkEvent.Error | Terminal failure (tokenize, confirm, or 3DS) | ApiErrorPayload |
| expired | SdkEvent.Expired | Checkout session expired | void |
| threeds_started | SdkEvent.ThreeDSStarted | 3DS challenge flow started | object |
| threeds_complete | SdkEvent.ThreeDSComplete | 3DS challenge completed | object |
| threeds_canceled | SdkEvent.ThreeDSCanceled | 3DS challenge canceled by user | object |
| threeds_error | SdkEvent.ThreeDSError | 3DS challenge error | ApiErrorPayload or object |
Usage Examples
Using string event names:
const checkout = PaypercutCheckout({ id: '01KB23M9EC960C50G3AH14FTTT', containerId: '#checkout' });
checkout.on('loaded', () => {
console.log('Checkout loaded');
});
checkout.on('processing', ({ checkoutId }) => {
console.log('Payment processing started for checkout:', checkoutId);
});
checkout.on('success', (payload) => {
// PaymentSuccessPayload
console.log('Payment successful');
console.log('Card brand:', payload.payment_method.brand);
console.log('Last 4:', payload.payment_method.last4);
console.log('Expiry:', payload.payment_method.exp_month + '/' + payload.payment_method.exp_year);
});
checkout.on('error', (err) => {
console.error('Payment error:', err.code, err.message);
});
checkout.on('expired', () => {
console.warn('Checkout session expired');
});Using SdkEvent enum (recommended for TypeScript):
import { PaypercutCheckout, SdkEvent } from '@paypercut/checkout-js';
const checkout = PaypercutCheckout({ id: '01KB23M9EC960C50G3AH14FTTT', containerId: '#checkout' });
checkout.on(SdkEvent.Loaded, () => {
console.log('Checkout loaded');
});
checkout.on(SdkEvent.Processing, ({ checkoutId }) => {
console.log('Payment processing started for checkout:', checkoutId);
});
checkout.on(SdkEvent.Success, (payload) => {
console.log('Payment successful', payload.payment_method);
});
checkout.on(SdkEvent.Error, (err) => {
console.error('Payment error:', err.code, err.message);
});
checkout.on(SdkEvent.Expired, () => {
console.warn('Checkout session expired');
});
// 3DS events
checkout.on(SdkEvent.ThreeDSStarted, (ctx) => {
console.log('3DS challenge started');
});
checkout.on(SdkEvent.ThreeDSComplete, (payload) => {
console.log('3DS completed');
});
checkout.on(SdkEvent.ThreeDSCanceled, (payload) => {
console.log('3DS canceled by user');
});
checkout.on(SdkEvent.ThreeDSError, (err) => {
console.error('3DS error:', err);
});Success Payload
The success event returns a PaymentSuccessPayload with the payment method details:
type PaymentSuccessPayload = {
payment_method: {
brand: string; // e.g., 'visa', 'mastercard', 'amex'
last4: string; // Last 4 digits of card number
exp_month: number; // Expiration month (1-12)
exp_year: number; // Expiration year (e.g., 2030)
};
};Example:
checkout.on('success', (payload) => {
// payload:
// {
// payment_method: {
// brand: 'visa',
// last4: '4242',
// exp_month: 12,
// exp_year: 2030
// }
// }
console.log(
`Paid with ${payload.payment_method.brand} ending in ${payload.payment_method.last4}`,
);
// Output: "Paid with visa ending in 4242"
});Error Handling
checkout.on('error', (err) => {
switch (err.code) {
case 'card_validation_error':
// err.errors[] has field-level issues. Messages are localized.
break;
case 'card_declined':
// Optional err.decline_code and user-friendly err.message
break;
case 'threeds_error':
case 'threeds_authentication_failed':
case 'threeds_canceled':
// 3DS issue. Some servers use status_code 424 with a detailed message.
break;
default:
// Other terminal errors (e.g., authentication_failed, session_expired)
break;
}
});Notes
error: the SDK forwards a normalizedApiErrorPayloadwhen provided by Hosted Checkout.processing: non-terminal event. Use it to disable close buttons or show "do not close" messaging, then clear that UI onsuccess,error,cancel, orexpired.threeds_error: forwardspayload.errorwhen available; otherwise the raw message data.threeds_*non-error events: payload is forwarded as-is from Hosted Checkout (shape may evolve).
Form Validation for Wallet Payments
How It Works
- User clicks Apple Pay or Google Pay button in the checkout
- SDK emits
form_validationevent to your code - You validate your form and call either:
completeFormValidation(wallet)- allows wallet to proceedfailFormValidation(wallet, errors)- blocks wallet, you show your own errors
- If validation passes, the wallet payment sheet opens
Event: form_validation
| Property | Type | Description |
| ------------ | ----------------------------- | ------------------------------- |
| checkoutId | string | The checkout session ID |
| wallet | 'apple_pay' \| 'google_pay' | Which wallet button was clicked |
Methods
completeFormValidation(wallet)
Call this when your form is valid. The wallet payment sheet will open.
checkout.completeFormValidation('google_pay');failFormValidation(wallet, errors?)
Call this when your form has errors. The wallet will not open, and you should display your own error messages.
checkout.failFormValidation('apple_pay', [
{ code: 'invalid_email', message: 'Please enter a valid email address' },
{ code: 'missing_address', message: 'Shipping address is required' },
]);Basic Example
import { PaypercutCheckout, SdkEvent } from '@paypercut/checkout-js';
const checkout = PaypercutCheckout({
id: '01KB23M9EC960C50G3AH14FTTT',
containerId: '#checkout',
wallet_options: ['apple_pay', 'google_pay'],
});
// Handle form validation for wallet payments
checkout.on(SdkEvent.FormValidation, ({ wallet, checkoutId }) => {
// Validate your own form fields
const email = document.getElementById('email').value;
const isEmailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
if (isEmailValid) {
// Form is valid - allow wallet to proceed
checkout.completeFormValidation(wallet);
} else {
// Form has errors - block wallet and show your errors
document.getElementById('email-error').textContent = 'Please enter a valid email';
checkout.failFormValidation(wallet, [
{ code: 'invalid_email', message: 'Please enter a valid email' },
]);
}
});
checkout.render();React Example
import { useEffect, useRef, useState } from 'react';
import { PaypercutCheckout, CheckoutInstance, SdkEvent } from '@paypercut/checkout-js';
export function CheckoutWithForm({ checkoutId }: { checkoutId: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const checkoutRef = useRef<CheckoutInstance | null>(null);
const [email, setEmail] = useState('');
const [emailError, setEmailError] = useState('');
useEffect(() => {
if (!containerRef.current) return;
const checkout = PaypercutCheckout({
id: checkoutId,
containerId: containerRef.current,
ui_mode: 'embedded',
wallet_options: ['apple_pay', 'google_pay'],
validate_form: true,
});
// Handle wallet form validation
checkout.on(SdkEvent.FormValidation, ({ wallet }) => {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
if (isValid) {
setEmailError('');
checkout.completeFormValidation(wallet);
} else {
setEmailError('Please enter a valid email address');
checkout.failFormValidation(wallet, [{ code: 'invalid_email', message: 'Invalid email' }]);
}
});
checkout.on('success', (payload) => {
console.log('Payment successful:', payload.payment_method);
});
checkout.render();
checkoutRef.current = checkout;
return () => checkout.destroy();
}, [checkoutId, email]);
return (
<div>
{/* Your custom form fields */}
<div>
<label htmlFor='email'>Email</label>
<input
id='email'
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder='[email protected]'
/>
{emailError && <span style={{ color: 'red' }}>{emailError}</span>}
</div>
{/* Checkout iframe */}
<div ref={containerRef} />
</div>
);
}Timeout Behavior
If you don't respond to the form_validation event within 10 seconds, the SDK will:
- Emit an
errorevent with codeform_validation_timeout - Block the wallet payment from starting
This ensures the checkout doesn't hang indefinitely if the handler is not implemented.
When to Use
Use form validation when you have:
- Custom email/phone fields outside the checkout iframe
- Shipping address forms that must be filled before payment
- Terms & conditions checkboxes that must be accepted
- Any other merchant-side validation requirements
If you don't need to validate anything, you can simply auto-approve:
checkout.on('form_validation', ({ wallet }) => {
// No validation needed - always allow wallet to proceed
checkout.completeFormValidation(wallet);
});Paypercut Elements errors
PaypercutElements.submit(), paypercut.createPaymentMethod(), and each
Element's error event expose a stable
machine-readable code and a recoverable flag.
The accompanying message is diagnostic English text. Customer-facing copy
should be owned and localized by the merchant integration by mapping code to
its own translated message.
try {
await elements.submit();
const paymentMethod = await paypercut.createPaymentMethod({ elements });
await sendPaymentMethodToYourServer(paymentMethod);
} catch (error) {
const elementsError = error as ElementsSubmitError;
showLocalizedPaymentError(elementsError.code);
if (!elementsError.recoverable) {
disableThisPaymentMethod();
}
}The public ElementsErrorCode type contains the known codes below and an
open string member. This lets a newer hosted component introduce a safe code
without forcing the merchant to upgrade Paypercut.js first. Integrations must
always keep a generic translated fallback.
| Family | Codes | Meaning and integration response |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Payment details | elements_payment_details_incomplete, elements_card_fields_incomplete | Required secure fields are empty or incomplete. Keep the form visible, focus or highlight its fields, and ask the customer to complete it. |
| Payment details | elements_payment_details_invalid | One or more completed fields are invalid. Keep the form visible and ask the customer to correct the highlighted fields. |
| Payment Method creation | elements_payment_method_creation_failed | The secure component could not create the server-consumable Payment Method. Keep the form mounted; offer a retry or another payment method according to recoverable. |
| Provider tokenization | elements_tokenization_duplicate, elements_tokenization_timeout | The private provider tokenization exchange was duplicated or timed out. Do not retry blindly; follow recoverable and reconcile any active submission first. |
| Element lifecycle | elements_destroyed, elements_not_mounted, elements_not_ready, elements_reinitialized | The caller submitted before work started against an unavailable Element. Wait for ready or mount the required source. elements_reinitialized remains in the public compatibility union but is not used to make an in-flight submission safely retryable. |
| Controller/component initialization | elements_initialization_invalid, elements_locale_invalid, elements_controller_failed, elements_component_configuration_invalid, elements_component_connect_failed, elements_component_attach_failed, elements_component_not_ready, elements_provider_initialization_failed | The secure Elements surface could not initialize. Do not expose internal diagnostics; show a translated unavailable message and offer another payment method. |
| Elements session | elements_session_expired, elements_session_binding_mismatch, elements_session_verification_failed | The form's authoritative configuration is expired or cannot be verified. Recreate or reload the Elements group instead of blindly resubmitting. |
| Submission sequencing and concurrency | elements_submit_required, elements_submission_in_progress, elements_submit_in_progress, elements_test_preset_in_progress | Validate after the latest input change before creation, or wait for the operation that already owns the Element to settle. |
| Submission result | elements_submission_failed, elements_submit_failed | Submission failed with no more specific safe classification. Use the generic translated fallback and recoverable. |
| Ambiguous submission | elements_submit_indeterminate, elements_submit_invalid_response, elements_submit_timeout | The caller cannot safely infer whether the operation completed. Do not retry in the same Elements group. Reconcile server state, then create a new group only for a deliberate new attempt. |
| Action shell | elements_action_canceled, elements_action_channel_unavailable, elements_action_shell_timeout | The detached action was canceled or its trusted channel could not be established. Keep the payment form available and give translated retry/alternative guidance. |
| 3DS verification | `elemen
