Collect a Top-Up Card

Collect an external card for top-up inside Wirex-served iframes and hand the resulting card id to the top-up API.

Before You Start

Read the following guides before proceeding:

GuideWhy
PCI Compliant SDKIsolation model and package map
SDK SetupClient id, environments, installation, CSP
Card Top-UpThe REST flow the collected card feeds
Events and Error HandlingChange events and failure modes

Overview

@wirexapp/card-topup-web collects an external debit or credit card — PAN, expiry, CVV and cardholder
name — inside Wirex-served iframes and tokenizes it straight from the frame. Your page receives a cardId
and non-sensitive metadata.

The cardId is the same value that
Top-Up Card Registration returns from
POST {pci_base_url}/b2b/cards/topup. Using the SDK replaces that direct call: card data reaches Wirex
from inside the frame, so the PAN and CVV never enter your application.

Card top-up requires the CardTopup capability to be active. Check it with GET /api/v2/user before
mounting the fields.


Choose a Layout Variant

Per-fieldCombined
CreatorcreateCardTopUpFields(sdk, config)createCardTopUpForm(sdk, config)
IframesOne per field — number, expiry, cvv, nameOne, containing every field
Layout controlYours — each field mounts into your own containerWirex's stock layout
Mount callfield(name).mount(container), four timesmount(container), once
TokenizertokenizeTopUp(params)tokenizeTopUp(params)

Both accept the same config and return the same result. Per-field is the recommended variant; combined is
the shortest drop-in. Follow one path top to bottom — the two are not mixed within one card form.


Configuration

createCardTopUpFields(
  { environment, clientId },
  {
    auth: { getBearerToken },
    appearance,
    strings,
    onChange,
  },
);
FieldTypeRequiredDescription
auth.getBearerToken() => Promise<string>YesReturns a fresh user token. Called at tokenize in the per-field variant; at mount and at tokenize in the combined variant
appearanceobjectNoWhitelisted styling tokens — see Appearance and Localization
stringsobjectNoTranslated labels and validation messages — see Strings Keys
onChange(state) => voidNoAggregated validity and detected brand. Never carries card values

appearance and strings are read when a field mounts. Changing either afterwards has no effect until the
element is destroyed and recreated.


Step 1: Mount the Fields

import { createCardTopUpFields } from '@wirexapp/card-topup-web';

const topUp = createCardTopUpFields(
  { environment: 'uat', clientId: 'a4f6c1e2-9b03-4d77-8a51-2f0c6b8e4d19' },
  {
    auth: { getBearerToken: () => session.getAccessToken() },
    onChange: state => {
      submitButton.disabled = !state.complete;
      brandIcon.dataset.brand = state.brand;
    },
  },
);

await topUp.field('number').mount(document.querySelector('#card-number'));
await topUp.field('expiry').mount(document.querySelector('#card-expiry'));
await topUp.field('cvv').mount(document.querySelector('#card-cvv'));
await topUp.field('name').mount(document.querySelector('#card-name'));

Each field() handle exposes mount(container) and destroy(). The frames combine their values inside
the Wirex origin at tokenize time; the values never pass through your page.

All four fields must be mounted before tokenizing. The card-number field aggregates the others and
issues the request.

CallThrows
field(name) for a name outside the profileField "<name>" is not part of the "topup" profile
field(name) twice for the same nameField "<name>" already created for this group
tokenizeTopUp() without a mounted number fieldThe card-number field is not mounted

The combined variant mounts once:

import { createCardTopUpForm } from '@wirexapp/card-topup-web';

const topUp = createCardTopUpForm(sdk, { auth, onChange });
await topUp.mount(document.querySelector('#card-form'));

Step 2: Gate Your Submit Button

onChange reports non-sensitive state while the user types.

onChange: state => {
  state.complete;   // boolean — every field of the profile passes validation
  state.brand;      // 'Visa' | 'MasterCard' | 'unknown'
  state.fields;     // { number: { valid, touched }, expiry: {…}, cvv: {…}, name: {…} }
}
FieldTypeDescription
completebooleantrue when every field of the profile is valid
brandenumVisa, MasterCard or unknown
fieldsobjectPer-field { valid, touched }, keyed by field name

Step 3: Tokenize

Collect the billing address with your own form components — it carries no cardholder data.

const result = await topUp.tokenizeTopUp({
  billingDetails: {
    line1: '1 High Street',
    line2: 'Flat 2',
    city: 'London',
    state: 'LND',
    zipCode: 'N1 9GU',
    country: 'GB',
  },
  label: 'Personal Visa',
});
FieldTypeRequiredDescription
billingDetails.line1stringYesFirst address line
billingDetails.line2stringNoSecond address line
billingDetails.citystringYesCity
billingDetails.statestringNoState or region
billingDetails.zipCodestringYesPostal code
billingDetails.countrystringYesISO 3166-1 alpha-2 country code
labelstringNoSaved-card nickname. Falls back to the cardholder name typed in the name field when blank

