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 prefix | Environment |
|---|---|
pk_test_ | Sandbox - test cards only. |
pk_live_ | Live. |
Passing a secret key (sk_…) throws immediately: secret keys must never reach the browser.
| Method | Description |
|---|---|
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.
| Member | Description |
|---|---|
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. |
ready | Promise 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
| Event | Payload | Fires 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});
| Variable | What it changes | Accepts |
|---|---|---|
fontFamily | All text in the fields. | A font stack: letters, digits, spaces, commas, quotes, hyphens. |
fontSize, labelFontSize | Input text; labels. | One length in px, rem, em, or %. |
labelFontWeight | Labels. | normal, bold, or 100–900. |
colorText, colorLabel, colorPlaceholder, colorBackground, colorBorder | The fields at rest. | #hex, rgb() / hsl(), or a named colour. |
colorPrimary | Focus border and ring. | A colour. |
colorDanger | Invalid border and ring, error messages. | A colour. |
borderRadius, borderWidth | The inputs. | One length. |
inputPadding, rowGap | Input 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, andupdate({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(), orcalc()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
localeto 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-srcentry. - 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.readystill 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 field | Description |
|---|---|
token | The card token (tok_…). Opaque, non-sensitive - safe to store and send to your server. |
brand | Card scheme (e.g. visa) - null until identified. |
last4 / bin | Display-safe digits. |
expMonth / expYear | Expiry; 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 field | Description |
|---|---|
status | succeeded, 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. |
paymentId | The payment's order_id. |
declineCode | On 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. |
error | On 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": "…"
}
type | Meaning |
|---|---|
validation_error | Field-level problems, raised before anything leaves the browser. |
authentication_error | The client_secret was rejected or has expired. |
api_error | Orchestr failed to process the request. |
network_error | The request never reached Orchestr. |
integration_error | Developer 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:
| Directive | Live | Sandbox |
|---|---|---|
script-src | https://js.upprove.com | https://js.upprove.com |
frame-src | https://fields.card.upprove.com | https://fields.sandbox.card.upprove.com |
connect-src | https://api.upprove.com | https://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.