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:

GuideWhy
Getting StartedPlatform overview and setup
Api BasicsRequired headers and request configuration
AuthenticationHow to obtain access tokens
RecipientsCorridor transfers pay a saved recipient
CapabilitiesEach 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

CorridorPayment RailCurrencyProviderConfirm step
sepaSEPAEURservice-bankNo
achACHUSDservice-bankNo
faster-paymentsFaster PaymentsGBPservice-bankNo
pixPIXBRLservice-cpnYes
speiSPEIMXNservice-cpnYes
fedwireFEDWIREUSDservice-cpnYes
swiftWIREUSDservice-cpnYes
cipsCIPSCNYservice-cpnYes
chatsCHATSHKDservice-cpnYes
fps_hkFPSHKDservice-cpnYes
impsIMPS/NEFT/UPIINRservice-offrampNo
instapayInstaPay/PESONetPHPservice-offrampNo
bi-fastBI-FASTIDRservice-offrampNo
nipNIPNGNservice-offrampNo
ippIPPAEDservice-offrampNo
psePSECOPservice-offrampNo

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.

Provideraccount_id on estimateFlow
service-bankRequiredEstimate → Initiate
service-cpnNot usedEstimate → Initiate → Confirm
service-offrampNot usedEstimate → Initiate

Prerequisites

RequirementDetail
Corridor capabilityEach corridor maps to its own *Out3rdParty capability, which must be Active
Saved recipientrecipient_id and recipient_payment_details_id come from Recipients
Bank accountservice-bank corridors additionally require an account_id

Capability names follow the corridor: sepaSepaOut3rdParty, pixPixOut3rdParty,
fps_hkFpsHkOut3rdParty, 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"]
}
FieldTypeRequiredDescription
destination_amountnumberYesAmount the recipient receives, in the corridor's currency. Must be greater than 0
destination_currencystringNoCorridor currency. When sent, must equal the corridor's currency exactly
recipient_idstringYesSaved recipient (UUID)
recipient_payment_details_idstringYesWhich of the recipient's payment details to pay (UUID)
account_idstringConditionalRequired for service-bank corridors (sepa, ach, faster-payments); not used otherwise
source_tokensstring[]NoRestrict 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"
    }
  ]
}
FieldDescription
idEstimation identifier. Pass as estimation_id to step 2
expires_atUnix timestamp after which the estimate is no longer valid
amountDestination amount, echoing the request
currencyDestination currency
estimated_amountsOne entry per token that could fund the payout
estimated_amounts[].amountAmount in the token, scaled to the token's precision
estimated_amounts[].precise_amountSame amount in the token's smallest unit
estimated_amounts[].token_addressToken contract address. This is the value for source_token_address in step 2
estimated_amounts[].token_symbolToken symbol
estimated_amounts[].rateToken-to-destination-currency rate
estimated_amounts[].fee_amountPer-token fee, token precision. Absent when the upstream estimator reports no per-token fee
estimated_amounts[].fee_precise_amountSame 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"
}
FieldTypeRequiredDescription
estimation_idstringYesThe id from step 1. Must be a valid UUID
source_token_addressstringYesToken to spend. Must be a valid, non-zero address from estimated_amounts
referencestringNoFree-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
      }
    }
  ]
}
FieldDescription
transfer_idTransfer identifier. Required by step 3 when there is one
statusTransfer status
required_actionsPresent only when the corridor needs something from you. Absent means the transfer is already in flight
required_actions[].typeAction type. FundTransfer is the only type currently issued
required_actions[].data.destination_addressAddress to send funds to
required_actions[].data.token_addressToken to send
required_actions[].data.amountAmount in the token's smallest unit
required_actions[].data.chain_idChain to send on
required_actions[].data.expires_atUnix 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"
      }
    }
  ]
}
FieldTypeRequiredDescription
transfer_idstringYesThe transfer_id from step 2. Must be a valid UUID
actionsarrayYesExactly one action. Zero or more than one is refused
actions[].typestringYesMust be FundTransfer
actions[].dataobjectYesAction payload
actions[].data.tx_hashstringYesHash 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"
}
FieldDescription
transfer_idTransfer identifier
statusTransfer status after confirmation
tx_hashFunding 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_at is 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_at is 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 though actions is a list.
  • FundTransfer is the only action type. Any other type value is refused.
  • Confirm is CPN-only. Calling it on a service-bank or service-offramp corridor is an error,
    not a no-op.
  • Per-token fees are not always reported. fee_amount and fee_precise_amount are omitted when
    the upstream estimator does not supply them — absence is not zero.
  • destination_currency cannot 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 DetailsDescriptionResolution
corridor: Unknown or unsupported corridor: {value}Corridor not in the registryUse a value from Corridors
corridor: Unsupported corridor: {value}Same, raised during body validationUse a supported corridor
destination_amount: Amount should be greater than 0Zero or negative amountSend a positive amount
recipient_id: Recipient ID is not a valid UUIDMalformed recipientUse the id from the recipients API
recipient_payment_details_id: Recipient Payment Details ID is not a valid UUIDMalformed payment detailsUse the id from the recipient's payment details
account_id: Account ID is required for {corridor} corridorMissing on a service-bank corridorSend the account id
account_id: Account ID is not a valid UUIDMalformed account idUse the id from the accounts API
source_tokens: Token address is invalidAn entry is not a valid address. token details the offending valueSend valid contract addresses
destination_currency: Currency {x} not supported for corridor {y}, expected {z}Currency does not match the corridorOmit the field or send the corridor's currency
estimation_id: Estimation ID is not a valid UUIDMalformed estimationUse id from the estimate response
source_token_address: SourceTokenAddress is required and should be a valid Ethereum addressMissing or zero addressUse a token_address from estimated_amounts
transfer_id: Transfer ID is not a valid UUIDMalformed transferUse transfer_id from the initiate response
actions: Exactly one action is requiredZero or multiple actionsSend exactly one
actions: Action item cannot be emptyNull entry in the arraySend a populated action
actions.type: Unsupported action type: {value}Type other than FundTransferSend FundTransfer
actions.data: Action data is requiredMissing data objectInclude the payload
actions.data.tx_hash: tx_hash is requiredEmpty hashSend the funding transaction hash
actions.data.tx_hash: tx_hash must have 0x prefixMissing prefixPrefix the hash
actions.data.tx_hash: tx_hash payload is emptyOnly 0x sentSend the full hash
actions.data.tx_hash: tx_hash must be a valid hex stringNon-hex charactersSend a hex-encoded hash
corridor: Confirm step is supported only for CPN corridorsConfirm called on a non-CPN corridorSkip the confirm step
corridor: Unknown provider for corridor: {value}Registry entry has no provider mappingContact Wirex
corridor: Invalid corridor typeCorridor has no capability mappingContact 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.


Did this page help you?