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 |
|---|---|
| Flutter SDK | Surfaces, isolation model and flows |
| SDK Setup | WirexCollect.configure and its credentials |
| Top-Up Transfer | The estimate and execute calls a top-up cardId feeds |
| Card Tokenization | Recipient creation with a payout card token |
Overview
WirexCollectFormView renders the card inputs as a native view. The user types the card inside it; the SDK
registers the card with Wirex and returns a token. Dart 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 WirexCollect.configure(chainId:, accessToken:, userEmail: | userWallet:, testMode:) —
see Collect Credentials.
Choose a Profile
| Profile | Use for | Tokenize call | Returns | Hand the token to |
|---|---|---|---|---|
WirexCollectProfile.topup | A card that funds a top-up | tokenizeTopUp(WirexTopUpTokenizeParams) | cardId | external_card_id on Top-Up Transfer |
WirexCollectProfile.octDestination | A push-to-card payout destination | tokenizeOctDestination(WirexOctTokenizeParams) | externalCardId | card.card_id on POST /api/v2/recipients, then external_card_id on Push to Card |
Step 1: Render the Form
import 'package:flutter/widgets.dart';
import 'package:wirex_mobile_sdk/wirex_mobile_sdk.dart';
class TopUpForm extends StatefulWidget {
const TopUpForm({super.key});
@override
State<TopUpForm> createState() => _TopUpFormState();
}
class _TopUpFormState extends State<TopUpForm> {
WirexCollectFormController? _controller;
WirexCollectState _state = const WirexCollectState.empty();
@override
Widget build(BuildContext context) => WirexCollectFormView(
profile: WirexCollectProfile.topup,
strings: const WirexCollectStrings(
labels: <WirexCollectField, String>{WirexCollectField.number: 'Card number'},
),
onCreated: (WirexCollectFormController controller) => _controller = controller,
onChange: (WirexCollectState state) => setState(() => _state = state),
);
}The form sizes itself from the native content.
Custom Layout
To lay the fields out yourself, keep the form mounted and render one
WirexCollectFieldView(controller:, field:) per field. errorLine sets where the validation message renders:
WirexErrorLine.hidden, reserved or animated.
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 | bool |
fields[field].error | WirexCollectFieldError.incomplete, invalid, expired, or null |
The sealed fields replace only the card inputs. The billing address and card label come from your own
widgets. Enable submit only when isComplete is true and your billing address is complete.
Step 3: Tokenize
Top-Up Card
Future<void> submit() async {
final WirexCollectFormController? controller = _controller;
if (controller == null || !_state.isComplete) return;
try {
final WirexTopUpCardToken token = await controller.tokenizeTopUp(const WirexTopUpTokenizeParams(
billingDetails: WirexBillingDetails(
line1: '1 High Street', line2: 'Flat 2', city: 'London', zipCode: 'N1 9GU', country: 'GB',
),
label: 'Personal Visa',
));
await sendToBackend(token.cardId);
} on WirexCollectError catch (error) {
debugPrint('${error.code} retryable=${error.retryable} reason=${error.reason}');
}
}| 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):
| Field | Description |
|---|---|
cardId | Card identifier. The only value to store |
brand | Visa, MasterCard or unknown |
bin | First 6 digits of the PAN |
last4 | Last 4 digits of the PAN |
expiryMonth | Expiry month, 1–12 |
expiryYear | Expiry year, four digits |
bin and last4 are out of PCI scope and safe to display.
Payout Card
final token = await controller.tokenizeOctDestination(const WirexOctTokenizeParams(
cardLabel: 'Alex Visa',
isSaved: true,
isThirdParty: false,
));| Field | Type | Description |
|---|---|---|
cardLabel | String | Display label for the saved destination card |
isSaved | bool | Saves the card for later payouts |
isThirdParty | bool | 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 |
octDestination | 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 throw WirexCollectError { 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 throws 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 the accessToken callback |
Full error handling: Errors, Logging and Troubleshooting.
Updated about 3 hours ago