Result:

{
  "cardId": "c34908da-d980-4a9c-9b39-4dabd6f6144e",
  "brand": "Visa",
  "bin": "424242",
  "last4": "4242",
  "expiryMonth": 12,
  "expiryYear": 2028
}
FieldDescription
cardIdCard identifier. The only value to store
brandVisa, MasterCard or unknown
binFirst 6 digits of the PAN
last4Last 4 digits of the PAN
expiryMonthExpiry month, 112
expiryYearExpiry year, four digits

bin and last4 are out of PCI scope and safe to display. Do not log the result beyond cardId, brand
and last4.


Step 4: Estimate and Execute the Top-Up

cardId is passed as external_card_id to the top-up endpoints. Full request and response documentation
is in Top-Up Transfer.

const estimate = await fetch(`${baseUrl}/api/v1/cards/topup/estimate`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'X-User-Wallet': userEoaAddress,
    'X-Chain-Id': chainId,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    external_card_id: result.cardId,
    destination_amount: '100000000',
    destination_token_address: destinationTokenAddress
  })
}).then(response => response.json());

const execute = await fetch(`${baseUrl}/api/v1/cards/topup/execute`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'X-User-Wallet': userEoaAddress,
    'X-Chain-Id': chainId,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ estimation_id: estimate.estimation_id })
}).then(response => response.json());

Step 5: Run the 3DS Challenge

When the execute response carries three_ds_state, the cardholder completes the issuer challenge before
the top-up settles.

import { createThreeDsChallenge } from '@wirexapp/card-3ds-web';

if (execute.three_ds_state?.url) {
  const challenge = createThreeDsChallenge({
    url: execute.three_ds_state.url,
    onComplete: status => finishTopUp(status),
    onError: message => { showToast(message); finishTopUp('Failed'); },
  });
  challenge.mount(document.querySelector('#threeds-container'));
}

The REST response field is three_ds_state in snake_case. The 3DS element takes its url value
verbatim. See 3DS Challenge Element.


Field Validation

The frames validate as the user types and render error messages inline inside the frame, styled by the
colorError token and localized by strings. Do not re-implement card validation on your page.

FieldRuleMessage key
numberNot emptyerrRequired
number13–19 digits after stripping non-digitserrCardNumber
numberBrand detected as Visa or MastercarderrCardBrand
numberPasses the Luhn checkerrCardLuhn
expiryMatches MM/YY and month is 0112errExpiryFormat
expiryEnd of the expiry month is in the futureerrExpired
cvv3 or 4 digitserrCvv
nameNot empty after trimmingerrName

Strings Keys

Pass already-translated values; the frames have no i18n runtime.

KeyDefault
cardNumberLabelCard Number
expiryLabelMM/YY
cvvLabelCVV
nameLabelFull Name
errRequiredRequired
errCardNumberCard number must be 13-19 digits
errCardBrandOnly Visa/Mastercard cards are supported
errCardLuhnCard number is invalid
errExpiryFormatUse MM/YY format
errExpiredCard has expired
errCvvCVV must be 3 or 4 digits
errNameEnter cardholder name

Error Handling

Failures the frame reports — validation and backend rejections — reject with PciFrameRequestError. Its
message is a safe, non-sensitive description and status carries the HTTP status of the backend call
where one was made. Transport and lifecycle failures reject with a plain Error.

import { PciFrameRequestError } from '@wirexapp/card-topup-web';

try {
  const result = await topUp.tokenizeTopUp({ billingDetails });
} catch (error) {
  const isFieldValidation =
    error instanceof PciFrameRequestError && error.message.startsWith('Validation failed');
  if (!isFieldValidation) {
    showToast('Failed to add card');
  }
}
MessageError classCauseResolution
Validation failed — …PciFrameRequestErrorA field is empty or invalid at tokenize timeShow nothing extra. The inline message is already visible inside the frame
Backend error text, status setPciFrameRequestErrorTokenization rejected by WirexSurface a generic failure and let the user retry
Network errorPciFrameRequestErrorThe frame could not reach WirexSurface a retry action
PCI frame request timed outErrorNo response within 30 secondsSurface a retry action
PCI frame is not mountedErrortokenizeTopUp called before mounting or after destroy()Create a new element

Mount failures, lifecycle rules and the full error surface are documented in
Events and Error Handling.


Lifecycle

Call destroy() on the element when your view unmounts. Elements are single-use: after destroy(), create
a new one rather than remounting.

topUp.destroy();

Did this page help you?