Skip to main content
This guide covers upgrading @payrails/web-sdk from 5.x to 6.x: every breaking
change, before/after code for each, and how to verify the result. It assumes a
working 5.x integration. If an AI coding agent performs the migration for you,
point it at the agent runbook, which routes through
this guide.

Upgrade checklist

Work through these in order — later steps assume the earlier ones are done:
  1. Update the package: npm install @payrails/web-sdk@6.
  2. Add await to every Payrails.init(...) call (it now returns a Promise) and make the surrounding code async — see §1.
  3. Remove the manual stylesheet import (import '@payrails/web-sdk/payrails-styles.css') — it no longer resolves; styles load automatically; see §1.
  4. If your site sets a Content-Security-Policy, allow assets.payrails.io in script-src and style-src.
  5. Remove any Payrails.preloadCardForm() calls; the method is gone — see §2.
  6. Replace every events: {} callback bag with the typed .on() API — the bags are gone in v6; the one exception is onClientInitialized at Payrails.init — see §4.
  7. Update onClientInitialized handlers: the argument is now the plain execution response, and its helper methods moved to the payrails instance — see §3.
  8. Replace wallet availability callbacks (onGooglePayAvailable, onApplePayAvailable, onPaypalAvailable) with await button.isAvailable — see §5.
  9. Migrate styles options to appearance on every Payrails-drawn element — see §6. Provider-drawn wallet buttons keep their styles chrome options.
  10. Update TypeScript type imports — see §7.
  11. Re-test your checkout visually: the components ship a refreshed design — see §8.

1. Payrails.init is asynchronous

In v6 the npm package is a thin loader. Payrails.init(...) returns a Promise of the SDK instance: at init the SDK loads its full bundle and stylesheet from the Payrails CDN, pinned to the SDK version configured for your merchant account (with fallback to the latest release of the current major). Every merchant runs an SDK version compatible with their account configuration, without waiting for you to update the npm dependency.
Notes:
  • Stylesheet — the SDK injects its stylesheet together with the bundle. Remove the manual payrails-styles.css import: the file is no longer in the package and the import fails to resolve (the package exports map has no CSS subpath), so a leftover import is a build error, not a silent no-op.
  • Content-Security-Policy — the bundle and stylesheet load from assets.payrails.io; allow it in script-src and style-src.
  • Failure mode — if the bundle cannot load (blocked CDN, offline, timeout), init rejects with a PayrailsError (PAYRAILS_INIT_FAILED) after 15 seconds. Handle the rejection where you handle other init errors.
  • Async propagation — callers of your init code may need to become async too. In React, initialize inside an effect and store the instance in state; in Vue, initialize in an async mounted/onMounted hook.
  • Browser only — init needs a DOM to load the bundle; call it in the browser, not during server-side rendering. In SSR frameworks, run it in a client-only lifecycle hook (or behind a typeof window !== 'undefined' guard).

2. Payrails.preloadCardForm() is removed

The static Payrails.preloadCardForm() no longer exists — the npm package is now a loader, so before init there is nothing to preload from. Delete the call; the SDK bundle itself is fetched at init, and the secure card fields load when the card form mounts. There is no v6 equivalent for warming the card form ahead of time — if you used preloadCardForm for perceived performance, mount the card form earlier (hidden if necessary) instead:

3. onClientInitialized receives the execution response object

onClientInitialized remains a callback passed at init — it is the one event with no .on() form, because it fires during initialization, before a listener could be registered. It also fires again after each session refresh (see sessionExpired below). Two things changed about it: The argument is now the plain workflow execution response (WorkflowExecutionResponse, e.g. execution.id) instead of the execution class instance. The class instance’s helper methods moved to the payrails instance. If your handler called helpers on the argument, call them on the SDK instance instead:
Read anything else you used to pull off the class directly from the response object.

4. Typed .on() event API replaces events: {} callback bags

