Bank Transfers
Estimate and execute a SEPA, ACH or Faster Payments transfer from a corporate bank account.
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 |
| Activating Bank Account Details | Where account_id comes from |
| Capabilities | Each rail is gated by its own capability |
Overview
A bank transfer sends fiat from a corporate bank account to a beneficiary, debiting a token balance from
the corporation wallet to fund it. The rail is not chosen by a field — it is implied by the
account_id, whose details type determines both the rail and the beneficiary fields required.
Two flows exist for the same outcome.
| v1 / v2 bank transfer | v3 corridor transfer | |
|---|---|---|
| Rails | SEPA, ACH, Faster Payments | Those three plus PIX, SPEI, FEDWIRE, SWIFT, CIPS, CHATS, FPS-HK, IMPS, InstaPay, BI-FAST, NIP, IPP, PSE |
| Beneficiary | Inline in the request body | A stored recipient, by recipient_id |
| Steps | Estimate, execute | Estimate, initiate, sometimes confirm |
| Endpoint | POST /api/v1/bank/transfer | POST /api/v3/bank/initiate/{corridor} |
| Guide | This page | Corridor Transfers |
Use v3 for anything beyond the three original rails, and whenever the beneficiary is already a stored
recipient. This page documents the v1 and v2 flow.
Step 1: Estimate the Transfer
Two estimate endpoints exist. They differ in one respect:
POST /api/v1/bank/transfer/estimate | POST /api/v2/bank/transfer/estimate | |
|---|---|---|
| Beneficiary in the request | Not accepted | recipient and recipient_account accepted, and required for ACH |
| Use when | SEPA and Faster Payments | ACH, or when you want the quote computed against the real beneficiary |
Both require TransactionSu or TransactionCreate.
POST /api/v2/bank/transfer/estimate
Request body:
{
"account_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6:64120850-73a1-4df5-a074-d463258c9deb",
"amount": 1000.00,
"reference": "Invoice 2024-0042",
"tokens": [
"0x0774164DC20524Bb239b39D1DC42573C3E4C6976"
],
"recipient": {
"first_name": "Alex",
"last_name": "Grey"
},
"recipient_account": {
"account_number": "12345678",
"routing_number": "123456789",
"bank_name": "The Bank of New York",
"legal_address": {
"line1": "10 Downing Street",
"city": "London",
"zip_code": "SW1A 2AA",
"country": "GB"
}
}
}| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | Yes | The composite id from GET /api/v1/bank/accounts — <accountId>:<detailsId> |
amount | number | Yes | Amount in the account's currency |
tokens | array of string | No | Token addresses to quote against. Every token available to the corporation when omitted |
reference | string | No | Comment attached to the transfer |
recipient | object | Conditional | Beneficiary identity. Required for ACH |
recipient_account | object | Conditional | Beneficiary bank details. Required for ACH |
Response:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"amount": 1000.00,
"currency": "EUR",
"expires_at": 1704107700,
"estimated_amounts": [
{
"amount": 1094.11,
"precise_amount": "1094110000000000000000",
"token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
"token_symbol": "WUSD",
"rate": 1.09411,
"fee_amount": 0.5,
"fee_precise_amount": "500000000000000000"
}
]
}| Field | Description |
|---|---|
id | The estimation id. Pass it to the execute call as estimation_id |
amount | Amount that will be sent, in currency |
currency | Currency of the bank account |
expires_at | Unix timestamp after which the estimate is no longer valid |
estimated_amounts[] | One entry per token — what would be debited if that token is chosen |
estimated_amounts[].fee_amount | Per-token fee. Empty when the estimator reports no per-token fee |
The estimate's field is
id; the execute call's field isestimation_id. They are the same value
under two names.
Step 2: Execute the Transfer
POST /api/v1/bank/transfer
Requires TransactionSu or TransactionCreate.
Request body:
{
"account_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6:64120850-73a1-4df5-a074-d463258c9deb",
"token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
"estimation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"reference": "Invoice 2024-0042",
"recipient": {
"first_name": "Alex",
"last_name": "Grey"
},
"recipient_account": {
"iban": "DE89370400440532013000",
"bic": "COBADEFF"
}
}| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | Yes | The composite account identifier. Determines the rail |
token_address | string | Yes | Token to debit from the corporation wallet |
recipient | object | Yes | Beneficiary identity |
recipient_account | object | Yes | Beneficiary bank details. Fields required depend on the rail |
estimation_id | string | Conditional | Required when amount is omitted. Uses the quoted rate |
amount | number | Conditional | Required when estimation_id is omitted. Executes at current market rates |
reference | string | No | Comment attached to the transfer |
Response:
{
"id": "64120850-73a1-4df5-a074-d463258c9deb"
}| Field | Description |
|---|---|
id | Identifier of the executed transaction. It is the activity feed item's id |
Send
estimation_id, notamount, unless you accept the market rate. Withamountthe transfer
executes at whatever the rate is at execution time, and the figure quoted to the corporation is not
what settles. Sending neither is rejected with400 ErrorMissingField.
Beneficiary Identity
| Field | Required when |
|---|---|
recipient.first_name, recipient.last_name | The beneficiary is a person |
recipient.company_name | The beneficiary is a business |
Beneficiary Bank Details by Rail
The rail comes from the details type behind account_id, and it decides which fields are mandatory:
| Rail | Required in recipient_account |
|---|---|
| SEPA | iban, bic |
| ACH | account_number, routing_number, legal_address |
| Faster Payments | account_number, sort_code |
legal_address requires line1, city, zip_code and country; line2 and state are optional.
legal_addressis triggered by the ACH field pair, not by the rail. Any request carrying both
account_numberandrouting_numbermust also carrylegal_address, or it is rejected with
recipient account.legal address is required for ACH.
An
account_idwhose details type is neither SEPA, ACH nor Faster Payments — a SPEI or SWIFT
account — is rejected with400 ErrorNotSupportedandInvalid account id. Use
Corridor Transfers for those rails.
Capability per Rail
The capability checked is the outbound one for the account's details type:
| Rail | Capability checked |
|---|---|
| SEPA | SepaOut1stParty |
| ACH | AchOut1stParty |
| Faster Payments | FasterPaymentsOut3rdParty |
| SPEI | SpeiOut1stParty — reachable only through the corridor endpoints |
The
1stPartycapabilities cover transfers to accounts the corporation owns. Faster Payments is
checked againstFasterPaymentsOut3rdPartyinstead. A corporation holding only
SepaOut3rdPartycannot send a SEPA transfer through this endpoint — it needsSepaOut1stParty.
Code
const estimateResponse = await fetch(`${baseUrl}/api/v2/bank/transfer/estimate`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${corporationToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ account_id: accountId, amount: 1000.00, tokens: [tokenAddress] })
});
const estimate = await estimateResponse.json();
const response = await fetch(`${baseUrl}/api/v1/bank/transfer`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${corporationToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
account_id: accountId,
token_address: tokenAddress,
estimation_id: estimate.id,
reference: 'Invoice 2024-0042',
recipient: { first_name: 'Alex', last_name: 'Grey' },
recipient_account: { iban: 'DE89370400440532013000', bic: 'COBADEFF' }
})
});
const { id: transactionId } = await response.json();estimate = requests.post(
f"{base_url}/api/v2/bank/transfer/estimate",
headers={
"Authorization": f"Bearer {corporation_token}",
"Content-Type": "application/json",
},
json={"account_id": account_id, "amount": 1000.00, "tokens": [token_address]},
).json()
response = requests.post(
f"{base_url}/api/v1/bank/transfer",
headers={
"Authorization": f"Bearer {corporation_token}",
"Content-Type": "application/json",
},
json={
"account_id": account_id,
"token_address": token_address,
"estimation_id": estimate["id"],
"reference": "Invoice 2024-0042",
"recipient": {"first_name": "Alex", "last_name": "Grey"},
"recipient_account": {"iban": "DE89370400440532013000", "bic": "COBADEFF"},
},
)
transaction_id = response.json()["id"]estimateBody, _ := json.Marshal(map[string]interface{}{
"account_id": accountID,
"amount": 1000.00,
"tokens": []string{tokenAddress},
})
estimateReq, _ := http.NewRequest("POST", baseURL+"/api/v2/bank/transfer/estimate", bytes.NewBuffer(estimateBody))
estimateReq.Header.Set("Authorization", "Bearer "+corporationToken)
estimateReq.Header.Set("Content-Type", "application/json")
estimateResp, _ := http.DefaultClient.Do(estimateReq)
defer estimateResp.Body.Close()
var estimate BankTransferEstimateResponse
json.NewDecoder(estimateResp.Body).Decode(&estimate)
body, _ := json.Marshal(map[string]interface{}{
"account_id": accountID,
"token_address": tokenAddress,
"estimation_id": estimate.Id,
"reference": "Invoice 2024-0042",
"recipient": map[string]string{"first_name": "Alex", "last_name": "Grey"},
"recipient_account": map[string]string{
"iban": "DE89370400440532013000",
"bic": "COBADEFF",
},
})
req, _ := http.NewRequest("POST", baseURL+"/api/v1/bank/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()What Happens Next
- The chosen token is debited from the corporation wallet.
- The fiat transfer is submitted on the rail.
- 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 debit to
/v2/webhooks/balances.
Settlement time is the rail's, not the API's: Faster Payments is near-instant, SEPA is same or next
business day, ACH takes one to three business days.
Error Handling
{
"error_reason": "ErrorInvalidField",
"error_description": "Invalid format for account id",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "account_id" },
{ "key": "issue", "details": "invalid_format" }
]
}Validation Errors (400)
| Error Reason | Error Details | Description | Resolution |
|---|---|---|---|
ErrorMissingField | field: account_id | account id is required | Send the composite id |
ErrorInvalidField | issue: invalid_format | Invalid format for account id | The value must be two UUIDs joined by : |
ErrorNotFound | issue: not_found_or_not_owned | Invalid account id | The account does not belong to this corporation |
ErrorNotSupported | issue: unsupported_bank_account_type | Invalid account id | The rail is not SEPA, ACH or Faster Payments. Use a corridor transfer |
ErrorInvalidField | field: token_address | token address must be a valid address | Send a token address from the estimate |
ErrorMissingField | field: amount, field: estimation_id | amount is required when estimation id missing | Send one of them |
ErrorInvalidField | field: estimation_id | estimation id must be a valid UUID | Send the estimate's id |
ErrorMissingField | field: recipient_account.iban | SEPA requires an IBAN | Send iban and bic |
ErrorMissingField | field: recipient_account.bic | SEPA requires a BIC | As above |
ErrorMissingField | field: recipient_account.account_number | ACH or Faster Payments requires an account number | Send the rail's required pair |
ErrorMissingField | field: recipient_account.sort_code | Faster Payments requires a sort code | Send sort_code |
ErrorMissingField | issue: required_for_ach | recipient account.legal address is required for ACH | Send legal_address with line1, city, zip_code, country |
ErrorGeneral | — | Capability is not active | The outbound capability for this rail is not Active |
Permission Errors (403)
| Error Reason | Description | Resolution |
|---|---|---|
ErrorPermissionDenied | User does not have required permissions | Transfers need TransactionCreate or TransactionSu |
Server Errors (500)
| Error Reason | Description | Resolution |
|---|---|---|
ErrorGeneral | Estimate failure | The rate service or bank service was unavailable. Retry |
ErrorGeneral | Execution failure | The transfer was not submitted. Re-estimate before retrying — the previous estimate may have expired |
Updated 20 days ago

