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
- What the SDK does
- Quick start
- Loading the SDK script
- The container
- RevupConfig — constructor
- RevupOptions — init()
- form — card form options
- apms — payment zones
- appearance — checkout styling
- userInfo — payer email
- Languages and right-to-left
- Lifecycle: cleanup and re-initialization
- Session API (Revup.mount())
- Event contract
- Environments, URLs and CSP
- 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.invalidFieldsis new: astring[]of the field keys currently failing validation. Existing handlers that only readisValid/isCallingare unaffected.- After every payment attempt settles, the SDK emits one extra
validation.changedcarrying the reset state (isValid: false,isCalling: false,invalidFields: []). If your handler treatsvalidation.changedas "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, newintegrityhash - [ ]
form.selfHostedDomainremoved 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.textDirectionremoved unless you actually want RTL mirroring - [ ]
validation.changedhandler tolerates the post-payment reset event - [ ]
.CardBrandSlotappearance rule re-checked, if you had one
What's new in 1.2
| Area | Change |
|---|---|
| 3-D Secure | Device-fingerprint (DDC) requests always run first-party on your page. form.selfHostedDomain removed |
| Payments | The 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 |
| Events | validation.changed now carries data.invalidFields |
| Events | A validation.changed reset is emitted after every payment attempt settles |
| Layout | form.textDirection: 'rtl' mirrors the whole widget for the first time |
| Layout | form.cardBrandIconPlacement: 'input' | 'button' (from 1.2.2) |
| Appearance | .CardBrandSlot is now a layout-only container instead of a chip surface |
| Payment methods | ideal 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) |
| Events | New validation.error event reports a configured value outside the allowed set for language, apms or appearance, instead of failing silently (from 1.2.5) |
| Layout | form.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 methods | klarna, 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 … │
└─────────────────────────────────────────────┘
| Zone | Position | Max entries | Hidden when |
|---|---|---|---|
primary | Top of the block, together with the card accordion row | 4 | Never — the card row can still render when form.enabled is true |
secondary | Below primary, after an "Or pay with" divider, in a responsive 2-column grid (single column on narrow containers) | 4 | Empty, or none of its methods are available |
other | Below secondary, behind a "+ N more payment methods" toggle | unlimited | Empty, 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>
Recommended — the Session API
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:
- Create the order on your backend and get its
orderId. - Load the script and wait for
onRevupSDKLoaded. - Make sure the container element exists in the DOM.
- Attach your event listener, then mount / init.
3. Loading the SDK script
https://<host>/payments-widget/sdk/<major>/<minor>/loader.js
| Environment | Loader URL (1.2 line) |
|---|---|
| Sandbox | https://test.hosted.revuppayments.com/payments-widget/sdk/1/2/loader.js |
| Live | https://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.
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',
});
| Property | Type | Required | Description |
|---|---|---|---|
merchantDomain | string | Yes | Your merchant domain, e.g. window.location.host or your production domain. Must match a domain registered for your merchant account. |
apiKey | string | Yes | Public API key for the environment (sandbox or live). |
orderId | string | Yes in TypeScript / optional at runtime | Deprecated 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. |
version | string | No | API 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
| Method | Returns | Description |
|---|---|---|
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() | void | Full teardown. See Lifecycle. |
revup.getApmsAvailable() | Promise<string[]> | Payment methods enabled for your merchant account. Safe to call before init(). |
revup.onRevupMessage(handler) | () => void | Subscribes 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()
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
containerId | string | Yes | — | id of the element that will host the payment UI. |
orderId | string | No* | 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. |
language | string | No | browser language, then 'en-US' | BCP 47 language code. See Languages. |
appearance | object | No | { theme: 'default' } | Checkout styling. See appearance. |
form | object | No | see below | Card form options. See form. |
apms | object | No | {} | Payment zones. See apms. |
userInfo | object | No | {} | 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',
}
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | When false, the card accordion row and the card iframe are not rendered at all — an APM-only checkout. |
collapsed | boolean | true | true = the card accordion starts closed; false = it starts expanded. Independent of how many APMs render. |
showZipCode | boolean | false | Shows the ZIP / postal-code field. |
cardBrands | CardBrand[] | ['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. |
showPopularBadge | boolean | false | Whether 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. |
textDirection | string | '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. |
userInfo | object | {} | Same shape as top-level userInfo; the top-level form is preferred. |
There is no
form.styles,form.theme,form.buttonStylesorform.selfHostedDomainin 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' },
],
}
| Property | Type | Description |
|---|---|---|
primary | ApmEntry[] | Rendered above / alongside the card accordion. Max 4. |
secondary | ApmEntry[] | 2-column grid below, after an "Or pay with" divider. Max 4. Hidden when empty. Light-surface default colours. |
other | ApmEntry[] | Behind a "+ N more payment methods" toggle. Unlimited. Hidden when empty. Light-surface default colours. |
ApmEntry
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The payment method identifier — see the list below. |
type | string | No | Vendor button style variant. Ignored by ideal, pix, sepa_debit, klarna, affirm and bizum — none of them renders a vendor button. |
color | string | No | Vendor button colour — see Colours. |
capabilityDetection | boolean | No | When 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_cardis not valid here — the card flow is configured throughform.- Use
sepa_debit, notsepa, for SEPA Direct Debit. amazon_payonly renders when your account routes the order through Stripe.getApmsAvailable()returns the methods your account has enabled.
How each method renders
| Method | Rendering |
|---|---|
apple_pay, google_pay | Native wallet button, opening the device payment sheet. When your merchant configuration routes them through Stripe, they open a Stripe Express Checkout sheet instead. |
paypal | PayPal button opening the PayPal popup. There is no page redirect — the SDK confirms in the background and dispatches payment.success. |
stripe_link, amazon_pay | Stripe Express Checkout button opening a Stripe payment sheet. |
sepa_debit, pix, klarna, affirm, bizum | An 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. |
ideal | An 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.
provider | What renders | Also dispatches |
|---|---|---|
stripe | Stripe Payment Element accordion row | — |
worldpay, or no provider at all | A 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 value | Nothing: ideal is not rendered and the SDK dispatches apm.unavailable, collapsing the slot | apm.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)
| Method | Accepted values |
|---|---|
apple_pay | add-money, book, buy, check-out, continue, contribute, donate, order, pay, plain, reload, rent, set-up, subscribe, support, tip, top-up |
google_pay | book, buy, checkout, donate, order, pay, plain, subscribe |
paypal | paypal, checkout, buynow, pay, installment |
stripe_link, amazon_pay, ideal, pix, sepa_debit, klarna, affirm, bizum | not applicable — the value is ignored |
An unrecognised value falls back to the vendor default and is reported; the button still renders.
Colours
| Method | Accepted color |
|---|---|
apple_pay | Any 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_pay | default, black, white. An invalid value logs a warning and falls back to default. |
paypal | gold, 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, bizum | Not customizable — the value is ignored. pix, sepa_debit, klarna, affirm, bizum render an inline form, styled by the Appearance API. |
ideal | white, 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' },
},
}
| Key | Type | Description |
|---|---|---|
theme | 'default' | 'dark' | 'flat' | 'minimal' | Preset used as the base. Defaults to 'default'. |
variables | object | High-level design tokens that override the preset. |
rules | object | Fine-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:
| Domain | What it styles | Examples |
|---|---|---|
| Global | Typography and motion across every surface | fontFamily, fontWeightRegular, fontWeightBold, disableAnimations, transitionDuration |
| Container | The RevUp block on your page | backgroundColor, textColor, width, height, maxWidth, padding, borderRadius, justifyContent, alignItems |
| Form slot | The wrapper around the card iframe | formBackgroundColor, formPadding, formBorderRadius |
| Payment methods | Shared row surface for every method | paymentMethodsContainerPadding, primaryGridGap, paymentMethodHeight, paymentMethodBorderWidth, paymentMethodBorderRadius, paymentMethodBackgroundColor, paymentMethodBorderColor, paymentMethodBoxShadow |
| Card accordion | The "Credit or debit card" header | cardButtonBackgroundColor, cardButtonTextColor, cardButtonBorderColor, cardButtonBorderWidth, cardButtonBorderRadius, cardButtonFontSize, cardButtonFontWeight, cardButtonPadding, and the …Hover / …Open variants |
| Card badge | The "POPULAR" pill — not rendered unless form.showPopularBadge is explicitly true, so by default these tokens have nothing to style | cardBadgeBackgroundColor, cardBadgeTextColor, cardBadgeFontSize, cardBadgeFontWeight, cardBadgeLetterSpacing, cardBadgeBorderRadius, cardBadgePadding, and the …Open variants |
| Field / label | Form rows and labels inside the iframe | fieldMinHeight, expiryCvcRowGap, labelTextColor, labelFontSize, labelFontWeight, labelTextColorFocus, labelTextColorInvalid |
| Input | Card inputs | inputHeight, inputFontSize, inputPadding, inputBorderColor, inputBorderWidth, inputBorderRadius, inputBackgroundColor, inputTextColor, inputBoxShadow, inputPlaceholderColor, plus …Hover, …Focus, …Invalid, …Disabled variants, and inputAdornmentColor |
| Error | Field error text | errorTextColor, errorFontSize, errorFontWeight |
| Submit button | The card form's pay button | submitButtonHeight, submitButtonBorderRadius, submitButtonBackgroundColor, submitButtonTextColor, submitButtonFontSize, submitButtonFontWeight, and the …Hover, …Active, …Disabled, submitButtonOpacityDisabled variants |
| Skeleton / overlay | Loading placeholders and scrims | skeletonBackgroundColor, skeletonShimmerColor, skeletonAnimationDuration, overlayLoadingBackgroundColor, overlayDisabledBackgroundColor |
| Secondary zone | The "Or pay with" divider and grid | secondaryGridGap, secondaryDividerColor, secondaryDividerThickness, secondaryDividerLabelTextColor, secondaryDividerLabelBackgroundColor, secondaryDividerLabelFontSize, secondaryDividerLabelFontWeight, secondaryDividerLabelPadding |
| Other zone | The "+ N more payment methods" toggle | otherGridGap, 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 family | Range |
|---|---|
| Submit button height | 44–55 px |
| Input height | 30–60 px |
| Any border radius | 0–60 px |
| Any border width | 0–10 px |
| Any font size | 8–32 px |
| Any font weight | 1–1000 |
| Any padding side | 0–40 px |
| Grid gaps | 0–40 px |
submitButtonOpacityDisabled | 0–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' },
}
| Property | Type | Default | Description |
|---|---|---|---|
email.required | boolean | false | When true, the card form shows an email field above the card inputs and requires a valid address before the payment can be submitted. |
email.value | string | '' | 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
| Member | Signature | Description |
|---|---|---|
set(patch) | Promise<UpdateReport> | Applies a partial update. See routing below. |
on(handler) | () => void | Subscribes to SDK events; returns an unsubscribe function. Survives rebuilds. |
subscribeTo(fields, handler) | () => void | Fires only when a successful update actually changed one of fields. noop updates never fire. |
state | SessionState | Deep-frozen snapshot of the last committed { config, options }. |
destroy() | void | Tears down the runtime and all listeners. Idempotent. |
The patch
A patch is a flat object whose keys fall into two disjoint groups:
| Group | Keys | Update path |
|---|---|---|
| Merchant config | merchantDomain, apiKey, containerId, apiVersion | cold — the runtime is destroyed and rebuilt internally |
| Checkout options | orderId, language, appearance, form, apms, userInfo | hot — the same runtime is reused |
undefinedmeans "leave this field alone".nullresets 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 field | Effect |
|---|---|
appearance | CSS variables and rules re-applied live. Nothing is rebuilt. |
form | The 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. |
apms | The zones are diffed. Methods whose configuration did not change keep their DOM and vendor state. The card iframe is untouched. |
orderId | The 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. |
userInfo | Re-synced without a rebuild. |
language | Falls 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 key | path: '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:
code | Meaning |
|---|---|
SESSION_DESTROYED | The session was destroyed before the patch could apply. |
INVALID_ARGUMENT | Malformed patch (unknown key, not a plain object, empty subscribeTo field list). |
INVALID_CONFIG | The merged state failed validation (e.g. empty orderId). |
UPDATE_FAILED | The update failed; the previous state was restored. |
SESSION_CONFLICT | Another session is already active on this page. |
UNRECOVERABLE | The 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
- Events are dispatched on
documentwithbubbles: false. A listener onwindow, or on your container element, will never fire. Listen ondocument. event.detailis deep-frozen — handlers cannot mutate it. Copy before editing.context,dataanderrorare omitted when empty — always optional-chain.- 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 onepayment.failed, re-enables the pay button and keeps the entered field values. - 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
orderIdpinned at submit time, never to the new one. - 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
transactionStatusare unaffected. Raw card data never appears in an event. - Critical misconfiguration (missing container, missing
orderId) throws — it is not dispatched as an event. - 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
type | source: '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.
| Field | Value |
|---|---|
context.orderId | the order id |
data.message | 'initialized' |
Use it to remove your loading placeholder — not as a signal that the payment can be submitted.
validation.changed
| Field | Value |
|---|---|
context.orderId | the order id |
data.isValid | boolean — all fields pass validation |
data.isCalling | boolean — a submission is in flight |
data.invalidFields | string[] — 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 extra
validation.changedwithisValid: 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.
| Field | Value |
|---|---|
context.orderId | the order id |
data.isValid | true |
data.isCalling | true |
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 transactionStatus | Event dispatched |
|---|---|
success | payment.success |
failed | payment.failed (error.name: 'PaymentFailed') |
cancelled, cancelled_by_user | payment.cancelled |
waiting_user_interaction, in_process | payment.action_required (in_process is normalized to waiting_user_interaction first) |
| anything else | payment.submitted |
Payload:
| Field | Value |
|---|---|
context.orderId | the order id, pinned at submit time |
context.transactionId | the gateway transaction id, when one exists |
data.action | present only when the gateway returned a challenge |
data.transactionResult.status | the HTTP status of the payment call |
data.transactionResult.response | the 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.
| Field | Value |
|---|---|
data.action.url | URL to redirect to or POST to |
data.action.method | 'GET' or 'POST' |
data.action.parameters | array 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
| Cause | error.name | Where the detail is |
|---|---|---|
| The gateway declined, or the payment call failed with a network / HTTP error | PaymentFailed | data.transactionResult.response |
| The card form could not submit at all (configuration / encryption failure before the payment call) | FormSubmitError | error.message |
Read the decline reason from
data, not fromerror.message. On aPaymentFailedeventerror.messageis the generic string"Payment failed". The gateway's actual reason is atdata.transactionResult.response.message. On aFormSubmitErrorevent there is no gateway response, anderror.messageis 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.
| Event | When | Payload |
|---|---|---|
payment.submitted | The payer opened the wallet sheet or popup, or pressed the method's own button | context: { orderId, apm }, data.message |
payment.action_required | The payment needs something before it can settle — the two paths differ, see below | context: { transactionId, orderId, apm, transactionStatus }, data.message, data.action? |
payment.success | Approved | context: { transactionId, orderId, apm, transactionStatus }, data.message |
payment.failed | Declined, or the vendor flow errored | context: { transactionId?, orderId, apm, transactionStatus? }, error: { name, message, statusCode? } |
payment.cancelled | The 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 signal | context: { orderId, apm, transactionId? }, data.message |
apm.unavailable | The method cannot render or has just been removed | context: { apm, orderId? }, error: { name, message, statusCode? } |
payment.action_required, by path.
| Path | What it means | data.action |
|---|---|---|
| Stripe-served methods | The payment intent reports waiting_user_interaction. Informational: no merchant handling required | never |
| Google Pay | The backend reports waiting_user_interaction — for example a Worldpay 3-D Secure challenge. Informational: no merchant handling required | never |
Native ideal | The 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
datacarries onlymessage, plus that optionalactionon nativeideal. There is nodata.transactionResulton 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.transactionStatusis set on APMpayment.action_requiredandpayment.success. Onpayment.failedit 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.transactionIdis absent onpayment.submitted— it fires before the transaction exists.apm.unavailablefrom the SDK's own bootstrap (config validation failure, no methods configured, availability lookup failure) carries nocontext.apm, and the merchant-level lookup failure carries nocontextat all — always writeevent.context?.apm.payment.cancelledis 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.unavailableto learn that.
Per-method triggers for apm.unavailable:
| Method | Typical triggers |
|---|---|
| Apple Pay | Vendor 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 Pay | Vendor SDK fails to load; configuration or order details unavailable; isReadyToPay reports unsupported; button container missing |
| PayPal | Container 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.
| Field | Value |
|---|---|
context.orderId | the live order id at the time of the update |
data | the 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'snameis 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-invalidlanguagefrom an earlier call is not re-emitted when you later setappearance. - 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.name | Origin |
|---|---|
apms_initialization_error | The SDK's APM bootstrap: configuration validation, or the availability lookup |
apple_pay_error | Apple Pay |
google_pay_error | Google Pay |
paypal_error | PayPal |
stripe_link_error | Stripe Link |
amazon_pay_error | Amazon Pay |
ideal_error | iDEAL |
sepa_debit_error | SEPA Direct Debit |
pix_error | Pix |
klarna_error | Klarna |
affirm_error | Affirm |
bizum_error | Bizum |
PaymentFailed | Card form — gateway decline or a failed payment request |
FormSubmitError | Card form — submission could not be sent |
configuration_error | Option 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
| Environment | Loader host |
|---|---|
| Sandbox | https://test.hosted.revuppayments.com |
| Live | https://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, andhttps://js.stripe.comwhen 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 liveintegrityhash andcrossorigin="anonymous" - [ ] Live API key in use, and
merchantDomainmatches a registered production domain - [ ] The container element exists before you mount, and nothing else writes into it
- [ ] Your
onRevupMessagelistener is attached todocumentbefore 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()(orsession.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.
| Patch | Merchant-visible change |
|---|---|
| 1.2.0 | Card 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.2 | form.cardBrandIconPlacement; right-to-left mirroring; .CardBrandSlot became layout-only |
| 1.2.3 | ideal 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.5 | New validation.error event: a configured value outside the allowed set for language, apms or appearance is reported instead of failing silently |
| 1.2.6 | form.showPopularBadge hides the "POPULAR" pill on the card row |
| 1.2.7 | Dependency security updates only — build-time dependencies, not shipped in the SDK bundle. No API, event, appearance or behaviour change. |
| 1.2.8 | Google 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.9 | The "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.10 | New 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.11 | Internal 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.12 | Apple 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.13 | Recurring 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.14 | Dependency security updates and build pipeline changes only. No API, event, appearance or behaviour change. |
| 1.2.15 | New 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.16 | Dependency 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.