Skip to main content

Revup Unified SDK 1.2

Reference release: 1.2.16 — the last patch of the 1.2 line. Status: maintained. New integrations should start on 1.3. Existing 1.2 integrations keep working; when you are ready to move, follow Migrating 1.2 → 1.3. Coming from 1.1? Start at Migrating from 1.1 to 1.2. Coming from 1.0? Apply 1.0 → 1.1 first, then this page.

Everything on this page describes 1.2.x only, and it is complete on its own — the full option reference and the full event contract are here, not inherited from 1.0 or 1.1.


Contents​

  1. What the SDK does
  2. Quick start
  3. Loading the SDK script
  4. The container
  5. RevupConfig — constructor
  6. RevupOptions — init()
  7. form — card form options
  8. apms — payment zones
  9. appearance — checkout styling
  10. userInfo — payer email
  11. Languages and right-to-left
  12. Lifecycle: cleanup and re-initialization
  13. Session API (Revup.mount())
  14. Event contract
  15. Environments, URLs and CSP
  16. Going live — checklist

Migrating from 1.1 to 1.2​

1.2 is a small migration. One option was removed; everything else is additive.

1. Change the loader path​

- src="https://test.hosted.revuppayments.com/payments-widget/sdk/1/1/loader.js"
+ src="https://test.hosted.revuppayments.com/payments-widget/sdk/1/2/loader.js"

The integrity hash changes with the minor. Request the 1.2 value from RevUp.

2. Nothing else is required​

RevupConfig, RevupOptions, apms zones and entries, appearance, userInfo, language, destroy() and the Session API are all unchanged. An existing 1.1 integration works on 1.2 once you have done step 1 and step 3.

3. Removed: form.selfHostedDomain​

  form: {
- selfHostedDomain: false,
collapsed: false,
}

form.selfHostedDomain (and the equivalent key under apms) is no longer a recognised option. Passing it is rejected at runtime and logged as:

selfHostedDomain is not a valid option, default value was set instead.

It is a log message, not a crash — but the option no longer does anything.

Why: in 1.2 the card form's only job is to collect, validate and encrypt card data. Everything after the payer presses Pay — the payment call and the 3-D Secure / device-fingerprint (DDC) exchange — is owned by the SDK running on your page. DDC therefore always runs first-party, in your checkout's browser context, which is what some gateways require. There is no merchant toggle any more, and no configuration in which DDC runs inside the iframe.

If you were passing selfHostedDomain: false: that is now the only behaviour, so just delete the key. If you were passing true or relying on the default: your 3-D Secure flow now runs on your page instead of inside the iframe. Re-test 3-D Secure end to end in sandbox, and make sure your CSP still allows the challenge origins.

4. Behaviour change — form.textDirection: 'rtl' now does something​

In 1.0 and 1.1 this option was accepted and stored but nothing read it. In 1.2 it actually mirrors the layout. If you were passing 'rtl' speculatively, your layout will change. See Right-to-left layout.

5. Behaviour change — two additions to validation.changed​

  • data.invalidFields is new: a string[] of the field keys currently failing validation. Existing handlers that only read isValid / isCalling are unaffected.
  • After every payment attempt settles, the SDK emits one extra validation.changed carrying the reset state (isValid: false, isCalling: false, invalidFields: []). If your handler treats validation.changed as "the payer edited something", it will now also fire once right after a terminal payment event. Drive your button state from the payload, not from the fact that the event fired.

6. Appearance — one rule target changed meaning​

In 1.1 .CardBrandSlot painted a single rounded chip behind the brand marks. In 1.2 it is a layout-only container: it arranges the marks with a gap and paints no surface of its own, and each brand mark carries its own chip instead. If you had styled .CardBrandSlot as a chip surface, a background-color there now fills the whole strip area including the gaps. Re-check that rule.

The target list itself is unchanged — .CardBrandSlot and .CardBrandIcon already existed in 1.1.

7. New capabilities you can adopt (all optional)​

  • form.cardBrandIconPlacement: 'button' moves the brand marks from the card-number field onto the card CTA. See Card-brand icon placement.
  • form.textDirection: 'rtl' for Arabic and other RTL storefronts.

Migration checklist​

  • [ ] Loader path 1/1 → 1/2, new integrity hash
  • [ ] form.selfHostedDomain removed from your config
  • [ ] 3-D Secure re-tested end to end in sandbox (it now runs on your page)
  • [ ] CSP re-checked for the 3-D Secure challenge origins
  • [ ] form.textDirection removed unless you actually want RTL mirroring
  • [ ] validation.changed handler tolerates the post-payment reset event
  • [ ] .CardBrandSlot appearance rule re-checked, if you had one

What's new in 1.2​

AreaChange
3-D SecureDevice-fingerprint (DDC) requests always run first-party on your page. form.selfHostedDomain removed
PaymentsThe card form hands off an encrypted payload and the SDK owns the payment call. Every submit gets exactly one terminal event; duplicate submits while one is in flight are ignored
Eventsvalidation.changed now carries data.invalidFields
EventsA validation.changed reset is emitted after every payment attempt settles
Layoutform.textDirection: 'rtl' mirrors the whole widget for the first time
Layoutform.cardBrandIconPlacement: 'input' | 'button' (from 1.2.2)
Appearance.CardBrandSlot is now a layout-only container instead of a chip surface
Payment methodsideal can be served natively — your own PSP instead of Stripe — as a one-click button, recurring or not. Which provider serves it is decided per order; see How ideal renders. pix and sepa_debit are always Stripe-served (1.2.3)
EventsNew validation.error event reports a configured value outside the allowed set for language, apms or appearance, instead of failing silently (from 1.2.5)
Layoutform.showPopularBadge controls the "POPULAR" pill on the card row (from 1.2.6). Hidden by default from 1.2.11 — set true to show it
Payment methodsklarna, affirm and bizum, served through Stripe as inline accordion rows (from 1.2.15)

1. What the SDK does​

RevUp is a single hosted script that renders a complete payment block inside one element of your checkout page. It orchestrates:

  • A credit / debit card form, hosted in a sandboxed iframe on RevUp's domain, so card data never touches your page. It collects, validates and encrypts the card details and hands the encrypted payload to the SDK; it never talks to a payment endpoint itself.
  • Alternative payment methods (APMs) — Apple Pay, Google Pay, PayPal, Stripe Link, Amazon Pay, iDEAL, SEPA Direct Debit, Pix, Klarna, Affirm and Bizum — arranged across three layout zones, limited to the methods enabled for your merchant account.

The script exposes one global constructor, Revup, and reports everything that happens through one native DOM event, onRevupMessage.

Checkout layout​

┌─ container ─────────────────────────────────┐
│ primary zone │
│ [ Apple Pay ] [ Google Pay ] [ PayPal ]│
│ ▸ Credit or debit card (POPULAR) │ ← card accordion
│ │
│ ──────────── Or pay with ───────────── │
│ secondary zone (2-column grid) │
│ [ Amazon Pay ] [ Stripe Link ] │
│ │
│ + 3 more payment methods ▾ │ ← other zone
│ │
│ 🔒 This is a secure 256-bit encrypted … │
└─────────────────────────────────────────────┘
ZonePositionMax entriesHidden when
primaryTop of the block, together with the card accordion row4Never — the card row can still render when form.enabled is true
secondaryBelow primary, after an "Or pay with" divider, in a responsive 2-column grid (single column on narrow containers)4Empty, or none of its methods are available
otherBelow secondary, behind a "+ N more payment methods" toggleunlimitedEmpty, or none of its methods are available

