Corridor Transfers
Send payouts over 16 payment rails through a single estimate, initiate and confirm API.
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 access tokens |
| Recipients | Corridor transfers pay a saved recipient |
| Capabilities | Each corridor is gated by its own capability |
Overview
The v3 bank API is a single set of three endpoints that covers every payout rail Wirex supports —
SEPA, ACH, PIX, SPEI, UPI and eleven more. The corridor is a path parameter, so adding a new rail to
your integration means changing one string, not writing a new flow.
POST /api/v3/bank/estimate/{corridor}
POST /api/v3/bank/initiate/{corridor}
POST /api/v3/bank/confirm/{corridor}
This differs from SEPA and ACH,
which are documented against the older v2 estimate and v1 transfer endpoints. Those pages remain
accurate for those two rails. Use v3 when you need any of the other fourteen corridors, or when you
want one code path for all of them.
Whether you need the third step depends on the corridor's provider. Some corridors settle from
the user's balance; CPN corridors require an on-chain funding transfer that you make yourself and
then confirm. Resolve this before writing code — see Corridors.
Corridors
| Corridor | Payment Rail | Currency | Provider | Confirm step |
|---|---|---|---|---|
sepa | SEPA | EUR | service-bank | No |
ach | ACH | USD | service-bank | No |
faster-payments | Faster Payments | GBP | service-bank | No |
pix | PIX | BRL | service-cpn | Yes |
spei | SPEI | MXN | service-cpn | Yes |
fedwire | FEDWIRE | USD | service-cpn | Yes |
swift | WIRE | USD | service-cpn | Yes |
cips | CIPS | CNY | service-cpn | Yes |
chats | CHATS | HKD | service-cpn | Yes |
fps_hk | FPS | HKD | service-cpn | Yes |
imps | IMPS/NEFT/UPI | INR | service-offramp | No |
instapay | InstaPay/PESONet | PHP | service-offramp | No |
bi-fast | BI-FAST | IDR | service-offramp | No |
nip | NIP | NGN | service-offramp | No |
ipp | IPP | AED | service-offramp | No |
pse | PSE | COP | service-offramp | No |
Anything outside this list is refused with
corridor: Unknown or unsupported corridor: {value}.
The provider column drives two things: whether account_id is required on the estimate, and whether
the flow has two steps or three.
| Provider | account_id on estimate | Flow |
|---|---|---|
service-bank | Required | Estimate → Initiate |
service-cpn | Not used | Estimate → Initiate → Confirm |
service-offramp | Not used | Estimate → Initiate |
Prerequisites
| Requirement | Detail |
|---|---|
| Corridor capability | Each corridor maps to its own *Out3rdParty capability, which must be Active |
| Saved recipient | recipient_id and recipient_payment_details_id come from Recipients |
| Bank account | service-bank corridors additionally require an account_id |
Capability names follow the corridor: sepa → SepaOut3rdParty, pix → PixOut3rdParty,
fps_hk → FpsHkOut3rdParty, and so on. Check them with GET /api/v1/capabilities before offering
a corridor in your UI — an inactive capability fails the estimate, not the transfer, so the user
would otherwise hit the error after entering an amount.
Step 1: Estimate
POST /api/v3/bank/estimate/{corridor}
Produces an id (the estimation identifier) consumed by step 2, and the set of token amounts that
would fund the payout.
Request
{
"destination_amount": 100.00,
"destination_currency": "INR",
"recipient_id": "550e8400-e29b-41d4-a716-446655440000",
"recipient_payment_details_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"source_tokens": ["0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"]
}| Field | Type | Required | Description |
|---|---|---|---|
destination_amount | number | Yes | Amount the recipient receives, in the corridor's currency. Must be greater than 0 |
destination_currency | string | No | Corridor currency. When sent, must equal the corridor's currency exactly |
recipient_id | string | Yes | Saved recipient (UUID) |
recipient_payment_details_id | string | Yes | Which of the recipient's payment details to pay (UUID) |
account_id | string | Conditional | Required for service-bank corridors (sepa, ach, faster-payments); not used otherwise |
source_tokens | string[] | No | Restrict funding to these token contract addresses. All must be valid addresses |
destination_amount is what the recipient receives. The estimate works backwards from it to the
tokens that must be spent — so the figure you show the user as "you will pay" comes from the
response, not the request.
Response
{
"id": "00000000-0000-0000-0000-000000000001",
"expires_at": 1617223200,
"amount": 100.00,
"currency": "INR",
"estimated_amounts": [
{
"amount": 109.41123037,
"precise_amount": "109411230370000000000",
"token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"token_symbol": "USDC",
"rate": 1.09345623,
"fee_amount": 0.5,
"fee_precise_amount": "500000000000000000"
}
]
}| Field | Description |
|---|---|
id | Estimation identifier. Pass as estimation_id to step 2 |
expires_at | Unix timestamp after which the estimate is no longer valid |
amount | Destination amount, echoing the request |
currency | Destination currency |
estimated_amounts | One entry per token that could fund the payout |
estimated_amounts[].amount | Amount in the token, scaled to the token's precision |
estimated_amounts[].precise_amount | Same amount in the token's smallest unit |
estimated_amounts[].token_address | Token contract address. This is the value for source_token_address in step 2 |
estimated_amounts[].token_symbol | Token symbol |
estimated_amounts[].rate | Token-to-destination-currency rate |
estimated_amounts[].fee_amount | Per-token fee, token precision. Absent when the upstream estimator reports no per-token fee |
estimated_amounts[].fee_precise_amount | Same fee in the token's smallest unit. Absent under the same condition |
estimated_amounts is a list of options, not a bill. Pick one entry and pass its token_address as
source_token_address in step 2. See Amounts for the
amount / precise_amount distinction.
Step 2: Initiate
POST /api/v3/bank/initiate/{corridor}
Request
{
"estimation_id": "00000000-0000-0000-0000-000000000001",
"source_token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"reference": "Payment for the services"
}| Field | Type | Required | Description |
|---|---|---|---|
estimation_id | string | Yes | The id from step 1. Must be a valid UUID |
source_token_address | string | Yes | Token to spend. Must be a valid, non-zero address from estimated_amounts |
reference | string | No | Free-text reference carried on the payment |
Response
{
"transfer_id": "00000000-0000-0000-0000-000000000001",
"status": "pending",
"required_actions": [
{
"type": "FundTransfer",
"data": {
"destination_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"amount": "100000000",
"chain_id": 8453,
"expires_at": 1735689600
}
}
]
}| Field | Description |
|---|---|
transfer_id | Transfer identifier. Required by step 3 when there is one |
status | Transfer status |
required_actions | Present only when the corridor needs something from you. Absent means the transfer is already in flight |
required_actions[].type | Action type. FundTransfer is the only type currently issued |
required_actions[].data.destination_address | Address to send funds to |
required_actions[].data.token_address | Token to send |
required_actions[].data.amount | Amount in the token's smallest unit |
required_actions[].data.chain_id | Chain to send on |
required_actions[].data.expires_at | Unix timestamp after which the action is no longer accepted |
Branch on required_actions, not on the corridor name. When it is absent or empty, the transfer
is submitted and you are done. When it is present, perform the action on-chain and continue to step 3.
Step 3: Confirm
POST /api/v3/bank/confirm/{corridor}
Only CPN corridors accept this call. Any other corridor is refused with
corridor: Confirm step is supported only for CPN corridors.
Send the funds described in required_actions on-chain first, then report the hash.
Request
{
"transfer_id": "00000000-0000-0000-0000-000000000001",
"actions": [
{
"type": "FundTransfer",
"data": {
"tx_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
transfer_id | string | Yes | The transfer_id from step 2. Must be a valid UUID |
actions | array | Yes | Exactly one action. Zero or more than one is refused |
actions[].type | string | Yes | Must be FundTransfer |
actions[].data | object | Yes | Action payload |
actions[].data.tx_hash | string | Yes | Hash of the funding transaction. 0x-prefixed, valid hex, non-empty after the prefix |
Response
{
"transfer_id": "00000000-0000-0000-0000-000000000001",
"status": "processing",
"tx_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}| Field | Description |
|---|---|
transfer_id | Transfer identifier |
status | Transfer status after confirmation |
tx_hash | Funding transaction hash, when recorded |
Code Examples
const headers = {
'Authorization': `Bearer ${accessToken}`,
'X-User-Wallet': userEoaAddress,
'X-Chain-Id': chainId,
'Content-Type': 'application/json'
};
// Step 1: estimate
const estimate = await fetch(`${baseUrl}/api/v3/bank/estimate/${corridor}`, {
method: 'POST',
headers,
body: JSON.stringify({
destination_amount: 100.00,
recipient_id: recipientId,
recipient_payment_details_id: recipientPaymentDetailsId
})
}).then(r => r.json());
const funding = estimate.estimated_amounts[0];
// Step 2: initiate
const transfer = await fetch(`${baseUrl}/api/v3/bank/initiate/${corridor}`, {
method: 'POST',
headers,
body: JSON.stringify({
estimation_id: estimate.id,
source_token_address: funding.token_address,
reference: 'Payment for the services'
})
}).then(r => r.json());
// Step 3: only when the corridor asked for it
if (transfer.required_actions?.length) {
const action = transfer.required_actions[0];
const txHash = await sendTokens(
action.data.destination_address,
action.data.token_address,
action.data.amount
);
await fetch(`${baseUrl}/api/v3/bank/confirm/${corridor}`, {
method: 'POST',
headers,
body: JSON.stringify({
transfer_id: transfer.transfer_id,
actions: [{ type: 'FundTransfer', data: { tx_hash: txHash } }]
})
});
}headers = {
"Authorization": f"Bearer {access_token}",
"X-User-Wallet": user_eoa_address,
"X-Chain-Id": chain_id,
"Content-Type": "application/json"
}
# Step 1: estimate
estimate = requests.post(
f"{base_url}/api/v3/bank/estimate/{corridor}",
headers=headers,
json={
"destination_amount": 100.00,
"recipient_id": recipient_id,
"recipient_payment_details_id": recipient_payment_details_id
}
).json()
funding = estimate["estimated_amounts"][0]
# Step 2: initiate
transfer = requests.post(
f"{base_url}/api/v3/bank/initiate/{corridor}",
headers=headers,
json={
"estimation_id": estimate["id"],
"source_token_address": funding["token_address"],
"reference": "Payment for the services"
}
).json()
# Step 3: only when the corridor asked for it
if transfer.get("required_actions"):
action = transfer["required_actions"][0]
tx_hash = send_tokens(
action["data"]["destination_address"],
action["data"]["token_address"],
action["data"]["amount"]
)
requests.post(
f"{base_url}/api/v3/bank/confirm/{corridor}",
headers=headers,
json={
"transfer_id": transfer["transfer_id"],
"actions": [{"type": "FundTransfer", "data": {"tx_hash": tx_hash}}]
}
)// Step 1: estimate
estimateBody, _ := json.Marshal(map[string]interface{}{
"destination_amount": 100.00,
"recipient_id": recipientId,
"recipient_payment_details_id": recipientPaymentDetailsId,
})
req, _ := http.NewRequest("POST", baseURL+"/api/v3/bank/estimate/"+corridor, bytes.NewReader(estimateBody))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Wallet", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var estimate BankCorridorEstimateResponse
json.NewDecoder(resp.Body).Decode(&estimate)
funding := estimate.EstimatedAmounts[0]
// Step 2: initiate
initiateBody, _ := json.Marshal(map[string]string{
"estimation_id": estimate.Id,
"source_token_address": funding.TokenAddress,
"reference": "Payment for the services",
})
req, _ = http.NewRequest("POST", baseURL+"/api/v3/bank/initiate/"+corridor, bytes.NewReader(initiateBody))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Wallet", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
req.Header.Set("Content-Type", "application/json")
resp, _ = http.DefaultClient.Do(req)
defer resp.Body.Close()
var transfer BankCorridorInitiateResponse
json.NewDecoder(resp.Body).Decode(&transfer)
// Step 3: only when the corridor asked for it
if len(transfer.RequiredActions) > 0 {
action := transfer.RequiredActions[0]
txHash := sendTokens(action.Data.DestinationAddress, action.Data.TokenAddress, action.Data.Amount)
confirmBody, _ := json.Marshal(map[string]interface{}{
"transfer_id": transfer.TransferId,
"actions": []map[string]interface{}{
{"type": "FundTransfer", "data": map[string]string{"tx_hash": txHash}},
},
})
req, _ = http.NewRequest("POST", baseURL+"/api/v3/bank/confirm/"+corridor, bytes.NewReader(confirmBody))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Wallet", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
req.Header.Set("Content-Type", "application/json")
resp, _ = http.DefaultClient.Do(req)
defer resp.Body.Close()
}Limitations
- Estimates expire.
expires_atis a Unix timestamp; an initiate against a lapsed estimate fails
and you must re-estimate rather than retry. - Funding actions expire separately.
required_actions[].data.expires_atis its own deadline,
and it starts running at initiate — not when your on-chain transfer confirms. - Exactly one action per confirm. The endpoint refuses an empty array and refuses more than one
entry, even thoughactionsis a list. FundTransferis the only action type. Any othertypevalue is refused.- Confirm is CPN-only. Calling it on a
service-bankorservice-offrampcorridor is an error,
not a no-op. - Per-token fees are not always reported.
fee_amountandfee_precise_amountare omitted when
the upstream estimator does not supply them — absence is not zero. destination_currencycannot override the corridor. It is validated against the corridor's own
currency and rejected on mismatch, so it can only ever restate what the path already fixed.
Error Handling
Validation Errors (400)
| Error Details | Description | Resolution |
|---|---|---|
corridor: Unknown or unsupported corridor: {value} | Corridor not in the registry | Use a value from Corridors |
corridor: Unsupported corridor: {value} | Same, raised during body validation | Use a supported corridor |
destination_amount: Amount should be greater than 0 | Zero or negative amount | Send a positive amount |
recipient_id: Recipient ID is not a valid UUID | Malformed recipient | Use the id from the recipients API |
recipient_payment_details_id: Recipient Payment Details ID is not a valid UUID | Malformed payment details | Use the id from the recipient's payment details |
account_id: Account ID is required for {corridor} corridor | Missing on a service-bank corridor | Send the account id |
account_id: Account ID is not a valid UUID | Malformed account id | Use the id from the accounts API |
source_tokens: Token address is invalid | An entry is not a valid address. token details the offending value | Send valid contract addresses |
destination_currency: Currency {x} not supported for corridor {y}, expected {z} | Currency does not match the corridor | Omit the field or send the corridor's currency |
estimation_id: Estimation ID is not a valid UUID | Malformed estimation | Use id from the estimate response |
source_token_address: SourceTokenAddress is required and should be a valid Ethereum address | Missing or zero address | Use a token_address from estimated_amounts |
transfer_id: Transfer ID is not a valid UUID | Malformed transfer | Use transfer_id from the initiate response |
actions: Exactly one action is required | Zero or multiple actions | Send exactly one |
actions: Action item cannot be empty | Null entry in the array | Send a populated action |
actions.type: Unsupported action type: {value} | Type other than FundTransfer | Send FundTransfer |
actions.data: Action data is required | Missing data object | Include the payload |
actions.data.tx_hash: tx_hash is required | Empty hash | Send the funding transaction hash |
actions.data.tx_hash: tx_hash must have 0x prefix | Missing prefix | Prefix the hash |
actions.data.tx_hash: tx_hash payload is empty | Only 0x sent | Send the full hash |
actions.data.tx_hash: tx_hash must be a valid hex string | Non-hex characters | Send a hex-encoded hash |
corridor: Confirm step is supported only for CPN corridors | Confirm called on a non-CPN corridor | Skip the confirm step |
corridor: Unknown provider for corridor: {value} | Registry entry has no provider mapping | Contact Wirex |
corridor: Invalid corridor type | Corridor has no capability mapping | Contact Wirex |
Example Error Response
{
"error_reason": "ErrorInvalidField",
"error_description": "Request failed validation",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "account_id", "details": "Account ID is required for sepa corridor" }
]
}Capability Errors
An inactive corridor capability fails all three endpoints. The estimate is where you will see it
first. Resolve through Capabilities — the request itself is not the
problem.
Updated 20 days ago

