npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

npm version License: MIT Bundle Size TypeScript


Table of Contents


Installation

Via NPM

npm install @paypercut/checkout-js

Via Yarn

yarn add @paypercut/checkout-js

Via PNPM

pnpm add @paypercut/checkout-js

Via 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:

  1. paypercut.elements(options) creates a group.
  2. elements.create(type) creates a visual Element; mounting it only renders secure collection UI.
  3. await elements.submit() validates and prepares the current group. It does not create a Payment Method or confirm a payment.
  4. await paypercut.createPaymentMethod({ elements, params? }) creates an opaque Payment Method.
  5. Send only the Payment Method ID and merchant-owned order data to your server.
  6. If the server returns an opaque client secret, call confirmPayment() (or its advanced alias handleNextAction()) 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 before render() 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 normalized ApiErrorPayload when provided by Hosted Checkout.
  • processing: non-terminal event. Use it to disable close buttons or show "do not close" messaging, then clear that UI on success, error, cancel, or expired.
  • threeds_error: forwards payload.error when available; otherwise the raw message data.
  • three![img.png](img.png)ds_* non-error events: payload is forwarded as-is from Hosted Checkout (shape may evolve).

Form Validation for Wallet Payments

How It Works

  1. User clicks Apple Pay or Google Pay button in the checkout
  2. SDK emits form_validation event to your code
  3. You validate your form and call either:
    • completeFormValidation(wallet) - allows wallet to proceed
    • failFormValidation(wallet, errors) - blocks wallet, you show your own errors
  4. 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:

  1. Emit an error event with code form_validation_timeout
  2. 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