Zone caps are applied before availability filtering. Configuring 6 primary entries drops entries 5 and 6 outright — even if entries 1–4 later turn out to be unavailable.

De-duplication. A method may appear in only one zone. If the same name is listed in several zones, the highest-priority placement wins: primary > secondary > other.

Availability filtering. Every configured name is intersected with the methods your merchant account has enabled. A method that is not enabled is silently skipped and produces no event at all.

Accordion behaviour. Only one expandable row is open at a time across the whole checkout. Opening the card accordion collapses an open inline payment form, and vice versa. Pressing Escape, or a pointer-down outside the row, collapses it.

Reactive layout. If a method fails after the first render (vendor SDK load error, capability check, no methods reported), the SDK removes that slot, updates the "+ N more" count and hides secondary / other if they become empty. It also dispatches apm.unavailable. No merchant handling is required.

Height changes. Expanding the card accordion or an inline payment form changes the container's height. Make the surrounding layout tolerant of that.


2. Quick start​

<div id="revup-container"></div>

<script
type="text/javascript"
charset="utf-8"
src="https://test.hosted.revuppayments.com/payments-widget/sdk/1/2/loader.js"
crossorigin="anonymous"
integrity="sha512-…">
</script>
window.addEventListener('onRevupSDKLoaded', async () => {
const session = await Revup.mount({
merchantDomain: window.location.host,
apiKey: 'YOUR_PUBLIC_API_KEY',
containerId: 'revup-container',
orderId: 'ORDER_ID_FROM_YOUR_BACKEND',
language: 'en-US',
form: { collapsed: false, showZipCode: true },
apms: {
primary: [
{ name: 'apple_pay', capabilityDetection: true },
{ name: 'google_pay', capabilityDetection: true },
{ name: 'paypal', type: 'checkout' },
],
secondary: [{ name: 'amazon_pay' }, { name: 'stripe_link' }],
other: [{ name: 'ideal' }, { name: 'sepa_debit' }, { name: 'pix' }],
},
onMessage: (event) => console.log(event.type, event),
});

// Later — update in place, no teardown
await session.set({ orderId: nextOrderId });
await session.set({ appearance: { theme: 'dark' } });
});

Also supported — the constructor flow​

const revup = new Revup({
merchantDomain: window.location.host,
apiKey: 'YOUR_PUBLIC_API_KEY',
orderId: 'ORDER_ID',
});
const stopListening = revup.onRevupMessage((event) => { /* … */ });
await revup.init({ containerId: 'revup-container', orderId: 'ORDER_ID' });

Order of operations that must be respected either way:

  1. Create the order on your backend and get its orderId.
  2. Load the script and wait for onRevupSDKLoaded.
  3. Make sure the container element exists in the DOM.
  4. Attach your event listener, then mount / init.

3. Loading the SDK script​

https://<host>/payments-widget/sdk/<major>/<minor>/loader.js
EnvironmentLoader URL (1.2 line)
Sandboxhttps://test.hosted.revuppayments.com/payments-widget/sdk/1/2/loader.js
Livehttps://hosted.revuppayments.com/payments-widget/sdk/1/2/loader.js

The two path segments before loader.js are the SDK's major and minor. Patch releases inside a minor (1.2.1 → 1.2.2) do not change your script URL or your integrity hash — the loader resolves the current patch on every page load. You only edit your <script> tag when RevUp announces a new minor or major.

Use integrity in production, paired with crossorigin="anonymous". The SRI hash is issued per environment and per minor line; request the current value from RevUp rather than copying it from an example.

Integrity hash

The integrity value is not published in this documentation. You may request it from RevUp by escalating a support ticket.

Dynamic loading (React, Vue, …)​

const script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.src = REVUP_SDK_URL;
script.crossOrigin = 'anonymous';
if (INTEGRITY_HASH) script.integrity = INTEGRITY_HASH;
document.head.appendChild(script);

The ready signal​

The loader fires onRevupSDKLoaded on window once Revup is available:

window.addEventListener('onRevupSDKLoaded', () => {
// window.Revup is now defined
});

4. The container​

<div id="revup-container"></div>

The element must already exist in the DOM when you mount. If it does not, the SDK throws:

Container with id: <containerId> not found, please check if the container is present in the DOM

The SDK takes ownership of the element: it clears the contents and writes its own class name, layout attributes and inline sizing. Do not target it by class name and do not put your own children inside it.

A localized secure-payment footer ("This is a secure 256-bit encrypted payment. You're safe.") is always appended inside the container. It is not configurable.


5. RevupConfig — constructor​

Only needed for the constructor flow. With Revup.mount() you pass these fields directly to mount().

const revup = new Revup({
merchantDomain: window.location.host,
apiKey: 'YOUR_PUBLIC_API_KEY',
orderId: 'ORDER_ID',
// version: '2',
});
PropertyTypeRequiredDescription
merchantDomainstringYesYour merchant domain, e.g. window.location.host or your production domain. Must match a domain registered for your merchant account.
apiKeystringYesPublic API key for the environment (sandbox or live).
orderIdstringYes in TypeScript / optional at runtimeDeprecated here — prefer passing it to init(). The RevupConfig type still declares it as required, so TypeScript integrations must supply it even when they also pass it to init(); plain-JS integrations may omit it. When present in both, the init() value wins.
versionstringNoAPI version path segment. Defaults to the SDK's built-in version (currently "2"). Unrelated to the SDK version in the loader URL.

If neither the constructor nor init() supplies an orderId, init() throws:

Missing orderId: pass `orderId` to Revup#init() (or to the Revup constructor for backwards compatibility).

Public methods​

MethodReturnsDescription
Revup.mount(options)Promise<RevupSession>Static. Recommended entry point. See Session API.
Revup.getAvailableLanguages()string[]Static. All supported language codes. Callable before mounting.
revup.init(options)Promise<void>Builds the layout, initializes payment methods, loads the card form.
revup.destroy()voidFull teardown. See Lifecycle.
revup.getApmsAvailable()Promise<string[]>Payment methods enabled for your merchant account. Safe to call before init().
revup.onRevupMessage(handler)() => voidSubscribes to SDK events; returns a cleanup function.

getApmsAvailable()never rejects. On a configuration or HTTP failure it dispatches apm.unavailable and resolves to []. An empty array therefore means either "no methods enabled" or "the lookup failed" — listen for apm.unavailable to tell them apart. It also tells you what your account has enabled, not what will render on the current device.

onRevupMessage(handler, target) accepts a second argument, but SDK events are only ever dispatched on document; any other target is coerced to document and a warning is logged.


6. RevupOptions — init()​

PropertyTypeRequiredDefaultDescription
containerIdstringYes—id of the element that will host the payment UI.
orderIdstringNo*value from the constructor*Required on the first call unless supplied to the constructor. When present in both, this value wins. Passing a different value to a later init() re-keys the per-order caches and re-fetches the order and its per-method configuration. Subsequent calls may omit it to keep the previous value — the SDK reuses it silently, with no warning.
languagestringNobrowser language, then 'en-US'BCP 47 language code. See Languages.
appearanceobjectNo{ theme: 'default' }Checkout styling. See appearance.
formobjectNosee belowCard form options. See form.
apmsobjectNo{}Payment zones. See apms.
userInfoobjectNo{}Payer information. See userInfo.

init() performs a full build: every payment method is destroyed and re-initialized, and the card iframe is recreated, so anything the payer had already typed is lost. Use the Session API when you need in-place updates.