v6 removes the legacy events: {} callback bags from every element factory and from payrails.dropin() — passing one is a compile error, with no back-compat bridge. Subscribe with the typed .on(name, handler) API instead. .on() supports multiple listeners per event and returns an unsubscribe function:
Events live at two levels:
  • Instance — payrails.on(...) for session and payment-attempt events. Payment-attempt payloads carry executionId, paymentMethodCode, and action ('AUTHORIZE' | 'TOKENIZE').
  • Element — element.on(...) for events about one element (a card form, a button, the drop-in), where element is the object returned by payrails.cardForm(), payrails.paymentButton(), and so on.

Behavior differences from the legacy callbacks

  1. Canceling uses event.preventDefault(), not a boolean return. Callbacks that returned Promise<boolean> to cancel a flow are now cancelable events.
  2. Payment-attempt events are per-session, not per-element. In v5 each element had its own bag: applePayButton({ events: { onSuccess } }) fired only for that button. In v6 a single payrails.on('success', ...) fires for any element’s success in the session — card form, wallet buttons, drop-in, all of them. Filter with event.paymentMethodCode or event.action if you need per-method behavior:
  3. A thrown handler no longer blocks the payment. In v5, a gate callback (onRequestStart, onPaymentButtonClicked) that threw propagated as a payment failure. In v6 the SDK catches and logs the error and the flow continues. Blocking validation or fraud checks must call event.preventDefault() explicitly:
  4. sessionExpired handlers run serially. The SDK awaits each handler in registration order; a slow handler delays the session refresh. Do only refresh-adjacent work there.

sessionExpired can refresh the session

A handler may return fresh init options ({ version, data }); the SDK re- initializes from the first non-null result and re-runs onClientInitialized:

Instance events

Payload notes: failed carries the failure essentials in e.data ({ code?, message? }) rather than at the top level; buttonClicked adds bin? for card payments; deliveryAddressChanged replaces the v5 resolve-false-to-reject contract with preventDefault().

Element events

Full payload types and cancelation semantics: Events reference.

5. Wallet availability is a promise, not an event

Wallet availability is an environment check that settles once — modelling it as an event meant subscribers could race the check. In v6 every express payment button (googlePayButton, applePayButton, paypalButton) exposes readonly isAvailable: Promise<boolean> instead:
The promise never rejects — it resolves to false on any check-side failure. The instance-level checks payrails.isGooglePayAvailable(merchantName?) and payrails.isApplePayAvailable() also remain available.

6. styles becomes appearance

v6 replaces the per-component structured styles object with a single appearance option on every element the SDK draws itself. appearance.rules is plain CSS-like key/value: selectors on the outside, CSS declarations on the inside. Selectors target stable class names the SDK guarantees on the DOM; state variants live on BEM modifier classes.
Unlike the closed vocabulary of v5 styles keys, rules accepts any selector a browser supports — :focus-visible, ::placeholder, ::selection, @media, @supports.

Which options changed

Collect elementsinputStyles, labelStyles, and errorTextStyles still exist on createCollectElement’s options type but have no effect at runtime in v6—migrate them to appearance or your field styling silently disappears.

The class names on the DOM

Target these generic classes from appearance.rules: .payrails-input, .payrails-button, .payrails-dropdown, .payrails-label, .payrails-tile, .payrails-container, .payrails-row, .payrails-cell, .payrails-icon, .payrails-checkbox, .payrails-error, .payrails-text. State variants use BEM modifiers: Native pseudo-classes (:hover, :focus, :focus-visible, :disabled, :autofill, ::placeholder, ::selection) work anywhere they are valid. The --invalid and --valid classes clear while a field is focused, so a field being corrected does not show the error state. For a persistent invalid look on touched fields, key off --dirty:

Card form: field-by-field mapping

Per-field-type keys (styles.inputFields.CARD_NUMBER.*) have no direct equivalent — in practice the generic .payrails-input covers most needs. Nested widgets — the installments dropdown, address selector, and brand selector — each have their own slot on CardFormAppearance:
The card payment button is a sibling of the card form, not a child: pass its appearance to payrails.paymentButton({ appearance }) (standalone) or to DropinAppearance.cardPaymentButton (drop-in mode).

Collect elements: field-by-field mapping

Only the root { rules } is forwarded to each collect element’s iframe; nested widget keys are ignored (secure fields have no sub-widgets).

