Card Transfer
Push funds from the corporation wallet to an external payment card over OCT rails.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Getting Started | Platform overview and setup |
| Api Basics | Required headers and request configuration |
| Authentication | How to obtain the corporation token |
| Capabilities | Card transfer is capability-gated |
| Creating a Recipient | The destination card is a recipient payment detail |
Overview
A card transfer credits an external Visa or Mastercard over OCT (Original Credit Transaction) rails,
debiting a token balance from the corporation wallet. It is the fastest off-ramp available to a
corporation: no bank account, no corridor, no recipient bank details.
The flow is estimate then execute. The estimate quotes the transfer across the tokens you nominate and
returns an estimation_id; execution names the token to debit.
Prerequisites
| Requirement | How to check |
|---|---|
The CardTransfer capability is Active | GET /api/v1/corporations |
The caller holds CardSu | See Permissions |
| The destination card has been tokenized and holds a card identifier | See below |
Both card-transfer endpoints require
CardSu— notCardCreate, notCardManage, not
TransactionCreate. A role built for payments does not reach them unless it also carriesCardSu.
The Destination Card Identifier
external_card_id is the identifier of a tokenized card. Tokenization happens in Wirex's
PCI-compliant secure environment, not in the Corporate API — the Corporate API never accepts a card
number.
POST {pci_base_url}/b2b/cards/oct?user_id={corporation_address}
Query parameters:
| Parameter | Description |
|---|---|
user_id | The corporation wallet address — corporation_address, not an employee id |
Request body:
{
"card_number": "4242424242424242",
"cardholder_name": "ALEX GREY",
"is_saved": true
}| Field | Type | Required | Description |
|---|---|---|---|
card_number | string | Yes | Full card number (PAN) |
cardholder_name | string | Yes | Name as printed on the card, uppercase |
is_saved | boolean | Yes | true to keep the card for future transfers |
Response:
{
"value": "tok_a1b2c3d4e5f6789012345678"
}| Field | Description |
|---|---|
value | The card token. This is the value passed as external_card_id |
user_idcarries the corporation address here, not a user identifier. The retail flow passes a
user UUID to the same path; the corporate flow passescorporation_address. Sending a user id
tokenizes the card against the wrong owner and the transfer will not find it.
See Corporate Environments for the PCI base URL, and contact Wirex for
its credentials — they are separate from the API credentials.
Once tokenized, register the card as a recipient payment detail of type Card, which stores
card.card_id and card.card_pan_last. Either the token from value or card.card_id can be passed
as external_card_id; storing it as a recipient is what makes it reusable and auditable.
See Creating a Recipient.
Step 1: Estimate the Transfer
POST /api/v1/cards/transfer/estimate
Requires CardSu, and CardTransfer must be Active.
Request body:
{
"external_card_id": "wirex_123456789",
"amount": 250.00,
"currency": "USD",
"tokens": [
"0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
"0x5c55F314624718019A326F16a62A05D6C6d8C8A2"
]
}| Field | Type | Required | Description |
|---|---|---|---|
external_card_id | string | Yes | Tokenized card identifier — card.card_id from the recipient's payment details |
amount | number | Yes | Amount to credit to the card, in currency. Must be greater than zero |
tokens | array of string | Yes | Token addresses to quote against. At least one. Unified tokens such as WUSD and WEUR are accepted directly — unlike Corporate FX, no unwrap is needed |
currency | string | No | Currency of amount. Defaults to USD when omitted |
Response:
{
"estimation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"amount": 250.00,
"currency": "USD",
"fee_amount": 2.50,
"expires_at": 1704107700,
"estimated_amounts": [
{
"amount": 252.50,
"precise_amount": "252500000000000000000",
"token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
"token_symbol": "WUSD",
"rate": 1.0,
"fee_amount": 2.50,
"fee_precise_amount": "2500000000000000000"
}
]
}| Field | Description |
|---|---|
estimation_id | Identifier required by the execute call |
amount | Amount that will reach the card, in currency |
currency | Currency of the credit |
fee_amount | Fee charged for the transfer |
expires_at | Unix timestamp after which the estimate is no longer valid |
estimated_amounts[] | One entry per requested token — what would be debited if that token is chosen |
estimated_amounts[].precise_amount | Debit amount in the token's smallest unit |
estimated_amounts[].fee_amount | Per-token fee. Empty when the estimator reports no per-token fee |
Code
const response = await fetch(`${baseUrl}/api/v1/cards/transfer/estimate`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${corporationToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
external_card_id: externalCardId,
amount: 250.00,
currency: 'USD',
tokens: [tokenAddress]
})
});
const estimate = await response.json();response = requests.post(
f"{base_url}/api/v1/cards/transfer/estimate",
headers={
"Authorization": f"Bearer {corporation_token}",
"Content-Type": "application/json",
},
json={
"external_card_id": external_card_id,
"amount": 250.00,
"currency": "USD",
"tokens": [token_address],
},
)
estimate = response.json()body, _ := json.Marshal(map[string]interface{}{
"external_card_id": externalCardID,
"amount": 250.00,
"currency": "USD",
"tokens": []string{tokenAddress},
})
req, _ := http.NewRequest("POST", baseURL+"/api/v1/cards/transfer/estimate", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+corporationToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var estimate CardTransferEstimateResponse
json.NewDecoder(resp.Body).Decode(&estimate)Step 2: Execute the Transfer
POST /api/v1/cards/transfer
Requires CardSu, and CardTransfer must be Active.
Request body:
{
"estimation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976"
}| Field | Type | Required | Description |
|---|---|---|---|
estimation_id | string | Yes | estimation_id from Step 1. Must be a valid UUID |
token_address | string | Yes | Which token to debit. Must be one of the addresses quoted in the estimate |
Response:
{
"id": "64120850-73a1-4df5-a074-d463258c9deb"
}| Field | Description |
|---|---|
id | Identifier of the executed transaction. It is the activity feed item's id |
Code
const response = await fetch(`${baseUrl}/api/v1/cards/transfer`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${corporationToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
estimation_id: estimate.estimation_id,
token_address: tokenAddress
})
});
const { id: transactionId } = await response.json();response = requests.post(
f"{base_url}/api/v1/cards/transfer",
headers={
"Authorization": f"Bearer {corporation_token}",
"Content-Type": "application/json",
},
json={
"estimation_id": estimate["estimation_id"],
"token_address": token_address,
},
)
transaction_id = response.json()["id"]body, _ := json.Marshal(map[string]string{
"estimation_id": estimate.EstimationId,
"token_address": tokenAddress,
})
req, _ := http.NewRequest("POST", baseURL+"/api/v1/cards/transfer", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+corporationToken)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var executeResp struct {
Id string `json:"id"`
}
json.NewDecoder(resp.Body).Decode(&executeResp)What Happens Next
- The chosen token is debited from the corporation wallet.
- The OCT is submitted to the card scheme.
- The movement appears in
GET /api/v1/activity/feedunder the returnedid. - Progress is delivered to
POST {your_webhook_base_url}/v2/webhooks/activities, and the balance
change to/v2/webhooks/balances.
There is no card-transfer-specific webhook and no status endpoint. Track completion through the
activity feed.
Limitations
- The estimate expires at
expires_at. An expiredestimation_idis rejected — re-estimate rather
than retrying. - There is no cancel or reversal endpoint. A completed OCT is final.
currencydefaults toUSDsilently. A caller intending a different currency and omitting the field
gets aUSDquote with no warning.
Error Handling
{
"error_reason": "ErrorInvalidField",
"error_description": "Invalid value for amount",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "amount" }
]
}Validation Errors (400)
| Error Reason | Error Details | Description | Resolution |
|---|---|---|---|
ErrorMissingField | field: external_card_id | external card id is required | Send the tokenized card identifier |
ErrorInvalidField | field: amount | Invalid value for amount | The amount must be greater than zero |
ErrorMissingField | field: tokens | tokens is required | Send at least one token address |
ErrorInvalidField | field: token_address | token address must be a valid address | Send a token address from the estimate |
ErrorInvalidField | field: estimation_id | estimation id must be a valid UUID | Send the estimation_id from the estimate |
ErrorGeneral | — | Capability is not active | CardTransfer is not Active. Read GET /api/v1/corporations |
Permission Errors (403)
| Error Reason | Description | Resolution |
|---|---|---|
ErrorPermissionDenied | User does not have required permissions | Both endpoints require CardSu |
Server Errors (500)
| Error Reason | Description | Resolution |
|---|---|---|
ErrorGeneral | Failed to estimate card transfer | The OCT provider rejected the quote — unknown card identifier, unsupported destination, or an amount outside its limits |
ErrorGeneral | Failed to execute card transfer | The estimate expired, was already executed, or the wallet lacks the balance. Re-estimate |
ErrorGeneral | Failed to read tokens | The token catalogue was unavailable. Retry |
Updated 20 days ago

