Collect a Card
Capture a top-up or payout card in a sealed native form and hand the returned token to the top-up and push-to-card endpoints.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| React Native SDK | Surfaces, isolation model and flows |
| SDK Setup | Configure and the collect credential provider |
| Top-Up Transfer | The estimate and execute calls a top-up cardId feeds |
| Card Tokenization | Recipient creation with a payout card token |
Overview
WirexCollect renders the card inputs as native views. The user types the card inside them; the SDK
registers the card with Wirex and returns a token. JavaScript receives the token, the brand, the BIN and the
last four digits — never the PAN or the CVV.
Collect needs no action token. Configure it with a WirexCollectCredentialProvider (bearer and identity) or
a full WirexCredentialProvider.
Choose a Profile
| Profile | Use for | Tokenize call | Returns | Hand the token to |
|---|---|---|---|---|
'topup' | A card that funds a top-up | tokenizeTopUp(params) | cardId | external_card_id on Top-Up Transfer |
'oct-destination' | A push-to-card payout destination | tokenizeOctDestination(params) | externalCardId | card.card_id on POST /api/v2/recipients, then external_card_id on Push to Card |
Choose a Layout
| Layout | Component | When |
|---|---|---|
| Form view | WirexCollectFormView | One native form with every field, sized from its own content |
| Headless form | WirexCollect.createForm({ profile }) + one WirexCollectFieldView per field | Your own layout, for example expiry and CVV side by side |
Step 1: Render the Form
Form View
import * as React from 'react';
import { useRef, useState } from 'react';
import {
WirexCollect, WirexCollectFormView,
type WirexCollectCredentialProvider, type WirexCollectFormHandle, type WirexCollectState,
} from '@wirexapp/mobile-sdk';
declare const credentials: WirexCollectCredentialProvider;
export const configureCollect = () =>
WirexCollect.configure({ testMode: true, chainId: '<chain id>', credentials });
export function TopUpCardForm() {
const form = useRef<WirexCollectFormHandle | null>(null);
const [state, setState] = useState<WirexCollectState | null>(null);
return (
<WirexCollectFormView
profile="topup"
strings={{ labels: { number: 'Card number' }, errors: { '*': { incomplete: 'Required' } } }}
onReady={(handle) => { form.current = handle; }}
onChange={setState}
style={{ width: '100%' }}
/>
);
}Headless Form
import * as React from 'react';
import { useEffect, useState } from 'react';
import { View } from 'react-native';
import {
WirexCollect, WirexCollectFieldView,
type WirexCollectFormHandle, type WirexCollectState,
} from '@wirexapp/mobile-sdk';
export function NewCardFields() {
const [form, setForm] = useState<WirexCollectFormHandle | null>(null);
const [state, setState] = useState<WirexCollectState | null>(null);
useEffect(() => {
let handle: WirexCollectFormHandle | null = null;
void WirexCollect.createForm({ profile: 'topup', onChange: setState }).then((h) => { handle = h; setForm(h); });
return () => { if (handle) void WirexCollect.destroyForm(handle); };
}, []);
if (!form) return null;
return (
<View>
<WirexCollectFieldView form={form} field="number" />
<View style={{ flexDirection: 'row', gap: 16 }}>
<WirexCollectFieldView form={form} field="expiry" style={{ flex: 1 }} />
<WirexCollectFieldView form={form} field="cvv" style={{ flex: 1 }} />
</View>
<WirexCollectFieldView form={form} field="name" />
</View>
);
}errorLine on WirexCollectFieldView sets where the validation message renders: 'hidden', 'reserved' or
'animated'. Release a headless form with WirexCollect.destroyForm(handle) when the screen goes away.
Step 2: Gate Your Submit Button
onChange delivers WirexCollectState:
| Property | Values |
|---|---|
isComplete | true when every field is valid |
brand | 'Visa', 'MasterCard', 'unknown' |
fields.<field>.isValid | boolean |
fields.<field>.error | 'incomplete', 'invalid', 'expired', null |
The sealed fields replace only the card inputs. The billing address and card label come from your own
inputs. Enable submit only when isComplete is true and your billing address is complete.
Step 3: Tokenize
Top-Up Card
const token = await form.current!.tokenizeTopUp({
billingDetails: { line1: '1 High Street', line2: 'Flat 2', city: 'London', zipCode: 'N1 9GU', country: 'GB' },
label: 'Personal Visa',
});| Field | Type | Required | Description |
|---|---|---|---|
billingDetails.line1 | string | Yes | First address line |
billingDetails.line2 | string | No | Second address line |
billingDetails.city | string | Yes | City |
billingDetails.state | string | No | State or region |
billingDetails.zipCode | string | Yes | Postal code |
billingDetails.country | string | Yes | ISO 3166-1 alpha-2 country code |
label | string | No | Saved-card nickname |
Result (WirexTopUpCardToken):
{
"cardId": "c34908da-d980-4a9c-9b39-4dabd6f6144e",
"brand": "Visa",
"bin": "424242",
"last4": "4242",
"expiryMonth": 12,
"expiryYear": 2028
}cardId is the only value to store. bin and last4 are out of PCI scope and safe to display.
Payout Card
const token = await form.current!.tokenizeOctDestination({
cardLabel: 'Alex Visa',
isSaved: true,
isThirdParty: false,
});| Field | Type | Description |
|---|---|---|
cardLabel | string | Display label for the saved destination card |
isSaved | boolean | Saves the card for later payouts |
isThirdParty | boolean | The card belongs to someone other than the user |
The result carries externalCardId — the card token for push to card.
Step 4: Hand the Token to Your Backend
The SDK makes no money-moving call. Your backend continues the REST flow:
| Profile | Next calls |
|---|---|
'topup' | POST /api/v1/cards/topup/estimate and POST /api/v1/cards/topup/execute with cardId as external_card_id. Execute returns three_ds_state when a challenge is required — run it with 3DS Challenge |
'oct-destination' | POST /api/v2/recipients with externalCardId as card.card_id, then POST /api/v1/cards/transfer/estimate and POST /api/v1/cards/transfer with external_card_id |
Errors
tokenizeTopUp and tokenizeOctDestination reject with { code, retryable, reason? }.
reason | Meaning | Resolution |
|---|---|---|
cardAlreadyLinked | The card is already linked | Use the existing card, or ask the user for another card |
cardLimitReached | The user holds the maximum number of saved cards | Remove a saved card before adding another |
| Symptom | Cause | Resolution |
|---|---|---|
Tokenize rejects without reason | Billing details missing, or the user is not enrolled at the acquirer | Send line1, zipCode, city, country. Assumption: acquirer enrollment follows the CardTopup capability — check it via GET /api/v2/user |
unauthorized | Stale bearer — the SDK never retries a 401 | Refresh inside getAccessToken |
Full error handling: Errors, Logging and Troubleshooting.
Updated about 3 hours ago