7. form — card form options​

form: {
enabled: true,
collapsed: false,
showZipCode: true,
cardBrands: ['visa', 'mastercard', 'american_express', 'discover'],
cardBrandIconPlacement: 'button',
textDirection: 'ltr',
}
PropertyTypeDefaultDescription
enabledbooleantrueWhen false, the card accordion row and the card iframe are not rendered at all — an APM-only checkout.
collapsedbooleantruetrue = the card accordion starts closed; false = it starts expanded. Independent of how many APMs render.
showZipCodebooleanfalseShows the ZIP / postal-code field.
cardBrandsCardBrand[]['visa', 'mastercard']Brand marks shown before a brand is detected — see below.
cardBrandIconPlacement'input' | 'button''input'Where the brand marks render. Available from 1.2.2. Any other value is rejected (and logged), falling back to 'input'. See Card-brand icon placement.
showPopularBadgebooleanfalseWhether the "POPULAR" pill renders on the card accordion row. Available from 1.2.6; hidden by default from 1.2.11 — set true to show it. A non-boolean value is rejected (and logged), falling back to false.
textDirectionstring'ltr'Text direction for the whole RevUp widget. Only the exact lowercase 'rtl' mirrors; any other string (including 'RTL') resolves to 'ltr'. Not derived from language. See Right-to-left layout.
userInfoobject{}Same shape as top-level userInfo; the top-level form is preferred.

There is no form.styles, form.theme, form.buttonStyles or form.selfHostedDomain in 1.2. Visual configuration goes through appearance; 3-D Secure has no merchant toggle.

Option validation​

Every key inside form is type-checked at runtime. A key with the wrong type — or a key the SDK does not recognise — is rejected, the default is used instead, and a message is written to the browser console:

<key> is not a valid option, default value was set instead.

userInfo is validated to the depth the form actually reads, so a typo like email: { require: true } is rejected with the same message rather than silently never rendering the field. Validation never throws and never blocks the checkout.

cardBrands​

Brand marks shown before a brand has been detected. Order is preserved as given, and only the first N entries are shown — the tail is dropped, so put the brands you most want visible first. An empty array or an omitted value falls back to ['visa', 'mastercard']. An unrecognised value renders the generic card mark rather than failing.

Supported brands: visa, mastercard, american_express, unionpay, jcb, discover, diners_club, maestro, visa_electron, vpay, rupay, elo, elo_new, hipercard, verve, troy, mir, cartes_bancaires, girocard, dankort, bancontact, interac, bc_card, dinacard, cabal, napas, mada, knet, benefit.

Card-brand icon placement​

Available from 1.2.2.

With the default cardBrandIconPlacement: 'input', the marks appear inside the card-number field (up to 4; 2 on very narrow layouts).

With 'button' they appear on the card CTA instead:

  • Card number empty, not yet typed, or after a reset → the CTA strip shows the first N brands of cardBrands, in your order.
  • A recognised brand → the strip collapses to that single mark, live as digits are typed.
  • Digits entered but the brand is unrecognised → a single generic card mark.
  • Clearing the field, or a reset after a payment response, restores the default strip.
  • N is measured, not a breakpoint: it steps 4 → 3 → 2 → 1 as the button narrows and grows back as it widens — never below 1, never above min(4, cardBrands.length).
  • The card-number field renders no brand icon at all in this mode.
  • The CTA's own left-hand generic card glyph is present in both modes and is not a brand mark.
  • The strip is decorative (aria-hidden); the button's accessible name is unchanged.
form: {
cardBrandIconPlacement: 'button',
cardBrands: ['visa', 'mastercard', 'american_express', 'discover'],
}

Appearance targets: .CardBrandSlot is the strip container — layout only, it paints no surface of its own, so a background-color there fills the whole strip area including the gaps. .CardBrandIcon matches every brand mark in the strip and the CTA's own generic glyph, in both placement modes; that glyph paints fill: currentColor and carries no chip, so avoid giving .CardBrandIcon a light background-color unless you also restyle the CTA.

Right-to-left (RTL) layout​

New in 1.2. With form.textDirection: 'rtl' the SDK mirrors both the hosted card form (labels, inputs, caret, CVC tooltip, error text) and the parent-page widget chrome (the card CTA and its badge, the payment zones, the "Or pay with" divider, the footer). Brand logos are never flipped — direction reorders layout, not artwork. Mirroring applies to APM-only layouts too, even when form.enabled is false.

Scoping. The direction is applied as a dir attribute on the element you name in containerId, and on the SDK's own iframe document. No dir attribute is ever written to your <html> or <body>. The SDK always writes an explicit direction, so a RevUp widget dropped into a storefront that is itself dir="rtl" stays left-to-right unless you set form.textDirection: 'rtl' — direction is never inherited from the host page.

languagedoes not set direction. language: 'ar-SA' translates the strings only; pair it with form.textDirection: 'rtl'.

await revup.init({
containerId: 'revup-container',
orderId,
language: 'ar-SA',
form: {
textDirection: 'rtl', // required — 'ar-SA' alone does not mirror the layout
collapsed: false,
},
});

Which form options update in place​

cardBrandIconPlacement, cardBrands, collapsed, showPopularBadge and textDirection are reconciled without re-initializing the checkout when you change them through RevupSession.set(patch): payment methods are not torn down and the card iframe is not reloaded, so data the payer already typed is preserved.

Calling revup.init() again always performs a full rebuild instead. form.enabled is structural and forces a full re-init on either path. Changing cardBrands alone while a brand is currently detected is deferred — the detected mark keeps showing, and the new defaults appear once the field is cleared.


8. apms — payment zones​

apms: {
primary: [
{ name: 'apple_pay', color: '#111111', capabilityDetection: true },
{ name: 'google_pay', color: 'white', capabilityDetection: true },
{ name: 'paypal', type: 'checkout', color: 'blue' },
],
secondary: [
{ name: 'amazon_pay' },
{ name: 'stripe_link' },
],
other: [
{ name: 'ideal' },
{ name: 'sepa_debit' },
{ name: 'pix' },
],
}
PropertyTypeDescription
primaryApmEntry[]Rendered above / alongside the card accordion. Max 4.
secondaryApmEntry[]2-column grid below, after an "Or pay with" divider. Max 4. Hidden when empty. Light-surface default colours.
otherApmEntry[]Behind a "+ N more payment methods" toggle. Unlimited. Hidden when empty. Light-surface default colours.

ApmEntry​

FieldTypeRequiredDescription
namestringYesThe payment method identifier — see the list below.
typestringNoVendor button style variant. Ignored by ideal, pix, sepa_debit, klarna, affirm and bizum — none of them renders a vendor button.
colorstringNoVendor button colour — see Colours.
capabilityDetectionbooleanNoWhen true, run a device/browser support check before rendering, hiding the button if unsupported. Only honoured by Apple Pay and Google Pay; ignored by everything else. When you omit it, the SDK falls back to the capabilityDetection flag in your account's payment-method configuration — detection is disabled only when neither is set. If the check fails, the SDK dispatches apm.unavailable.

Valid name values​

apple_pay, google_pay, paypal, stripe_link, amazon_pay, ideal, sepa_debit, pix, klarna, affirm, bizum.

  • credit_card is not valid here — the card flow is configured through form.
  • Use sepa_debit, not sepa, for SEPA Direct Debit.
  • amazon_pay only renders when your account routes the order through Stripe.
  • getApmsAvailable() returns the methods your account has enabled.

How each method renders​

