@payrails/web-sdk from 5.x to 6.x: every breakingchange, 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:- Update the package:
npm install @payrails/web-sdk@6. - Add
awaitto everyPayrails.init(...)call (it now returns aPromise) and make the surrounding code async — see §1. - Remove the manual stylesheet import
(
import '@payrails/web-sdk/payrails-styles.css') — it no longer resolves; styles load automatically; see §1. - If your site sets a Content-Security-Policy, allow
assets.payrails.ioinscript-srcandstyle-src. - Remove any
Payrails.preloadCardForm()calls; the method is gone — see §2. - Replace every
events: {}callback bag with the typed.on()API — the bags are gone in v6; the one exception isonClientInitializedatPayrails.init— see §4. - Update
onClientInitializedhandlers: the argument is now the plain execution response, and its helper methods moved to thepayrailsinstance — see §3. - Replace wallet availability callbacks (
onGooglePayAvailable,onApplePayAvailable,onPaypalAvailable) withawait button.isAvailable— see §5. - Migrate
stylesoptions toappearanceon every Payrails-drawn element — see §6. Provider-drawn wallet buttons keep theirstyleschrome options. - Update TypeScript type imports — see §7.
- 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.
- Stylesheet — the SDK injects its stylesheet together with the bundle.
Remove the manual
payrails-styles.cssimport: the file is no longer in the package and the import fails to resolve (the packageexportsmap 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 inscript-srcandstyle-src. - Failure mode — if the bundle cannot load (blocked CDN, offline, timeout),
initrejects with aPayrailsError(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/onMountedhook. - Browser only —
initneeds 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 atypeof 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:
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:
- Instance —
payrails.on(...)for session and payment-attempt events. Payment-attempt payloads carryexecutionId,paymentMethodCode, andaction('AUTHORIZE'|'TOKENIZE'). - Element —
element.on(...)for events about one element (a card form, a button, the drop-in), whereelementis the object returned bypayrails.cardForm(),payrails.paymentButton(), and so on.
Behavior differences from the legacy callbacks
-
Canceling uses
event.preventDefault(), not a boolean return. Callbacks that returnedPromise<boolean>to cancel a flow are now cancelable events. -
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 singlepayrails.on('success', ...)fires for any element’s success in the session — card form, wallet buttons, drop-in, all of them. Filter withevent.paymentMethodCodeorevent.actionif you need per-method behavior: -
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 callevent.preventDefault()explicitly: -
sessionExpiredhandlers 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:
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.
styles keys, rules accepts any selector a
browser supports — :focus-visible, ::placeholder, ::selection, @media,
@supports.
Which options changed
Collect elements
inputStyles, 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 fromappearance.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:
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:
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:
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
PayrailsContainerTypeis removed along with thecontainerTypeoption ofCollectContainerOptions— secure-fields containers no longer come in two flavors.CollectContainerOptionsnow hascontainerIdandfontsonly. The container returned bypayrails.collectContainer()keeps its exportedFramesContainertype, unchanged from v5.- New appearance types are exported:
Appearance,AppearanceRules,AppearanceDeclarations,CardFormAppearance,DropinAppearance. RevolutPayStylesremains exported — it is now the type ofrevolutOptions.- Event types are exported for the
.on()surface:PayrailsEvents,PayrailsEventName,PayrailsEventHandler,ElementEvents,ElementEventName,ElementEventHandler,PaymentAttemptContext,ActionRequiredEvent, and the per-event payload types.
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:- It compiles. Run your type-check/build. Leftover v5 usage (
events:bags on element options, the CSS import,preloadCardForm,PayrailsContainerType) fails the build. - Init resolves. The page loads the SDK bundle and stylesheet from
assets.payrails.io(visible in the network tab) andawait Payrails.initresolves withoutPAYRAILS_INIT_FAILED. Test with your production CSP if you have one. - Elements render styled. The card form / drop-in mounts with the v6 design
and your
appearancerules applied — a completely unstyled form usually means styling was left on dead v5 options. - A test payment fires your listeners. Complete a payment in the
TESTenvironment and confirm yoursuccess/failedhandlers fire (with the rightpaymentMethodCodefilters if you pay with multiple methods). - Gates still gate. If you migrated
onRequestStart/onButtonClickedlogic, confirm a rejected check actually blocks the payment — remember that throwing no longer blocks (§4). - Session refresh works if you use
sessionExpired: let a session expire (or force it) and confirm the SDK re-initializes andonClientInitializedfires again.