Drop-in: appearance keyed by building block

DropinAppearance mirrors the drop-in’s structure — root rules paint the container; each building block takes its own appearance under a matching key:
The full slot list and per-slot class contract: Appearance reference.

Provider-drawn buttons keep styles — but the drop-in path moved

Google Pay, Apple Pay, and PayPal buttons are drawn by the provider’s SDK, so CSS cannot reach them. Their chrome options (buttonColor/buttonType for Google Pay, type/style for Apple Pay, color/shape for PayPal) stay on the standalone element’s styles option, unchanged from v5. In drop-in mode the nesting moved: dropin.styles.googlePayButton (and equivalents) is gone; pass the same object under paymentMethodsConfiguration.<method>.styles instead:

Revolut Pay: styles → revolutOptions

The Revolut Pay button reads Revolut’s own branded config ({ theme, width, borderRadius }), not CSS. The field is renamed accordingly — standalone on genericRedirectButton and in the drop-in:
Non-Revolut redirect buttons style via appearance.rules on .payrails-generic-button like any other button.

Lean button: styles.button → appearance, styles.dialog → dialogCustomization

The Lean-hosted bank dialog reads Lean SDK config (theme colors, border radius) — appearance.rules cannot reach into Lean’s iframe, so that part moved to its own option:

Cascade behavior

SDK defaults live in the CSS layer @layer payrails-defaults; your appearance rules land in @layer payrails-appearance, which always wins over the defaults. Rules in your own external stylesheets are unlayered and beat both — if you previously targeted .payrails-* classes from your own CSS, that keeps working, but internal markup changed in the redesign (§8), so re-test every override and prefer moving it into appearance.rules. translations and fonts did not change shape.

7. TypeScript changes

  • PayrailsContainerType is removed along with the containerType option of CollectContainerOptions — secure-fields containers no longer come in two flavors. CollectContainerOptions now has containerId and fonts only. The container returned by payrails.collectContainer() keeps its exported FramesContainer type, unchanged from v5.
  • New appearance types are exported: Appearance, AppearanceRules, AppearanceDeclarations, CardFormAppearance, DropinAppearance.
  • RevolutPayStyles remains exported — it is now the type of revolutOptions.
  • Event types are exported for the .on() surface: PayrailsEvents, PayrailsEventName, PayrailsEventHandler, ElementEvents, ElementEventName, ElementEventHandler, PaymentAttemptContext, ActionRequiredEvent, and the per-event payload types.
These affect type annotations only; runtime behavior is covered by the sections above.

8. Refreshed visual design

The drop-in, card form, payment buttons, and result screens ship a refreshed design: a unified button style across payment methods, updated typography and spacing, and improved responsiveness in narrow containers. No code changes are required, but the rendered DOM and default styles have changed:
  • If you customized the checkout in v5, re-apply your intent through appearance (§6) and re-test each customized component.
  • If you override SDK styles with your own CSS selectors, expect breakage — internal class names and markup have changed. Re-test and update your overrides. §6 documents the supported class names and state modifiers.

Verify the migration

After migrating, confirm each of these:
  1. It compiles. Run your type-check/build. Leftover v5 usage (events: bags on element options, the CSS import, preloadCardForm, PayrailsContainerType) fails the build.
  2. Init resolves. The page loads the SDK bundle and stylesheet from assets.payrails.io (visible in the network tab) and await Payrails.init resolves without PAYRAILS_INIT_FAILED. Test with your production CSP if you have one.
  3. Elements render styled. The card form / drop-in mounts with the v6 design and your appearance rules applied — a completely unstyled form usually means styling was left on dead v5 options.
  4. A test payment fires your listeners. Complete a payment in the TEST environment and confirm your success/failed handlers fire (with the right paymentMethodCode filters if you pay with multiple methods).
  5. Gates still gate. If you migrated onRequestStart/onButtonClicked logic, confirm a rejected check actually blocks the payment — remember that throwing no longer blocks (§4).
  6. Session refresh works if you use sessionExpired: let a session expire (or force it) and confirm the SDK re-initializes and onClientInitialized fires again.

Last modified on September 30, 2026