Skip to main content

Upprove.js reference

Upprove.js is the browser SDK behind card payments. It mounts the secure card fields, exchanges the entered card for a token, confirms the payment, and renders 3-D Secure - while your page never sees a card number.

Install​

Always load the SDK from our CDN - never bundle or self-host it. The pinned-major URL means a security fix reaches your payment page without you redeploying, and PCI DSS v4's script-integrity duties (6.4.3, 11.6.1) remain on Orchestr's side:

<script src="https://js.upprove.com/v1/upprove.js"></script>

For bundlers, @upprove/card-js is a tiny loader that injects that same tag and gives you full TypeScript types:

npm install @upprove/card-js
import {loadUpprove} from '@upprove/card-js';
const Upprove = await loadUpprove();

Upprove(publishableKey)​

Creates a client. The key's prefix selects the environment - a pk_test_ key can never reach production:

Key prefixEnvironment
pk_test_Sandbox - test cards only.
pk_live_Live.

Passing a secret key (sk_…) throws immediately: secret keys must never reach the browser.

MethodDescription
upprove.elements(options?)A new element group. Accepts {locale, appearance}; both become the defaults for every element created in it.
upprove.collectCard(clientSecret)Collect the mounted card, resolve with a token.
upprove.pay(clientSecret, token)Pay with the collected token; renders 3-D Secure if required.

The card element​

const card = upprove.elements().create('card');
card.mount('#card-element');
await card.ready; // resolves once the secure fields have loaded

One combined element renders the number, expiry, and CVC fields. It lives in an iframe on Orchestr's origin, sized automatically to its content. create('card', {locale, appearance, clientSecret}) overrides the group's defaults for that element; see Appearance and Card on file.

MemberDescription
mount(target)Mount into a selector or element. Throws if the target is missing.
unmount()Detach; the element can be mounted again.
destroy()Terminal teardown - release the element and all listeners.
readyPromise resolving when the fields are interactive. Await before enabling Pay.
update({locale, appearance, clientSecret})Change options in place. Each key named replaces that option entirely.
clear() / focus() / blur()Field utilities.
on(event, handler) / off(event, handler)Subscribe / unsubscribe.

Card on file​

When your server charges a saved card with the customer present, the payment comes back with a collect_cvv action. Pass its client_secret to create() and the element mounts as a CVC-only form: a read-only row with the card's brand and last four digits, and one security-code field below it. No number, expiry or name is rendered, and change reports complete as soon as the code is valid for that brand.

const card = upprove.elements().create('card', {clientSecret});
card.mount('#card-element');

collectCard(clientSecret) takes the same secret and resolves with the saved card's token; expMonth and expYear are null, since the fields never saw them. The element holds its first paint until the secret arrives (300 ms at most), so the full card form never flashes before the CVC-only one. A collect_card secret passed the same way is harmless: the full form renders as usual. When a retry mints a new payment, hand the new secret over with update({clientSecret}) before collecting again.

Events​

EventPayloadFires when…
ready-The secure fields have loaded and are interactive.
change{complete, error, fields}Validity changes. Enable your Pay button on complete.
focus / blur{field}A field gains or loses focus.
submitting-A collection has started (UX hint - show a spinner).
loaderror{error}The fields failed to load (usually a CSP issue - see below).

Appearance​

The secure fields ship with a neutral look. To match your page, pass an appearance with the style variables you want to change. Set it on the element group so every element inherits it, on create() to override it for one element, or later through update():

const elements = upprove.elements({
appearance: {
variables: {
fontFamily: 'Manrope, system-ui, sans-serif',
fontSize: '14px',
colorText: '#1b1850',
colorLabel: '#6b7192',
colorPlaceholder: '#a3a8c3',
colorBackground: '#ffffff',
colorBorder: '#d6d9e6',
colorPrimary: '#282277', // focus border and ring
colorDanger: '#b42318', // invalid border, error text
borderRadius: '11px',
borderWidth: '1px',
inputPadding: '7px 10px',
rowGap: '4px',
labelFontSize: '11px',
labelFontWeight: '600',
},
},
});

const card = elements.create('card');
card.mount('#card-element');

// Later - for example, when the customer switches to dark mode:
card.update({appearance: darkAppearance});
VariableWhat it changesAccepts
fontFamilyAll text in the fields.A font stack: letters, digits, spaces, commas, quotes, hyphens.
fontSize, labelFontSizeInput text; labels.One length in px, rem, em, or %.
labelFontWeightLabels.normal, bold, or 100–900.
colorText, colorLabel, colorPlaceholder, colorBackground, colorBorderThe fields at rest.#hex, rgb() / hsl(), or a named colour.
colorPrimaryFocus border and ring.A colour.
colorDangerInvalid border and ring, error messages.A colour.
borderRadius, borderWidthThe inputs.One length.
inputPadding, rowGapInput padding; the gap between rows.One to four lengths.

