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 |
|---|---|
| Android SDK | Surfaces, isolation model and flows |
| SDK Setup | create(...) and the 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
WirexCollectForm is one view holding the card inputs. The user types the card inside it; the SDK registers
the card with Wirex and returns a token. Your code receives the token, the brand, the BIN and the last four
digits — never the PAN or the CVV.
Collect needs no action token: it uses accessToken() and the identity from your WirexCredentialProvider.
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.OCT_DESTINATION | 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: Add the Form
class CollectScreen(private val activity: Activity, credentials: WirexCredentialProvider) {
private val scope = CoroutineScope(Dispatchers.Main)
private val collect: WirexCollect = WirexCollect.create(credentials = credentials, testMode = true, chainId = "<chain id>")
?: error("this build has no endpoints for that environment")
private lateinit var form: WirexCollectForm
fun mount(holder: FrameLayout) {
form = collect.form(context = activity, profile = WirexCollectProfile.TOPUP)
form.onChange = { state -> updateSubmit(state.isComplete) }
holder.addView(form, MATCH_PARENT, WRAP_CONTENT)
}
}Use WRAP_CONTENT or form.naturalHeightPx(widthPx) for the height. To lay fields out yourself,
form.row(field) returns a single sealed row. form.errorLine sets where validation messages render:
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 | Boolean |
fields[field].error | INCOMPLETE, INVALID, EXPIRED, or null |
The sealed fields replace only the card inputs. The billing address and card label come from your own views.
Enable submit only when isComplete is true and your billing address is complete.
Step 3: Tokenize
Top-Up Card
fun tokenize() {
scope.launch {
try {
val token = form.tokenizeTopUp(WirexTopUpTokenizeParams(
billingDetails = WirexBillingDetails(line1 = "1 High Street", zipCode = "N1 9GU", city = "London", country = "GB"),
label = "Personal Visa",
))
backend.topUp(externalCardId = token.cardId)
} catch (error: WirexError) {
showError(error.code, error.retryable, 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 | Card brand |
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
val token = form.tokenizeOctDestination(WirexOctTokenizeParams(
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.
Call form.clear() when the screen goes away.
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 throw WirexError(code, retryable, reason).
code / reason | Meaning | Resolution |
|---|---|---|
INVALID_INPUT / CARD_ALREADY_LINKED | The card is already linked | Ask the user for a different card |
INVALID_INPUT / CARD_LIMIT_REACHED | The user holds the maximum number of saved cards | Remove a saved card before adding another |
INVALID_INPUT, no reason | A field is invalid | Fix the highlighted field |
| Symptom | Cause | Resolution |
|---|---|---|
Tokenize fails 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 |
Code UNAUTHORIZED | Stale bearer — the SDK never retries a 401 | Refresh inside accessToken() |
Full error handling: Errors, Logging and Troubleshooting.
Updated about 3 hours ago