MethodRendering
apple_pay, google_payNative wallet button, opening the device payment sheet. When your merchant configuration routes them through Stripe, they open a Stripe Express Checkout sheet instead.
paypalPayPal button opening the PayPal popup. There is no page redirect — the SDK confirms in the background and dispatches payment.success.
stripe_link, amazon_payStripe Express Checkout button opening a Stripe payment sheet.
sepa_debit, pix, klarna, affirm, bizumAn inline Stripe Payment Element accordion row — see Accordion rows below. klarna, affirm and bizum send the payer to the provider's own page to authorise the payment, and back to the return URL RevUp supplies with the payment.
idealAn accordion row or a one-click button, depending on the order — see How ideal renders below.

Accordion rows​

An accordion row is collapsed by default and expands to a form with its own Pay Now button. It participates in the same one-open-at-a-time behaviour as the card accordion: pressing Escape or clicking outside collapses it.

How ideal renders​

ideal is the one method whose appearance is decided per order, by the provider your backend returns with it. You do not choose it from the integration — you enable ideal once, and the SDK renders the matching shape. Whether the order is recurring makes no difference: from 1.2.13 the native shape is the one-click button in both cases.

providerWhat rendersAlso dispatches
stripeStripe Payment Element accordion row—
worldpay, or no provider at allA one-click button — RevUp's own UI against your own backend, not routed through Stripe. The same button whether or not the order is recurring—
any other non-empty valueNothing: ideal is not rendered and the SDK dispatches apm.unavailable, collapsing the slotapm.unavailable

An unrecognized provider — a typo, a renamed provider, a PSP never onboarded — fails closed on purpose. Apple Pay and Google Pay integrate the same way whatever provider sits behind them, but ideal served natively and ideal served through Stripe pay through different rails, so the SDK will not guess a rail from a value it cannot interpret.

sepa_debit, pix, klarna, affirm and bizum have no native variant: they are always Stripe-served.

Button style variants (type)​

MethodAccepted values
apple_payadd-money, book, buy, check-out, continue, contribute, donate, order, pay, plain, reload, rent, set-up, subscribe, support, tip, top-up
google_paybook, buy, checkout, donate, order, pay, plain, subscribe
paypalpaypal, checkout, buynow, pay, installment
stripe_link, amazon_pay, ideal, pix, sepa_debit, klarna, affirm, bizumnot applicable — the value is ignored

An unrecognised value falls back to the vendor default and is reported; the button still renders.

Colours​