Rules worth knowing:

  • Every update() replaces the whole appearance. A variable you leave out reverts to the default, and update({appearance: {}}) restores the stock look.
  • Invalid values are ignored, not errors. The secure fields validate every variable and keep the default for anything they cannot parse. Function-shaped values such as url(), var(), or calc() are never accepted.
  • Installed fonts only. The secure fields load no web fonts, so name fonts the customer's device already has and end the stack with system-ui, sans-serif.
  • No wording changes. Labels, placeholders, and validation messages come from the fields' own localized text; pass locale to pick the language.
  • Your Content-Security-Policy is unaffected. The variables travel over the SDK's existing channel and are applied inside the secure frame, so the CSP below needs no style-src entry.
  • When an appearance is configured, the fields hold their first paint (300 ms at most) until the variables arrive, so your page never flashes the default look. card.ready still resolves only once the appearance has been sent.

collectCard(clientSecret)​

Submits the card from inside the secure fields and exchanges it for a durable token. The clientSecret is the next_action.client_secret from your server's POST /v1/payments call - a short-lived grant scoped to that one payment. On a collect_cvv secret it submits only the security code and resolves with the token of the card on file the secret was minted for.

The same method also serves the collection leg of a card transfer, where the client secret comes from POST /v1/transfers and the token it mints is payout-scoped rather than charge-scoped. Don't call pay() on that flow - a transfer is settled by your server with POST /v1/transfers/{orderId}/execute.

const {card, error} = await upprove.collectCard(clientSecret);

Resolves with either card or error, never throws on a decline:

card fieldDescription
tokenThe card token (tok_…). Opaque, non-sensitive - safe to store and send to your server.
brandCard scheme (e.g. visa) - null until identified.
last4 / binDisplay-safe digits.
expMonth / expYearExpiry; null for a card on file.

On failure the fields keep their contents so the customer can correct and retry. collectCard is never retried automatically.

pay(clientSecret, token)​

Pays with the collected token. clientSecret is the same collect_card or collect_cvv grant you passed to collectCard() - it identifies the payment, so there is no separate order id to pass - and token is card.token from the collection result. When the issuer requires 3-D Secure, the SDK renders the challenge in your page and completes the flow - you write no 3DS code.

const result = await upprove.pay(clientSecret, collected.token);
// result: {status, paymentId?, error?, declineCode?}
result fieldDescription
statussucceeded, failed, or requires_action. A manual-capture authorization resolves succeeded - the hold stands until your server captures it, and payment.authorized is the webhook to wait for.
paymentIdThe payment's order_id.
declineCodeOn a failed decline, the normalized decline code - the same vocabulary as decline_code on the webhook. Absent when the failure is an API or network error (see error), or the provider gave no reason. Pick your own customer copy from it; never show it raw.
errorOn an API or network failure, the error object.

Don't branch on succeeded alone - treat anything that isn't failed as "not yet refused" and wait for the webhook.

Treat the value as a display hint only - the signed payment.* webhooks are the authoritative result, and the only safe trigger for fulfilment.

How the challenge is presented​

When 3-D Secure is required, how the SDK mounts the challenge is a per-account setting, surfaced to the SDK as a display_mode of inline, popup, or redirect. redirect navigates the whole window away from your page, so don't rely on your page's JavaScript state surviving the challenge - re-read the payment on return.

display_mode and challenge_url are browser-SDK concerns and never appear on the Merchant API. If you want to render 3-D Secure yourself from your own server, charge the saved card with collect_cvv: false and see server-to-server card payments instead.

Errors​

Every failure resolves with one shape:

{
"type": "validation_error",
"code": "CLIENT_SECRET_EXPIRED",
"message": "…",
"correlationId": "…"
}
typeMeaning
validation_errorField-level problems, raised before anything leaves the browser.
authentication_errorThe client_secret was rejected or has expired.
api_errorOrchestr failed to process the request.
network_errorThe request never reached Orchestr.
integration_errorDeveloper misuse - these throw instead of resolving, and always mean a bug in the integration.

Branch on type, key your customer-facing copy off code, and quote correlationId in support tickets. A client_secret lives 15 minutes; an expired one fails fast with CLIENT_SECRET_EXPIRED - create a new payment to mint a fresh one.

Content-Security-Policy​

If your page sets a CSP, allow the SDK script, the secure-fields frame, and the confirm call:

DirectiveLiveSandbox
script-srchttps://js.upprove.comhttps://js.upprove.com
frame-srchttps://fields.card.upprove.comhttps://fields.sandbox.card.upprove.com
connect-srchttps://api.upprove.comhttps://api.sandbox.upprove.com

A missing frame-src is the usual cause of a loaderror with code IFRAME_BLOCKED.

Using with React​

No bindings package is needed - mount in an effect and destroy on unmount:

function CardField({upprove, onReady}) {
const ref = useRef(null);

useEffect(() => {
const card = upprove.elements().create('card');
card.mount(ref.current);
card.ready.then(onReady, () => {});
return () => card.destroy(); // required: React 18 StrictMode mounts twice
}, []);

return <div ref={ref} />;
}

Next steps​

  • Card payments - the end-to-end integration guide.
  • Webhooks - fulfil on payment.succeeded, never on the browser result.