Revup Unified SDK 1.3
Reference release: 1.3.0 — the first release of the 1.3 line. Status: current. New integrations should start here — 1.3 is the newest published line. Coming from 1.2? Start at Migrating from 1.2 to 1.3. Coming from 1.1 or earlier? Apply 1.1 → 1.2 first, then this page.
Everything on this page describes 1.3.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, 1.1 or 1.2.
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.2 to 1.3
1.3 changes the event contract only. Options (form, apms, appearance, userInfo, language) and the payment methods themselves are unchanged, and so are the Session API and destroy() signatures (see item 8 for what destroy() now sends). Your configuration works as it is.
What you have to touch is your onRevupMessage handler. Items 1–3 are required for every integration. Items 4–8 change which event you receive in specific situations: read the ones for the methods you offer. Item 9 lists new fields you can adopt when you like.
1. Change the loader path
- src="https://test.hosted.revuppayments.com/payments-widget/sdk/1/2/loader.js"
+ src="https://test.hosted.revuppayments.com/payments-widget/sdk/1/3/loader.js"
The integrity hash changes with the minor. Request the 1.3 value from RevUp.
2. Breaking — source: 'sdk' is now source: 'revup'
The source of the SDK-level events (revup.updated, validation.error and the new apms.unavailable) was renamed. There is no alias: a filter on 'sdk' silently stops matching.
- if (event.source === 'sdk' && event.type === 'revup.updated') { … }
+ if (event.source === 'revup' && event.type === 'revup.updated') { … }
If you only switch on event.type, nothing changes for you.
3. Two new events for "this part of the checkout cannot be used"
| New event | Replaces in 1.2 | Means |
|---|---|---|
apms.unavailable (source: 'revup') | apm.unavailable without context.apm | The alternative payment methods as a whole could not load: the merchant configuration is incomplete (API key or merchant domain missing), or the request that lists them failed. No APM will render; the card form is not affected. |
form.unavailable (source: 'form') | payment.failed with error.name: 'FormSubmitError' | The card form could not come up (for example, its encryption setup failed). No payment was attempted. |
So in 1.3:
apm.unavailablealways names one method and always carriescontext.apmandcontext.orderId.payment.failedalways means a payment was attempted — it never fires for a problem that happens before the payer pays.getApmsAvailable()reports a failed lookup throughapms.unavailable(it still resolves to[]and never rejects).- A valid configuration with no methods enabled sends no event. In 1.2 it sent
apm.unavailable.
If you hid the APM area, or showed a fallback, on apm.unavailable or payment.failed, listen for the new events as well:
case 'apms.unavailable':
showCardOnlyCheckout();
break;
case 'form.unavailable':
showCardFormUnavailable();
break;
4. payment.success now always means "approved"
In 1.2 some methods reported payment.success for payments that were not yet paid. In 1.3 every method follows one decision table (see Status to event). What changed:
| Method | 1.2 | 1.3 |
|---|---|---|
| Stripe-served methods (Stripe Link, Amazon Pay, SEPA Direct Debit, Pix, Klarna, Bizum, Stripe-served iDEAL) | payment.success whenever Stripe confirmed without an error | payment.success only when the payment intent succeeded. canceled → payment.cancelled. Any other status (processing, requires_action, requires_capture, requires_payment_method, …) → payment.action_required |
| Google Pay | A backend status other than "waiting" or failed could be reported as payment.success; any response with an error field was payment.failed | The backend status decides: success → success, failed → failed, cancelled → cancelled, any other value → payment.action_required, no status → failed |
| Apple Pay | Any backend status other than success → payment.failed. A failed merchant validation after the sheet opened → apm.unavailable (the button is removed) and no ending | cancelled → payment.cancelled. A waiting status → payment.action_required (the sheet completes; the payer finishes the step elsewhere). No status → payment.failed. A failed merchant validation → payment.failed, and the button stays |
| Card | A successful HTTP response with no status, or an unrecognised status → a second payment.submitted and no ending | No status → payment.failed (error.code: 'unexpected'). An unrecognised status → payment.action_required, and the fields are cleared |
| iDEAL (natively served) | An unrecognised status without a redirect URL → payment.failed | payment.action_required |
Two consequences worth knowing:
- SEPA Direct Debit normally ends with
payment.action_required, because a debit is stillprocessingwhen the payer finishes. Pix ends the same way while the QR code is unpaid. Confirm both on your server. - Audit past orders. If you fulfilled Stripe-served orders (Amazon Pay in particular) on the 1.2
payment.successalone, some of them may not have been charged.
5. Cancellations and in-form errors
| Situation | 1.2 | 1.3 |
|---|---|---|
| The payer closes the PayPal popup window | payment.failed | payment.cancelled |
PayPal's backend reports the order cancelled or cancelled_by_user when it is created | payment.failed | payment.cancelled |
| The payer clicks PayPal again while the first window is still open | — | payment.cancelled for the first attempt, then payment.submitted for the new one |
| PayPal reports an error or a cancel before the payer clicked | payment.failed / payment.cancelled with no payment.submitted | No event |
| A field the Stripe Payment Element rejects inline (for example an incomplete IBAN) | payment.submitted + payment.failed | No event — Stripe shows the error in the field |
A Stripe payment intent is canceled | payment.success | payment.cancelled |
| A new click on a Stripe Express Checkout button (Stripe Link, Amazon Pay, Stripe-routed wallets) while the previous payment is still open | — | payment.cancelled for the previous payment, then payment.submitted for the new one |
payment.cancelled now also carries context.transactionId when a transaction already existed.
6. One status vocabulary
context.transactionStatus is now set on every payment outcome — payment.action_required, payment.success, payment.failed and payment.cancelled — for every method, card included. It only ever holds one of four values:
success · failed · cancelled · waiting_user_interaction
The gateway's in_process is reported as waiting_user_interaction, and cancelled_by_user as cancelled — also inside the card's data.transactionResult.response.transactionStatus. For the card, in_process was already waiting_user_interaction in 1.2; what changed is that cancelled_by_user now reads as cancelled, and an unrecognised status as waiting_user_interaction. For APM events, if you matched in_process, match waiting_user_interaction instead. Branch your logic on the event type; use transactionStatus for logging and reconciliation.
7. Redirects the SDK performs: data.redirecting
When the SDK itself sends the payer to a provider page — natively-served iDEAL, and the Stripe-served redirect methods (iDEAL, Klarna, Bizum) — payment.action_required now carries data.redirecting: true and no data.action. There is nothing for you to do; the page is about to leave.
- Natively-served iDEAL never carries
data.action: the SDK performs the redirect itself. - A response with a final status (
success,failed,cancelled) is never redirected, even when it also carries a redirect URL: the status is reported instead. - The card's 3-D Secure challenge is unchanged:
data.actionis still there, because you perform that step.
case 'payment.action_required':
if (event.data?.redirecting) break; // the SDK is already sending the payer away
if (event.source === 'form' && event.data?.action) redirectToChallenge(event.data.action);
break;
8. An interrupted payment now ends with payment.cancelled
If a payment is in progress when you call destroy(), change the orderId, change the apms entry of that method, or make a change that rebuilds the checkout, the SDK closes it with payment.cancelled and data.reason: 'session_updated'. In 1.2 it ended silently, after a payment.submitted. The SDK also stops that payment's flow (it closes the wallet sheet or popup, and sends no further payment request). See When the SDK closes a payment.
The real outcome of such a payment is unknown — a request already sent may still complete. Confirm it on your server.
With the constructor flow, call destroy() before calling the stopListening() function returned by onRevupMessage() (the 1.2 guide showed the opposite order). With the Session API, destroy() delivers the event to your session.on() listeners and then removes them, so there is nothing to remove yourself.
9. New fields you can adopt (optional)
| Field | What it gives you |
|---|---|
context.paymentMethod | The method on every form and apm event — 'credit_card' for the card |
context.attemptId | The same id on payment.submitted and on every later event of that payment — use it to correlate them |
error.code | The class of failure: declined, network, configuration, invalid_input or unexpected. Use it to choose the message you show the payer |
error.vendorCode | The payment provider's own error code, when it supplied one |
data.reason | 'session_updated' on a payment.cancelled the SDK closed itself (item 8) |
data.redirecting | true when the SDK is sending the payer to a provider page (item 7) |
Nothing was renamed or removed apart from item 2: error.name, context.apm and the rest of the 1.2 payload are still there.
Smaller changes
- Card failures carry
error.code. On a decline,error.messageis now the gateway's own reason when it gave one (in 1.2 it was always"Payment failed"). On a network failure,error.statusCodeis set when an HTTP response was received. - Messages.
data.messageanderror.messagename methods by their display label ("Apple Pay", "SEPA Direct Debit"), never by their key. Backend error responses are capped at 200 characters; a decline reason is passed as the gateway gave it. Show payers your own copy, chosen byerror.code— logerror.message, do not display it. - After a payment ends, the forced
isValid: falsereport of 1.2 is gone. The form's real validity is reported when it changes, typically when the fields are cleared after a successful or waiting payment. - Card events carry
data.messageonpayment.submittedand on every outcome exceptpayment.failed. - Stripe-served declines put the provider's code in
error.vendorCode. In 1.2 it was inerror.statusCode, which is now absent for those. - Error names always follow
<method>_errorfor APMs;FormSubmitErrornow only appears onform.unavailable. validation.errornow also carrieserror.code: 'invalid_input'.
TypeScript
SdkEvent is now a union with one member per event type, so narrowing on type gives you the exact payload (for example, error is always present on payment.failed). SdkEvent is no longer generic: replace SdkEvent<T> with Extract<SdkEvent, { type: 'payment.failed' }>. SdkEventSource is 'form' | 'apm' | 'revup'.
Migration checklist
- [ ] Loader path
1/2→1/3, newintegrityhash - [ ] Every
event.source === 'sdk'changed to'revup' - [ ]
apms.unavailableandform.unavailablehandled - [ ] Nothing fulfilled on
payment.successalone;payment.action_requiredtreated as "not paid yet" (SEPA Direct Debit, Pix, wallets) - [ ] PayPal re-tested: closing the popup is now
payment.cancelled - [ ] Matches on
in_processreplaced bywaiting_user_interaction - [ ]
payment.action_requiredwithdata.redirecting: trueignored by your redirect code - [ ]
payment.cancelledwithdata.reason: 'session_updated'confirmed server-side - [ ] Constructor flow:
destroy()called beforestopListening(). Session API:session.destroy()removes yoursession.on()listeners itself - [ ] Payer-facing error copy chosen by
error.code, not taken fromerror.message - [ ] TypeScript:
SdkEvent<T>replaced,'sdk'removed from source checks
What's new in 1.3
| Area | Change |
|---|---|
| Events | source: 'sdk' renamed to 'revup' (breaking) |
| Events | New apms.unavailable: the alternative payment methods as a whole could not load |
| Events | New form.unavailable: the card form could not come up. payment.failed now always means a payment was attempted |
| Events | One decision table for every method: payment.success only for an approved payment, one status vocabulary in context.transactionStatus, set on every outcome |
| Events | Every payment has one start (payment.submitted) and exactly one ending |
| Events | New fields: context.paymentMethod, context.attemptId, error.code, error.vendorCode, data.redirecting, data.reason |
| Payments | A payment interrupted by destroy() or by an update ends with payment.cancelled (data.reason: 'session_updated'), and the SDK stops its flow |
| Payment methods | PayPal: closing the popup is payment.cancelled. Google Pay and Apple Pay: the backend status decides the outcome. Stripe-served methods: the payment intent status decides the outcome |
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 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/3/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.3 line) |
|---|---|
| Sandbox | https://test.hosted.revuppayments.com/payments-widget/sdk/1/3/loader.js |
| Live | https://hosted.revuppayments.com/payments-widget/sdk/1/3/loader.js |
The two path segments before loader.js are the SDK's major and minor. Patch releases inside a minor (1.3.0 → 1.3.1) 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 apms.unavailable and resolves to []. An empty array therefore means either "no methods enabled" or "the lookup failed" — listen for apms.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. A payment in progress is closed with payment.cancelled first. 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. 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. 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.3. 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
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
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.
language does 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 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, 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 (server-side) and dispatches payment.success. |
stripe_link, amazon_pay | Stripe Express Checkout button opening a Stripe payment sheet. |
sepa_debit, pix, klarna, bizum | An inline Stripe Payment Element accordion row — see Accordion rows below. klarna 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. |
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: the native shape is the one-click button in both cases.
provider | What renders | Also dispatches |
|---|---|---|
stripe | Stripe Payment Element accordion row | — |
worldpay, another payment gateway RevUp serves natively, 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 | — |
| a value RevUp does not recognize | Nothing: ideal is not rendered and the SDK dispatches apm.unavailable, collapsing the slot | apm.unavailable |
A provider value RevUp does not recognize — 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 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, 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, bizum | Not customizable — the value is ignored. pix, sepa_debit, klarna, 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 (see 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. 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', and is reported through validation.error.
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.
A payment in progress when the order changes is closed with payment.cancelled (data.reason: 'session_updated') before the new order loads. See When the SDK closes a payment.
Full teardown
revup.destroy(); // 1. tear the SDK down — a payment in progress ends with payment.cancelled here
stopListening(); // 2. then remove your event listener
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.
In the constructor flow, destroy() does not remove your onRevupMessage event listener — that's a separate subscription you own. Call stopListening() (the function returned by onRevupMessage()) yourself, after destroy() returns; otherwise your listener stays attached even after the SDK is torn down. Removing it first would make you miss the payment.cancelled that destroy() sends for a payment in progress. With the Session API, session.destroy() delivers that event to your session.on() listeners and then removes them.
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, then removes the listeners added with on(). Idempotent. A payment in progress ends with payment.cancelled, which on() listeners still receive. |
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 | A payment in progress… |
|---|---|---|
appearance | CSS variables and rules re-applied live. Nothing is rebuilt. | continues |
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. | continues (closed when form.enabled changes) |
apms | The zones are diffed. Methods whose configuration did not change keep their DOM and vendor state. A removed method, or one whose type, color or zone changed, is rebuilt. The card iframe is untouched. | is closed only if its own method was removed or rebuilt |
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. | is closed |
userInfo | Re-synced without a rebuild. | continues |
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'. | is closed |
| any merchant-config key | path: 'cold' — full internal rebuild. | is closed |
"Closed" means the payment ends with payment.cancelled and data.reason: 'session_updated' — see When the SDK closes a payment.
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 payment, one start, one ending. Every payment attempt starts with exactly one
payment.submittedand ends with exactly one ofpayment.success,payment.failed,payment.cancelledorpayment.action_required(see Status to event for the twopayment.action_requiredcases that do not end it). Every event of the attempt carries the samecontext.attemptId. A second card submit while one is in flight is ignored and produces no events. - Payment events only exist inside a payment. A problem that happens before the payer pays is reported as
form.unavailable,apm.unavailableorapms.unavailable— never aspayment.failed. A field error a payment method shows inline produces no event at all. - An interrupted payment ends as cancelled. When the SDK closes a payment in progress (see When the SDK closes a payment), it sends
payment.cancelledwithdata.reason: 'session_updated'and stops that payment's flow. A response that arrives after that is not reported. A response that arrives before it is attributed to theorderIdthe payment started with, never to a new one. - One status vocabulary.
context.transactionStatusand the card'sdata.transactionResult.response.transactionStatusonly ever containsuccess,failed,cancelledorwaiting_user_interaction. - Payloads are masked before dispatch: sensitive keys are redacted, and card numbers, IBAN, CPF/CNPJ, email and other PCI/PII patterns inside string values are masked, including inside
error. Raw card data never appears in an event. Masking works on patterns, so a free-text message from a backend can still contain personal data: never render amessageas HTML (usetextContent), and apply your own retention policy to what you log. - 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' | 'revup';
timestamp: string; // ISO 8601
context?: {
orderId?: string;
paymentMethod?: string; // form and apm events — 'credit_card' for the card form
apm?: string; // apm events — same value as paymentMethod
attemptId?: string; // payment events — the same on every event of one payment
transactionId?: string;
transactionStatus?: 'success' | 'failed' | 'cancelled' | 'waiting_user_interaction';
};
data?: unknown;
error?: {
name: string; // where the failure came from — see Error names
code: 'declined' | 'network' | 'configuration' | 'invalid_input' | 'unexpected';
message: string; // for your logs, not for the payer
statusCode?: number; // HTTP status, when an HTTP response was received
vendorCode?: string; // the payment provider's own error code, when it supplied one
};
}
sourcefollows the payment method: the card is'form', every other method is'apm', and events about the SDK itself are'revup'.context.attemptIdis set frompayment.submittedonwards. It is absent oninitialized,validation.*and the*.unavailableevents.context.transactionIdis absent onpayment.submitted, which fires before the transaction exists.context.transactionStatusis set on every payment outcome and absent onpayment.submitted.data.messageis a short sentence on every payment event exceptpayment.failed, whose text is inerror.message.- For the full list of
error.namevalues, see Error names.
error.code
Use error.code for your logic and to choose the message you show the payer. error.name tells you where the failure came from.
error.code | Meaning |
|---|---|
declined | The provider or the gateway refused the payment, or reported it as failed. |
network | A request failed at the HTTP or network level. statusCode is set when an HTTP response was received. |
configuration | The payment or the method could not start because its configuration or order data was unavailable. |
invalid_input | A value was rejected — the payer's input in a payment sheet, or a configured option (validation.error). |
unexpected | A response or state the SDK did not expect, including a missing transaction status. |
error.message is an SDK sentence in English, except on declined (the gateway's or provider's own reason, when given), on network with an HTTP response (the backend's message, capped at 200 characters), and on network without an HTTP response, where it is the browser's own error text (for example Failed to fetch). Log it; show payers your own localized copy.
Event / source matrix
type | source: 'form' | source: 'apm' | source: 'revup' |
|---|---|---|---|
initialized | ✅ card form | — | — |
validation.changed | ✅ card form | — | — |
validation.error | — | — | ✅ |
payment.submitted | ✅ | ✅ all methods | — |
payment.action_required | ✅ 3-D Secure | ✅ | — |
payment.success | ✅ | ✅ | — |
payment.failed | ✅ | ✅ | — |
payment.cancelled | ✅ | ✅ | — |
form.unavailable | ✅ card form | — | — |
apm.unavailable | — | ✅ one method | — |
apms.unavailable | — | — | ✅ all APMs |
revup.updated | — | — | ✅ Session API only |
Status to event
One table decides the outcome for every method. The input is the status the SDK receives from the gateway or from Stripe.
| Received | Event | context.transactionStatus |
|---|---|---|
Gateway success | payment.success | success |
Gateway failed | payment.failed (error.code: 'declined') | failed |
Gateway cancelled or cancelled_by_user | payment.cancelled | cancelled |
Gateway waiting_user_interaction, in_process or any other value | payment.action_required | waiting_user_interaction |
| Gateway status missing | payment.failed (error.code: 'unexpected') | failed |
Stripe payment intent succeeded | payment.success | success |
Stripe payment intent canceled | payment.cancelled | cancelled |
Stripe payment intent processing, requires_action, requires_capture, requires_confirmation, requires_payment_method, or missing | payment.action_required | waiting_user_interaction |
| A redirect URL with no status, or a status that is not final | payment.action_required with data.redirecting: true — the SDK sends the payer to the provider | waiting_user_interaction |
- A final status always wins over a redirect URL.
payment.action_requiredends the payment, except in two Stripe-served steps that come before Stripe's confirmation: a backend status that is not final (waiting, in process or an unrecognised value), and thedata.redirecting: truenotice that precedes a Stripe redirect (iDEAL, Klarna, Bizum). In both, the same payment continues and ends with a later event.- While a payment is in progress the SDK greys out the other payment methods, and re-enables them when it ends.
When the SDK closes a payment
The SDK ends a payment in progress by itself only in these cases. The event is always payment.cancelled with context.transactionStatus: 'cancelled', data.reason: 'session_updated' and no error.
| You call | Payments closed |
|---|---|
destroy() | any method, card included |
init() again, or set() with a merchant-config key, language or form.enabled | any method, card included |
set({ orderId }) | any method, card included |
set({ apms }) | only a method that was removed, or whose type, color or zone changed |
set() with appearance, userInfo or any other form option | none — the payment continues |
What the SDK stops once it has closed the payment:
| Method | After the close |
|---|---|
| Card | The request was already sent with the submit; its response is ignored. |
| iDEAL (native) | No payment request is sent and no redirect happens. |
| Stripe-served methods | No payment is created or confirmed with Stripe. |
| Apple Pay | The payment sheet is closed. No payment request is sent. |
| Google Pay | The sheet cannot be closed by the SDK, but the payer's token is discarded and never sent. |
| PayPal | The PayPal window is closed, and an approval that still arrives is not confirmed. |
The real outcome of a closed payment is unknown: a request already sent may still complete. Confirm it on your server with orderId, and transactionId when you have one. A cancellation by the payer or the gateway has no data.reason.
source: 'form' — the card form
Every card event carries context.paymentMethod: 'credit_card'.
initialized
Fires once per mount, and once per full re-init, 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 a payment ends, the next
validation.changedreports the form's real state withisCalling: false. It is typically the report for the cleared form after a successful or waiting payment. 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 |
context.attemptId | the payment's id |
data.message | a short sentence |
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 outcome.
Card outcomes — one shared payload
payment.action_required, payment.success, payment.failed and payment.cancelled come from the gateway response and share one payload shape. The event type follows Status to event.
| Field | Value |
|---|---|
context.orderId | the order id the payment started with |
context.attemptId | the payment's id |
context.transactionId | the gateway transaction id, when one exists |
context.transactionStatus | one of the four public values |
data.message | a short sentence (not on payment.failed) |
data.action | present only when you must act — the 3-D Secure challenge |
data.transactionResult.status | the HTTP status of the payment call, or 'error' when no HTTP response was received |
data.transactionResult.response | the gateway response body, with transactionStatus in the public vocabulary |
transactionId and transactionStatus are not repeated at the top of data — they are in context.
Field wiping. The card fields are cleared on success and waiting_user_interaction. A failure or a cancellation 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' || !data?.action) 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
error.name is always PaymentFailed. The cause is in error.code:
error.code | Cause |
|---|---|
declined | The gateway declined the payment. error.message is the gateway's reason when it gave one; it is also at data.transactionResult.response.message. |
network | The payment request failed. error.statusCode is set when an HTTP response was received. |
unexpected | The response had no transaction status, or the SDK could not process it (for example, the 3-D Secure device check could not run). |
configuration | The SDK could not send the payment (for example, the order id was missing). |
Do not rethrow — the form re-enables itself and the payer can retry.
form.unavailable
The card form could not come up — for example, its encryption setup failed. No payment was attempted, so no payment.submitted precedes it. It fires at most once per mount or full re-init.
| Field | Value |
|---|---|
context.orderId | the order id |
error.name | 'FormSubmitError' |
error.code | 'configuration' |
error.message | the description |
source: 'apm' — alternative payment methods
Every APM event carries context.apm and context.paymentMethod with the method identifier. All APMs share the shapes below; the per-method tables that follow list exactly what triggers each event.
| Event | When | Payload |
|---|---|---|
payment.submitted | The payer opened the wallet sheet or popup, or pressed the method's own button | context: { orderId, apm, paymentMethod, attemptId }, data.message |
payment.action_required | The payment is not finished yet — see Status to event | context: { …, transactionId?, transactionStatus }, data.message, data.redirecting? |
payment.success | Approved | context: { …, transactionId?, transactionStatus }, data.message |
payment.failed | Declined, or the payment flow errored | context: { …, transactionId?, transactionStatus }, error: { name, code, message, statusCode?, vendorCode? } |
payment.cancelled | The payer dismissed the sheet or popup, the provider reported the payment cancelled, or the SDK closed it (data.reason) | context: { …, transactionId?, transactionStatus }, data.message, data.reason? |
apm.unavailable | The method cannot render | context: { orderId, apm, paymentMethod }, error: { name, code, message, statusCode? } |
Notes:
- On the APM channel
datacarriesmessage, plusredirectingorreasonwhen they apply. There is nodata.actionand nodata.transactionResulton the APM channel: whenever a method needs a redirect, the SDK performs it itself.source === 'form'remains the correct filter for "an action I must perform". apm.unavailableis sent only while a method initializes, never during a payment.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.
iDEAL (natively served)
A one-click button; no form is rendered.
| Event | Trigger |
|---|---|
payment.submitted | The payer clicks the iDEAL button |
payment.action_required | The backend returns a status that is not final. With a redirect URL, the SDK also sends the payer to the bank (data.redirecting: true) |
payment.success | Backend status success |
payment.cancelled | Backend status cancelled or cancelled_by_user |
payment.failed | Backend failed (declined); a missing status or an unsafe redirect URL (unexpected); the payment request failed (network) |
Stripe-served methods
- Express Checkout —
stripe_link,amazon_pay, and Apple Pay / Google Pay when routed through Stripe: a Stripe sheet opened from a button. - Payment Element —
sepa_debit,pix,klarna,bizumand Stripe-servedideal: an inline form with its own Pay Now button.
| Event | Trigger |
|---|---|
payment.submitted | Express Checkout: the payer clicks the button. Payment Element: the payer submits and Stripe's own field validation passes |
payment.action_required | The payment intent is not finished (see Status to event). Before Stripe's confirmation, also a backend status that is not final (waiting, in process or unrecognised), and the data.redirecting: true notice of iDEAL, Klarna and Bizum — the payment continues after both |
payment.success | The payment intent succeeded |
payment.cancelled | The payer closes the Express Checkout sheet, or the payment intent is canceled. In Express Checkout, a new click also closes a payment still open |
payment.failed | The backend reports failed, or Stripe's confirmation returns an error (declined, with vendorCode when Stripe gives one); the payment request failed (network); the Express Checkout sheet rejected a field (invalid_input); an unexpected error (unexpected) |
- A field the Payment Element rejects inline (an incomplete IBAN, for example) is shown in the field and produces no event.
- SEPA Direct Debit normally ends with
payment.action_required: a debit is stillprocessingwhen the payer finishes. Pix ends the same way while the QR code is unpaid. - iDEAL, Klarna and Bizum take the payer to the provider's page; the last event you receive may be
payment.action_required. Confirm the outcome server-side.
Apple Pay
| Event | Trigger |
|---|---|
payment.submitted | The Apple Pay sheet opens |
payment.cancelled | The payer dismisses the sheet, or the backend reports cancelled |
payment.success | Backend status success |
payment.action_required | A backend status that is not final. The sheet completes successfully; the payer finishes the step elsewhere |
payment.failed | Backend failed (declined); a missing status or an empty merchant session (unexpected); missing Apple Pay configuration (configuration); the merchant validation or the payment request failed (network) |
Google Pay
| Event | Trigger |
|---|---|
payment.submitted | The payer clicks the Google Pay button |
payment.cancelled | The payer closes the Google Pay sheet, or the backend reports cancelled |
payment.success | Backend status success |
payment.action_required | A backend status that is not final — for example a 3-D Secure step at the gateway. Nothing further runs on your page |
payment.failed | Backend failed (declined); a missing backend status, no payment data from Google, or an error the Google Pay sheet itself reports, for example a developer error (unexpected); configuration or order details unavailable at click time (configuration); the payment request failed (network) |
The backend status decides the outcome; an error field in the response only supplies the failure message.
PayPal
| Event | Trigger |
|---|---|
payment.submitted | The payer clicks the PayPal button |
payment.success | The payer approves in the PayPal window and the SDK's confirmation request reaches RevUp |
payment.cancelled | The payer cancels in the PayPal window or closes it, or the backend reports the order cancelled when it is created. A second click on the button also closes the still-open payment before the new one starts |
payment.failed | The order is declined (declined); a request to create or confirm the order failed (network); a missing transaction id or an unexpected response (unexpected) |
- PayPal never sends
payment.action_required: the PayPal window is the step. - An error PayPal reports before the payer clicks produces no event.
- The SDK's confirmation request cannot read the final payment result, so
payment.successis confirmed by the backend, not by the event. Always confirm PayPal payments server-side.
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 configuration incomplete; order details unavailable at setup |
| Google Pay | Vendor SDK fails to load; Google Pay configuration unavailable; isReadyToPay reports unsupported; button container missing. A failed order fetch during a click is payment.failed instead |
| 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, bizum) | Stripe SDK fails to load; order details unavailable; public key missing; the element reports a load error; no available methods reported; an integration type the SDK does not support |
Native (ideal only) | The method cannot be initialized; provider is a value RevUp does not recognize — see How ideal renders |
source: 'revup' — SDK-level events
Events about the SDK itself, not about the card form or one payment method. In 1.2 this source was called 'sdk'.
apms.unavailable
The alternative payment methods as a whole could not load, so no APM renders. The card form is not affected. Different from apm.unavailable, which means one method failed while the rest of the checkout works.
| Trigger | error.code |
|---|---|
| Your merchant configuration is incomplete (API key or merchant domain missing) | configuration |
| The response that lists the APMs is missing or invalid | configuration |
The request that lists the APMs, or the availability lookup (getApmsAvailable()), failed | network |
| Field | Value |
|---|---|
context.orderId | the order id |
context.apm | never set — no single method is implicated |
error.name | 'apms_initialization_error' |
error.statusCode | set when an HTTP response was received |
- A valid configuration with no methods is not an error: no event is sent.
- It is sent once per cause; a later update that hits the same failure does not repeat it.
if (event.type === 'apms.unavailable') {
showCardOnlyCheckout();
}
revup.updated
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
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 and error.code is always invalid_input.
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(), 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 is the only emitter for the attempt.
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": "revup",
"context": { "orderId": "order_123" },
"error": {
"name": "configuration_error",
"code": "invalid_input",
"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 APMs as a whole (apms.unavailable) |
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 |
bizum_error | Bizum |
PaymentFailed | Card payment (payment.failed) |
FormSubmitError | Card form that could not come up (form.unavailable) |
configuration_error | A configured value outside the allowed set (validation.error) |
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':
if (data?.redirecting) break; // the SDK is sending the payer to the provider
// Only the card form carries an action for you to perform.
if (source === 'form' && data?.action) redirectToChallenge(data.action);
else showPendingNotice(); // not paid yet — confirm server-side
break;
case 'payment.success':
// Advisory — confirm server-side before fulfilling
confirmOnServer(context.orderId, context.transactionId);
break;
case 'payment.failed':
showError(myLocalizedMessage(error.code)); // error.message is for your logs
break;
case 'payment.cancelled':
if (data?.reason === 'session_updated') checkOnServer(context.orderId);
restoreCheckout();
break;
case 'form.unavailable':
showCardFormUnavailable();
break;
case 'apm.unavailable':
hideMethod(context.apm);
break;
case 'apms.unavailable':
showCardOnlyCheckout();
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, Stripe-served iDEAL, SEPA Direct Debit, Pix, Klarna, Bizum or Apple/Google Pay routed through Stripe). Native iDEAL does not need it.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. 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/3/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, andpayment.action_requiredis treated as "not paid yet" - [ ]
apms.unavailableandform.unavailablehandled, so the payer always sees a usable checkout - [ ] Payer-facing error messages chosen by
error.code, not taken fromerror.message - [ ] 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, before your listener is removed - [ ] 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.3 line
The loader always serves the newest patch of the minor, so a merchant pinned to /sdk/1/3/ is on 1.3.0.
| Patch | Merchant-visible change |
|---|---|
1.3.0 | New event contract. source: 'sdk' renamed to 'revup'. New apms.unavailable and form.unavailable events. One decision table for every method: payment.success only for an approved payment, one status vocabulary in context.transactionStatus, set on every outcome. New context.paymentMethod, context.attemptId, error.code, error.vendorCode, data.redirecting and data.reason fields. Closing the PayPal popup is payment.cancelled. A payment interrupted by destroy() or by an update ends with payment.cancelled, and the SDK stops its flow. Includes every change of the 1.2 line. |
If you are pinned to /sdk/1/3/ you are on 1.3.0 and everything on this page applies.