MethodAccepted color
apple_payAny valid CSS colour (e.g. #1f6feb, rgb(20,20,20)), or the presets black, white, white-outline. When omitted in primary, the SDK picks black or white from the luminance of the backgroundColor appearance variable, defaulting to black when that variable is unset. It does not read your page's real background.
google_paydefault, black, white. An invalid value logs a warning and falls back to default.
paypalgold, blue, silver, white, black. An invalid value logs a warning and falls back to the default.
stripe_link, amazon_pay, pix, sepa_debit, klarna, affirm, bizumNot customizable — the value is ignored. pix, sepa_debit, klarna, affirm, bizum render an inline form, styled by the Appearance API.
idealwhite, dark — a first-party style rather than a vendor preset, applied to the one-click button. Honoured only when ideal is served natively (how ideal renders); ignored on the Stripe-served path. An unrecognized value logs a warning and falls back to white.

Zone defaults. Entries that appear only in secondary or other and have no explicit color receive light-surface defaults: Apple Pay white-outline, Google Pay white, PayPal white, and native ideal white. primary entries never receive this default and keep their vendor look. An explicit color always wins, in every zone.

One exception, by method. Native ideal defaults to white in every zone, primary included — its one-click button carries a fixed magenta iDEAL mark, designed for a light surface. Set color: 'dark' explicitly if you want the accent treatment; an explicit color still wins over this per-method default.


9. appearance — checkout styling​

appearance is the single styling surface. It styles both the hosted card form and the parent-page chrome (card accordion, payment zones, divider, footer).

appearance: {
theme: 'default',
variables: {
fontFamily: 'Inter, system-ui, sans-serif',
backgroundColor: '#ffffff',
submitButtonBackgroundColor: '#306de5',
submitButtonTextColor: '#FFFFFF',
submitButtonHeight: '44px',
submitButtonBorderRadius: '16px',
inputBorderRadius: '8px',
},
rules: {
'.InputWrapper--focused': { 'border-width': '2px' },
},
}
KeyTypeDescription
theme'default' | 'dark' | 'flat' | 'minimal'Preset used as the base. Defaults to 'default'.
variablesobjectHigh-level design tokens that override the preset.
rulesobjectFine-grained CSS overrides, keyed by an allowlisted element target.

Variables​

Variable names follow <domain><Sub?><Property><State?> in camelCase, e.g. cardButtonBorderColorHover, submitButtonOpacityDisabled. State suffixes are Hover, Focus, Active, Disabled, Invalid, Open; no suffix means the default state.

Grouped by domain:

DomainWhat it stylesExamples
GlobalTypography and motion across every surfacefontFamily, fontWeightRegular, fontWeightBold, disableAnimations, transitionDuration
ContainerThe RevUp block on your pagebackgroundColor, textColor, width, height, maxWidth, padding, borderRadius, justifyContent, alignItems
Form slotThe wrapper around the card iframeformBackgroundColor, formPadding, formBorderRadius
Payment methodsShared row surface for every methodpaymentMethodsContainerPadding, primaryGridGap, paymentMethodHeight, paymentMethodBorderWidth, paymentMethodBorderRadius, paymentMethodBackgroundColor, paymentMethodBorderColor, paymentMethodBoxShadow
Card accordionThe "Credit or debit card" headercardButtonBackgroundColor, cardButtonTextColor, cardButtonBorderColor, cardButtonBorderWidth, cardButtonBorderRadius, cardButtonFontSize, cardButtonFontWeight, cardButtonPadding, and the …Hover / …Open variants
Card badgeThe "POPULAR" pill — not rendered unless form.showPopularBadge is explicitly true, so by default these tokens have nothing to stylecardBadgeBackgroundColor, cardBadgeTextColor, cardBadgeFontSize, cardBadgeFontWeight, cardBadgeLetterSpacing, cardBadgeBorderRadius, cardBadgePadding, and the …Open variants
Field / labelForm rows and labels inside the iframefieldMinHeight, expiryCvcRowGap, labelTextColor, labelFontSize, labelFontWeight, labelTextColorFocus, labelTextColorInvalid
InputCard inputsinputHeight, inputFontSize, inputPadding, inputBorderColor, inputBorderWidth, inputBorderRadius, inputBackgroundColor, inputTextColor, inputBoxShadow, inputPlaceholderColor, plus …Hover, …Focus, …Invalid, …Disabled variants, and inputAdornmentColor
ErrorField error texterrorTextColor, errorFontSize, errorFontWeight
Submit buttonThe card form's pay buttonsubmitButtonHeight, submitButtonBorderRadius, submitButtonBackgroundColor, submitButtonTextColor, submitButtonFontSize, submitButtonFontWeight, and the …Hover, …Active, …Disabled, submitButtonOpacityDisabled variants
Skeleton / overlayLoading placeholders and scrimsskeletonBackgroundColor, skeletonShimmerColor, skeletonAnimationDuration, overlayLoadingBackgroundColor, overlayDisabledBackgroundColor
Secondary zoneThe "Or pay with" divider and gridsecondaryGridGap, secondaryDividerColor, secondaryDividerThickness, secondaryDividerLabelTextColor, secondaryDividerLabelBackgroundColor, secondaryDividerLabelFontSize, secondaryDividerLabelFontWeight, secondaryDividerLabelPadding
Other zoneThe "+ N more payment methods" toggleotherGridGap, otherToggleTextColor, otherToggleTextColorHover, otherToggleFontSize, otherToggleFontWeight

Values are CSS strings, not numbers: '44px', '#306de5', '200ms'. Numeric weights (fontWeightRegular, …FontWeight) and submitButtonOpacityDisabled are numbers. Payment button weight. cardButtonFontWeight and submitButtonFontWeight are not set by any
preset: both buttons render at fontWeightBold (700) unless you set the token yourself. Set either token explicitly if you want a lighter weight.

Focus ring. From 1.2.10, inputBoxShadowFocus also drives how much lateral room the field reserves for its focus ring, so a wider ring is never clipped by the row edge. The reserved width is derived from the value's px lengths (offset + blur + spread, inset layers excluded). Express the token in px: if any length uses another unit (rem, em) or a calc() / var(), the SDK cannot resolve it and falls back to the stylesheet's own reserved width, which may clip a ring wider than the default 3 px.

Clamping. Out-of-range values are clamped rather than rejected:

Token familyRange
Submit button height44–55 px
Input height30–60 px
Any border radius0–60 px
Any border width0–10 px
Any font size8–32 px
Any font weight1–1000
Any padding side0–40 px
Grid gaps0–40 px
submitButtonOpacityDisabled0–1

Rules​

rules accepts an allowlist of abstract element targets — not raw CSS selectors — mapped to real selectors in each render context. An unrecognised target is ignored.

Inside the card iframe: .Field, .Field--focused, .Field--invalid, .Label, .Label--focused, .Label--invalid, .InputWrapper, .InputWrapper--hover, .InputWrapper--focused, .InputWrapper--invalid, .InputWrapper--disabled, .Input, .Input::placeholder, .Select (+ --hover, --focused, --invalid, --disabled), .InputAdornment, .Error, .SubmitButton (+ --hover, --active, --disabled), .FormContainer.

On your page: .RevupContainer, .PaymentMethodsContainer, .FormContainerSlot, .PaymentMethod (+ --primary, --card, --secondary, --other), .PaymentMethodSkeleton, .CardButton (+ --hover, --open), .CardBadge (+ --open), .CardBrandSlot, .CardBrandIcon, .SecondarySection, .SecondaryDivider, .SecondaryDividerLabel, .SecondaryGrid, .OtherSection, .OtherToggle (+ --hover), .OtherPanel (+ --open), .LoaderOverlay, .LoaderSpinner, .DisabledOverlay.

The SDK sets --revup-* CSS custom properties on <html> to carry these values, and the 3-D Secure flow appends a hidden 1×1 host element to <body>. Both are removed by destroy().


10. userInfo — payer email​

userInfo: {
email: { required: true, value: 'buyer@example.com' },
}
PropertyTypeDefaultDescription
email.requiredbooleanfalseWhen true, the card form shows an email field above the card inputs and requires a valid address before the payment can be submitted.
email.valuestring''Pre-fills the email field when it is shown.

When the payer supplies an email, the payment request includes it (trimmed). When email.required is false or omitted, no email field is shown.

A malformed userInfo (e.g. email: { require: true }) is rejected at runtime and logged, and the defaults are used.


11. Languages and right-to-left​

language accepts a BCP 47 code. Matching is lenient: an exact tag wins, otherwise the primary subtag is matched ('es' and 'es-MX' → 'es-ES'; 'zh' → 'zh-CN'). An unsupported tag never throws — it falls back to the browser language, then to 'en-US'.

19 supported languages: en-US, ar-SA, cs-CZ, de-DE, es-ES, fr-FR, it-IT, ja-JP, ko-KR, nl-NL, pl-PL, pt-PT, ro-RO, ru-RU, th-TH, tr-TR, uk-UA, zh-CN, zh-TW.

Read them at runtime with Revup.getAvailableLanguages() — a static method, callable before mounting.

ar-SA is the only right-to-left language, and it translates the strings only. To mirror the layout you must also set form.textDirection: 'rtl'.


12. Lifecycle: cleanup and re-initialization​

Switching to a new order​

With the Session API:

await session.set({ orderId: nextOrderId });

With the constructor flow:

await revup.init({ containerId: 'revup-container', orderId: newOrderId });

The orderId you pass to init() wins. If you omit it, the SDK silently reuses the previous order — no warning is emitted, so pass it explicitly whenever it changes.

Full teardown​

stopListening();   // 1. remove your event listener
revup.destroy(); // 2. tear the SDK down

destroy() does what clearing the container cannot: it tears down the handlers and the card iframe, resets the accordion coordinator, empties the container, clears the appearance CSS custom properties and parent-page rules, removes the rel="preconnect" links the SDK injected into your <head>, and detaches the document-level keyboard and pointer listeners used by the accordion rows. Skipping it leaks those listeners.

destroy() does not remove your onRevupMessage event listener — that's a separate subscription you own. Call stopListening() (the function returned by onRevupMessage()) yourself before or alongside destroy(); otherwise your listener stays attached even after the SDK is torn down.

Never use container.innerHTML = '' as teardown. Earlier revisions of this guide recommended it; it is not a sufficient teardown.

Do not reuse the same Revup instance for a different merchant configuration (merchantDomain / apiKey) without destroying it first.


13. Session API (Revup.mount())​

The recommended entry point. new Revup(config).init(options) remains fully supported and is not deprecated.

const session = await Revup.mount({
merchantDomain: window.location.host,
apiKey: 'YOUR_PUBLIC_API_KEY',
containerId: 'revup-container',
orderId: 'ORDER_ID',
language: 'en-US',
appearance: { theme: 'default' },
form: { collapsed: false },
apms: { primary: [{ name: 'apple_pay' }] },
onMessage: (event) => console.log(event.type, event),
});

Revup is the only global; RevupSession is reached through the object mount() resolves to.

RevupSession​

MemberSignatureDescription
set(patch)Promise<UpdateReport>Applies a partial update. See routing below.
on(handler)() => voidSubscribes to SDK events; returns an unsubscribe function. Survives rebuilds.
subscribeTo(fields, handler)() => voidFires only when a successful update actually changed one of fields. noop updates never fire.
stateSessionStateDeep-frozen snapshot of the last committed { config, options }.
destroy()voidTears down the runtime and all listeners. Idempotent.

The patch​

A patch is a flat object whose keys fall into two disjoint groups:

GroupKeysUpdate path
Merchant configmerchantDomain, apiKey, containerId, apiVersioncold — the runtime is destroyed and rebuilt internally
Checkout optionsorderId, language, appearance, form, apms, userInfohot — the same runtime is reused
  • undefined means "leave this field alone".
  • null resets an optional checkout field to its SDK default.
  • An unknown key, a symbol key, or a non-plain-object patch is rejected with a RevupSessionError.

What a hot update actually does​

Changed fieldEffect
appearanceCSS variables and rules re-applied live. Nothing is rebuilt.
formThe accordion state, brand-icon placement, brand strip and text direction are driven directly; the new options are pushed to the existing iframe. Payment methods are untouched and the iframe is not reloaded. Toggling form.enabled is structural and forces a full re-init.
apmsThe zones are diffed. Methods whose configuration did not change keep their DOM and vendor state. The card iframe is untouched.
orderIdThe order is re-fetched and every payment method is re-initialized against it (amount, currency and vendor session all change per order). The card iframe is reused, not reloaded, so anything already typed is preserved.
userInfoRe-synced without a rebuild.
languageFalls back to a full re-init on the same instance — labels and vendor buttons must re-render consistently. Still reported as path: 'hot'.
any merchant-config keypath: 'cold' — full internal rebuild.

UpdateReport​

type UpdateReport = {
path: 'hot' | 'cold' | 'noop';
changed: ReadonlyArray<string>; // fields actually re-applied; empty for noop
coalesced?: boolean; // true when several set() calls were merged
committed?: object; // final values for the changed fields
durationMs: number;
};

Concurrent set() calls made in the same tick are coalesced into one transition. Every coalesced caller resolves with the same report, and observers see exactly one revup.updated event for the merged batch.

Failure handling​

set() restores the previous state on failure and rejects with a RevupSessionError carrying a stable code:

codeMeaning
SESSION_DESTROYEDThe session was destroyed before the patch could apply.
INVALID_ARGUMENTMalformed patch (unknown key, not a plain object, empty subscribeTo field list).
INVALID_CONFIGThe merged state failed validation (e.g. empty orderId).
UPDATE_FAILEDThe update failed; the previous state was restored.
SESSION_CONFLICTAnother session is already active on this page.
UNRECOVERABLEThe update and the rollback both failed. The session is terminal.

Only one RevupSession may be active per page. Destroy the current one before mounting another.


14. Event contract​

Everything the SDK reports reaches your page as a single native CustomEvent named onRevupMessage, dispatched on document.

Listening​

const stop = revup.onRevupMessage((event) => {
console.log(event.type, event);
});

// or natively — read the SDK event from event.detail
document.addEventListener('onRevupMessage', (e) => {
const event = e.detail;
});

// or through the session
const stop = session.on((event) => { /* … */ });

Guarantees​

  1. Events are dispatched on document with bubbles: false. A listener on window, or on your container element, will never fire. Listen on document.
  2. event.detail is deep-frozen — handlers cannot mutate it. Copy before editing.
  3. context, data and error are omitted when empty — always optional-chain.
  4. One submit, one terminal event. Every card payment attempt ends in exactly one of payment.success / payment.failed / payment.cancelled / payment.action_required. A duplicate submit while one is already in flight is ignored and produces no extra events. A network error or gateway timeout produces exactly one payment.failed, re-enables the pay button and keeps the entered field values.
  5. Superseded submits are abandoned. If the order or API key changes while a submit is in flight, its response is discarded and no further event is dispatched for it. A response that lands before that reset is attributed to the orderId pinned at submit time, never to the new one.
  6. Payloads are sanitized before dispatch: sensitive keys are redacted, and card numbers, IBAN, CPF/CNPJ, email and other PCI/PII patterns inside string values are masked. Structural fields such as transactionStatus are unaffected. Raw card data never appears in an event.
  7. Critical misconfiguration (missing container, missing orderId) throws — it is not dispatched as an event.
  8. These events are advisory. Always confirm the final outcome server-side using transactionId / orderId.

Envelope​

{
id: string; // UUID v4, unique per event
type: SdkEventType;
source: 'form' | 'apm' | 'sdk';
timestamp: string; // ISO 8601

context?: {
orderId?: string;
transactionId?: string;
apm?: string; // APM events
transactionStatus?: string;
};

data?: unknown;
error?: {
name: string; // machine-readable — see Error names
message: string; // human-readable, sanitized
statusCode?: number; // HTTP status, when the failure came from an HTTP response
};
}

There is no error.code — the machine-readable field is error.name.

Event / source matrix​

typesource: 'form'source: 'apm'source: 'sdk'
initialized✅ card form——
validation.changed✅ card form——
validation.error——✅
payment.submitted✅✅ all methods—
payment.action_required✅ 3-D Secure✅ Stripe-served methods + google_pay + native ideal—
payment.success✅✅—
payment.failed✅✅—
payment.cancelled✅ gateway-reported✅ every APM except the Stripe Payment Element methods (sepa_debit, pix, klarna, affirm, bizum, Stripe-served ideal), which have no cancel signal—
apm.unavailable—✅—
revup.updated——✅ Session API only

Only payment.success, payment.failed and payment.cancelled end a payment attempt. On the Stripe-served methods payment.success means Stripe confirmed without an error — the payment may still be settling (SEPA Direct Debit) or unpaid (a Pix QR the payer closed) — and a redirect method (klarna, affirm, bizum, Stripe-served ideal) takes the payer off your page before any terminal event, so the last event you receive may be payment.submitted or payment.action_required. Confirm those outcomes server-side.


source: 'form' — the card form​

initialized​

Fires once, when the card iframe has loaded and reported its size.

FieldValue
context.orderIdthe order id
data.message'initialized'

Use it to remove your loading placeholder — not as a signal that the payment can be submitted.

validation.changed​

FieldValue
context.orderIdthe order id
data.isValidboolean — all fields pass validation
data.isCallingboolean — a submission is in flight
data.invalidFieldsstring[] — keys of the fields currently failing validation; empty when isValid is true

Possible invalidFields keys: cardHolder, cardNumber, expiryDate, cvc, plus email when userInfo.email.required is true and zipCode when form.showZipCode is true. Fields that are not rendered never appear.

Identical consecutive reports are deduplicated, so this is an edge signal rather than a per-keystroke stream.

This is the signal to enable or disable your own payment button:

if (event.type === 'validation.changed') {
const { isValid, isCalling, invalidFields } = event.data;
myPayButton.disabled = !isValid || isCalling;
highlightFields(invalidFields);
}

After every payment attempt settles, the SDK emits one extravalidation.changed with isValid: false, isCalling: false, invalidFields: []. This is the form resetting itself, not the payer editing a field. Always drive your UI from the payload rather than from the event firing.

payment.submitted​

Fires when the payer submits, before the gateway responds.

FieldValue
context.orderIdthe order id
data.isValidtrue
data.isCallingtrue

context.transactionId is absent — the transaction does not exist yet. A 3-D Secure device-check round trip emits no intermediate event; the next event after payment.submitted is the terminal outcome or payment.action_required.

Terminal card events — one shared payload​

payment.action_required, payment.success, payment.failed (gateway decline) and payment.cancelled all come from the same gateway response, so they share one payload shape. The event type is derived from the gateway's transactionStatus:

Gateway transactionStatusEvent dispatched
successpayment.success
failedpayment.failed (error.name: 'PaymentFailed')
cancelled, cancelled_by_userpayment.cancelled
waiting_user_interaction, in_processpayment.action_required (in_process is normalized to waiting_user_interaction first)
anything elsepayment.submitted

Payload:

FieldValue
context.orderIdthe order id, pinned at submit time
context.transactionIdthe gateway transaction id, when one exists
data.actionpresent only when the gateway returned a challenge
data.transactionResult.statusthe HTTP status of the payment call
data.transactionResult.responsethe full gateway response body

transactionStatus and transactionId are deliberately not repeated at the top of data — they are represented by the event type and by context.transactionId. The gateway's own copy is still at data.transactionResult.response.transactionStatus.

Field wiping. The card fields are cleared only on success or waiting_user_interaction. A returned action does not by itself wipe them, and a failure keeps everything the payer typed so they can retry.

payment.action_required — the 3-D Secure challenge​

data.action describes where to send the payer. The same object is also at data.transactionResult.response.action.

FieldValue
data.action.urlURL to redirect to or POST to
data.action.method'GET' or 'POST'
data.action.parametersarray of { key, value } pairs to submit
document.addEventListener('onRevupMessage', (e) => {
const { type, source, data } = e.detail;
if (type !== 'payment.action_required' || source !== 'form') return;

const { url, method, parameters } = data.action;

if (method === 'POST') {
const form = document.createElement('form');
form.method = 'POST';
form.action = url;
for (const { key, value } of parameters) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = value;
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
} else {
window.location.href = url;
}
});

Persist anything you need before you send the payer away — the challenge leaves your page.

payment.failed — two different causes​

Causeerror.nameWhere the detail is
The gateway declined, or the payment call failed with a network / HTTP errorPaymentFaileddata.transactionResult.response
The card form could not submit at all (configuration / encryption failure before the payment call)FormSubmitErrorerror.message

Read the decline reason from data, not from error.message. On a PaymentFailed event error.message is the generic string "Payment failed". The gateway's actual reason is at data.transactionResult.response.message. On a FormSubmitError event there is no gateway response, and error.message is the real description.

error.statusCode is never set on form events. The HTTP status of the payment call is at data.transactionResult.status.

Do not rethrow — the form re-enables itself and the payer can retry.


source: 'apm' — alternative payment methods​

Every APM event carries context.apm with the method identifier, except apm.unavailable raised by the SDK's own APM bootstrap. All APMs share the shapes below; where ideal differs it is called out under the table.

EventWhenPayload
payment.submittedThe payer opened the wallet sheet or popup, or pressed the method's own buttoncontext: { orderId, apm }, data.message
payment.action_requiredThe payment needs something before it can settle — the two paths differ, see belowcontext: { transactionId, orderId, apm, transactionStatus }, data.message, data.action?
payment.successApprovedcontext: { transactionId, orderId, apm, transactionStatus }, data.message
payment.failedDeclined, or the vendor flow erroredcontext: { transactionId?, orderId, apm, transactionStatus? }, error: { name, message, statusCode? }
payment.cancelledThe payer dismissed the sheet or popup, or the backend reported the payment cancelled. Not dispatched by the Stripe Payment Element methods (sepa_debit, pix, klarna, affirm, bizum, Stripe-served ideal), which have no cancel signalcontext: { orderId, apm, transactionId? }, data.message
apm.unavailableThe method cannot render or has just been removedcontext: { apm, orderId? }, error: { name, message, statusCode? }

payment.action_required, by path.

PathWhat it meansdata.action
Stripe-served methodsThe payment intent reports waiting_user_interaction. Informational: no merchant handling requirednever
Google PayThe backend reports waiting_user_interaction — for example a Worldpay 3-D Secure challenge. Informational: no merchant handling requirednever
Native idealThe backend reports waiting_user_interaction / in_process, or returns a URL to redirect to (iDEAL's issuer selection)may be present — the SDK acts on it itself

Notes:

  • On the APM channel data carries only message, plus that optional action on native ideal. There is no data.transactionResult on the APM channel.
  • While a payment is in flight the SDK greys out the other payment methods, and re-enables them on the terminal event. From 1.2.8, a Google Pay payment.action_required also re-enables them: nothing further runs client-side after that handoff, so the payer is not left with a frozen checkout. The Stripe-served methods keep the checkout locked on their payment.action_required, because there a real in-process continuation (the 3-D Secure confirmation) still follows.
  • context.transactionStatus is set on APM payment.action_required and payment.success. On payment.failed it is set by the Stripe-served methods (from 1.2.15, always 'failed') and — from 1.2.8 — by Google Pay when the failure came from the backend response (transactionStatus: 'failed', or an error field in the body). It is absent on the other APMs' payment.failed, and on Google Pay failures raised before the payment call completes (tokenization data unavailable, vendor exception). On the Stripe-served methods the value is the SDK's own reading of the outcome — 'waiting_user_interaction', 'success' or 'failed' — never Stripe's raw status.
  • context.transactionId is absent on payment.submitted — it fires before the transaction exists.
  • apm.unavailable from the SDK's own bootstrap (config validation failure, no methods configured, availability lookup failure) carries no context.apm, and the merchant-level lookup failure carries no context at all — always write event.context?.apm.
  • payment.cancelled is a normal outcome, not a failure.
  • A method your account does not have enabled is never rendered and produces no event at all — do not wait for apm.unavailable to learn that.

Per-method triggers for apm.unavailable:

MethodTypical triggers
Apple PayVendor SDK fails to load; ApplePaySession missing; capability detection enabled and the device cannot pay or has no active card; merchant session or button init error
Google PayVendor SDK fails to load; configuration or order details unavailable; isReadyToPay reports unsupported; button container missing
PayPalContainer missing; clientId or order currency missing; vendor SDK fails to load; button not eligible or render fails
Stripe-served (stripe_link, amazon_pay, ideal, sepa_debit, pix, klarna, affirm, bizum)Stripe SDK fails to load; order details unavailable; public key missing; the element reports a load error; no available methods reported
Native (ideal only)The method cannot be initialized (internal error); provider is a non-empty value the SDK does not recognize — see How ideal renders

Whichever way ideal is served, its events carry source: 'apm' and context.apm: 'ideal' like every other APM — including the data.action case above, which the SDK handles itself. source === 'form' remains the correct filter for "an action I must perform".


source: 'sdk' — revup.updated and validation.error​

Dispatched only when you Use the Session API: after the initial mount() and after every set() that produced an observable change.

FieldValue
context.orderIdthe live order id at the time of the update
datathe UpdateReport

noop updates are returned on the set() promise but never dispatched as an event. Use this for telemetry, devtools panels and store synchronization — never to gate payment logic.

validation.error​

New in 1.2.5. A value you configured was outside the allowed set for a field. Recoverable: the SDK falls back to a default (or drops the offending entry), continues, and reports it here. error.name is always configuration_error.

Missing required fields (merchantDomain, apiKey, containerId, orderId) still throw — the SDK cannot proceed without them. A present-but-invalid value for an enumerable field is reported through this event instead of throwing.

Emitted for five fields only:

  • language — tag not in the supported set; falls back to the detected/default language.
  • apms — an entry's name is not a known payment method; that entry is dropped.
  • appearance.theme — unknown preset; falls back to the default theme.
  • appearance.variables — unknown variable key; that key is ignored.
  • appearance.rules — unknown rule target; that rule is ignored.

No other invalid value raises this event. Invalid form options and invalid APM color values stay console-only (see Option validation).

type ValidationErrorData = {
/** 'language' | 'apms' | 'appearance.theme' | 'appearance.variables' | 'appearance.rules' */
field: string;
/** The allowed values for this field, when the set is enumerable. */
allowed?: string[];
/** The offending value — omitted for sensitive fields. */
received?: string;
};

One event per invalid value found while resolving the options. Emission is scoped so an uncorrected value does not become repeat noise:

  • On the initial init() / mount() (cold resolution), every finding from that pass fires.
  • On a set() hot update, only findings for the fields in that patch fire — a still-invalid language from an earlier call is not re-emitted when you later set appearance.
  • When a hot update falls back to a full rebuild, that rebuild's resolution is the sole emitter for the attempt; the aborted hot attempt emits nothing.

context.orderId is always present — it identifies the checkout session that logged the finding, never the offending value. data.received and error.message never carry the raw value of merchantDomain, apiKey, orderId or any userInfo / PII field.

For apms, data.allowed is the SDK's full known-payment-method set, which is wider than what you can configure in apms: credit_card goes through form, and sepa is the API-side identifier — use sepa_debit (see apms — payment zones).

{
"type": "validation.error",
"source": "sdk",
"context": { "orderId": "order_123" },
"error": {
"name": "configuration_error",
"message": "language: 'fr-XX' is not a supported language; falling back to the detected/default language."
},
"data": {
"field": "language",
"allowed": ["en-US", "ar-SA", "..."],
"received": "fr-XX"
}
}

Error names​

The complete set of observable error.name values:

error.nameOrigin
apms_initialization_errorThe SDK's APM bootstrap: configuration validation, or the availability lookup
apple_pay_errorApple Pay
google_pay_errorGoogle Pay
paypal_errorPayPal
stripe_link_errorStripe Link
amazon_pay_errorAmazon Pay
ideal_erroriDEAL
sepa_debit_errorSEPA Direct Debit
pix_errorPix
klarna_errorKlarna
affirm_errorAffirm
bizum_errorBizum
PaymentFailedCard form — gateway decline or a failed payment request
FormSubmitErrorCard form — submission could not be sent
configuration_errorOption resolution in Revup / RevupSession — a configured value was outside the allowed set for a field (validation.error only)

A complete handler​

const stop = revup.onRevupMessage((event) => {
const { type, source, context, data, error } = event;

switch (type) {
case 'initialized':
hideLoadingPlaceholder();
break;

case 'validation.changed':
myPayButton.disabled = !data.isValid || data.isCalling;
highlightFields(data.invalidFields);
break;

case 'payment.submitted':
showSpinner();
break;

case 'payment.action_required':
// Only the card form carries an action to act on.
if (source === 'form' && data?.action) redirectToChallenge(data.action);
break;

case 'payment.success':
// Advisory — confirm server-side before fulfilling
confirmOnServer(context.orderId, context.transactionId);
break;

case 'payment.failed':
showError(
source === 'form'
? data?.transactionResult?.response?.message ?? error.message
: error.message
);
break;

case 'payment.cancelled':
restoreCheckout();
break;

case 'apm.unavailable':
hideMethod(event.context?.apm);
break;

case 'revup.updated':
// Session API telemetry only
break;
}
});

15. Environments, URLs and CSP​

EnvironmentLoader host
Sandboxhttps://test.hosted.revuppayments.com
Livehttps://hosted.revuppayments.com

Use the API key that matches the environment; a sandbox key will not work against live.

Your Content Security Policy must allow:

  • script-src — the loader host above, and https://js.stripe.com when any Stripe-served method is enabled (Stripe Link, Amazon Pay, iDEAL, SEPA Direct Debit, Pix, Klarna, Affirm, Bizum or Apple/Google Pay routed through Stripe).
  • frame-src — the RevUp form origin, the wallet/vendor origins for the methods you enable, and the 3-D Secure challenge origins used by your gateway.
  • form-action / connect-src — the RevUp API origin and the 3-D Secure challenge endpoints. Since 1.2 the device-fingerprint exchange runs on your page, so these must be reachable from your checkout origin.

The exact origins for your account are provided with your API keys. The SDK's API base URL and form URL are fixed at build time; your integration only supplies merchantDomain, apiKey and orderId.


16. Going live — checklist​

  • [ ] Loader <script> points at the live host, /sdk/1/2/loader.js, with the live integrity hash and crossorigin="anonymous"
  • [ ] Live API key in use, and merchantDomain matches a registered production domain
  • [ ] The container element exists before you mount, and nothing else writes into it
  • [ ] Your onRevupMessage listener is attached to document before you mount
  • [ ] Every payment outcome is confirmed server-side by transactionId / orderId — never fulfilled from the event alone
  • [ ] 3-D Secure tested end to end in sandbox on your real checkout page
  • [ ] CSP verified with the SDK, your wallets and the 3-D Secure challenge
  • [ ] revup.destroy() (or session.destroy()) called when the checkout unmounts
  • [ ] Each enabled payment method tested on a real device (Apple Pay needs Safari on a device with an active card)
  • [ ] Every language you offer rendered at least once; RTL storefronts also pass form.textDirection: 'rtl'

Patch history of the 1.2 line​

The loader always serves the newest patch of the minor, so a merchant pinned to /sdk/1/2/ is on 1.2.16.

PatchMerchant-visible change
1.2.0Card payment submission and the 3-D Secure / device-fingerprint exchange moved out of the iframe onto your page; form.selfHostedDomain removed; validation.changed gained data.invalidFields and the post-payment reset event
1.2.2form.cardBrandIconPlacement; right-to-left mirroring; .CardBrandSlot became layout-only
1.2.3ideal can be served natively — your own PSP instead of Stripe — as a one-click button, or an accordion with a billing-address form on a recurring order; the native path adds payment.cancelled, data.action on payment.action_required, and (recurring only) initialized / validation.changed; color: 'white' | 'dark' styles it. pix and sepa_debit stay Stripe-served
1.2.5New validation.error event: a configured value outside the allowed set for language, apms or appearance is reported instead of failing silently
1.2.6form.showPopularBadge hides the "POPULAR" pill on the card row
1.2.7Dependency security updates only — build-time dependencies, not shipped in the SDK bundle. No API, event, appearance or behaviour change.
1.2.8Google Pay now emits payment.action_required when the gateway reports a pending challenge (for example a Worldpay 3-D Secure step), and payment.failed also covers the gateway's transactionStatus: 'failed'; both carry transactionStatus in context.
1.2.9The "POPULAR" pill on the card row is now hidden by default; set form.showPopularBadge: true to show it. No change to the card payment flow or to any other payment method.
1.2.10New default palette: the default preset moves from indigo to blue (the #306dd8 family) — checkouts that do not override appearance.variables change colour, with no action required. The card and submit buttons are now bold by default: cardButtonFontWeight and submitButtonFontWeight are no longer part of any preset and fall back to fontWeightBold; set either token to keep a lighter weight. inputBoxShadowFocus now also sizes the lateral room reserved for the focus ring — express it in px (see Focus ring).
1.2.11Internal load-time instrumentation only — the SDK now measures when each payment method actually becomes usable and reports it to RevUp's own telemetry. No API, event, appearance or behaviour change.
1.2.12Apple Pay subscription sheet corrected. The order amount is now always charged immediately — with a trial, it was previously presented as a deferred payment. The trial row now states its own billing interval and runs from today until the day before the subscription's start date, so the payer sees the real trial window instead of an open-ended one. Apple Pay served natively only; no API, event, appearance or configuration change.
1.2.13Recurring iDEAL no longer asks the payer for a billing address. Natively-served ideal is now the same one-click button whether or not the order is recurring — a recurring iDEAL payment is processed as a SEPA Direct Debit, whose billing address is optional, so the SDK collects none. The address form that 1.2.3 introduced for recurring orders is gone, and with it the initialized and validation.changed events it dispatched on the APM channel. Nothing to change in your integration; if you were reading those two events for ideal, they no longer fire.
1.2.14Dependency security updates and build pipeline changes only. No API, event, appearance or behaviour change.
1.2.15New payment methods: klarna, affirm and bizum. Served through Stripe as inline accordion rows, like sepa_debit and pix; the payer authorises on the provider's own page. Enable them in apms like any other method; their errors are klarna_error, affirm_error and bizum_error. On every Stripe-served method, context.transactionStatus is now always 'success' on payment.success and is also set — to 'failed' — on payment.failed. Which event fires is unchanged.
1.2.16Dependency security updates and build pipeline changes only. No API, event, appearance or behaviour change.

If you are pinned to /sdk/1/2/ you are on 1.2.16 and everything on this page applies.