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:

GuideWhy
Getting StartedPlatform overview and setup
Api BasicsRequired headers and request configuration
AuthenticationHow to obtain the corporation token
CapabilitiesCard transfer is capability-gated
Creating a RecipientThe 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

RequirementHow to check
The CardTransfer capability is ActiveGET /api/v1/corporations
The caller holds CardSuSee Permissions
The destination card has been tokenized and holds a card identifierSee below

Both card-transfer endpoints require CardSu — not CardCreate, not CardManage, not
TransactionCreate. A role built for payments does not reach them unless it also carries CardSu.

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:

ParameterDescription
user_idThe corporation wallet addresscorporation_address, not an employee id

Request body:

{
  "card_number": "4242424242424242",
  "cardholder_name": "ALEX GREY",
  "is_saved": true
}
FieldTypeRequiredDescription
card_numberstringYesFull card number (PAN)
cardholder_namestringYesName as printed on the card, uppercase
is_savedbooleanYestrue to keep the card for future transfers

Response:

{
  "value": "tok_a1b2c3d4e5f6789012345678"
}
FieldDescription
valueThe card token. This is the value passed as external_card_id

user_id carries the corporation address here, not a user identifier. The retail flow passes a
user UUID to the same path; the corporate flow passes corporation_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"
  ]
}
FieldTypeRequiredDescription
external_card_idstringYesTokenized card identifier — card.card_id from the recipient's payment details
amountnumberYesAmount to credit to the card, in currency. Must be greater than zero
tokensarray of stringYesToken addresses to quote against. At least one. Unified tokens such as WUSD and WEUR are accepted directly — unlike Corporate FX, no unwrap is needed
currencystringNoCurrency 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"
    }
  ]
}
FieldDescription
estimation_idIdentifier required by the execute call
amountAmount that will reach the card, in currency
currencyCurrency of the credit
fee_amountFee charged for the transfer
expires_atUnix 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_amountDebit amount in the token's smallest unit
estimated_amounts[].fee_amountPer-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"
}
FieldTypeRequiredDescription
estimation_idstringYesestimation_id from Step 1. Must be a valid UUID
token_addressstringYesWhich token to debit. Must be one of the addresses quoted in the estimate

Response:

{
  "id": "64120850-73a1-4df5-a074-d463258c9deb"
}
FieldDescription
idIdentifier 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

  1. The chosen token is debited from the corporation wallet.
  2. The OCT is submitted to the card scheme.
  3. The movement appears in GET /api/v1/activity/feed under the returned id.
  4. 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 expired estimation_id is rejected — re-estimate rather
    than retrying.
  • There is no cancel or reversal endpoint. A completed OCT is final.
  • currency defaults to USD silently. A caller intending a different currency and omitting the field
    gets a USD quote 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 ReasonError DetailsDescriptionResolution
ErrorMissingFieldfield: external_card_idexternal card id is requiredSend the tokenized card identifier
ErrorInvalidFieldfield: amountInvalid value for amountThe amount must be greater than zero
ErrorMissingFieldfield: tokenstokens is requiredSend at least one token address
ErrorInvalidFieldfield: token_addresstoken address must be a valid addressSend a token address from the estimate
ErrorInvalidFieldfield: estimation_idestimation id must be a valid UUIDSend the estimation_id from the estimate
ErrorGeneralCapability is not activeCardTransfer is not Active. Read GET /api/v1/corporations

Permission Errors (403)

Error ReasonDescriptionResolution
ErrorPermissionDeniedUser does not have required permissionsBoth endpoints require CardSu

Server Errors (500)

Error ReasonDescriptionResolution
ErrorGeneralFailed to estimate card transferThe OCT provider rejected the quote — unknown card identifier, unsupported destination, or an amount outside its limits
ErrorGeneralFailed to execute card transferThe estimate expired, was already executed, or the wallet lacks the balance. Re-estimate
ErrorGeneralFailed to read tokensThe token catalogue was unavailable. Retry

Did this page help you?