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 |
|---|---|
| iOS 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 UIView 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 |
|---|---|---|---|---|
.topup | A card that funds a top-up | tokenizeTopUp(WirexTopUpTokenizeParams) | cardId | external_card_id on Top-Up Transfer |
.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: Place the Form
import UIKit
import WirexMobileSDK
final class CollectViewController: UIViewController {
private let credentials = AppCredentials()
private var form: WirexCollectForm?
override func viewDidLoad() {
super.viewDidLoad()
guard let collect = WirexCollect.create(credentials: credentials, testMode: true, chainId: "<chain id>") else { return }
let form = collect.form(profile: .topup)
self.form = form
view.addSubview(form)
form.frame = CGRect(x: 16, y: 100, width: view.bounds.width - 32, height: form.naturalHeight)
form.onChange = { [weak self] state in self?.updateSubmit(isComplete: state.isComplete) }
}
}Size the form to form.naturalHeight. To lay fields out yourself, form.row(for:) 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 | Bool |
fields[field].error | incomplete, invalid, expired, or nil |
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
func tokenize() {
guard let form else { return }
Task { @MainActor in
do {
let token = try await form.tokenizeTopUp(WirexTopUpTokenizeParams(
billingDetails: WirexBillingDetails(line1: "1 High Street", zipCode: "N1 9GU", city: "London", country: "GB"),
label: "Personal Visa"))
try await backend.topUp(externalCardId: token.cardId)
} catch let error as WirexError {
print(error.code, error.retryable, error.reason as Any)
}
}
}| 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
let token = try await form.tokenizeOctDestination(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.
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 |
.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 WirexError { 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 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

