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:

GuideWhy
Getting StartedPlatform overview and setup
Api BasicsRequired headers and request configuration
AuthenticationHow to obtain the corporation token
Activating Bank Account DetailsWhere account_id comes from
CapabilitiesEach 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 transferv3 corridor transfer
RailsSEPA, ACH, Faster PaymentsThose three plus PIX, SPEI, FEDWIRE, SWIFT, CIPS, CHATS, FPS-HK, IMPS, InstaPay, BI-FAST, NIP, IPP, PSE
BeneficiaryInline in the request bodyA stored recipient, by recipient_id
StepsEstimate, executeEstimate, initiate, sometimes confirm
EndpointPOST /api/v1/bank/transferPOST /api/v3/bank/initiate/{corridor}
GuideThis pageCorridor 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/estimatePOST /api/v2/bank/transfer/estimate
Beneficiary in the requestNot acceptedrecipient and recipient_account accepted, and required for ACH
Use whenSEPA and Faster PaymentsACH, 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"
    }
  }
}
FieldTypeRequiredDescription
account_idstringYesThe composite id from GET /api/v1/bank/accounts<accountId>:<detailsId>
amountnumberYesAmount in the account's currency
tokensarray of stringNoToken addresses to quote against. Every token available to the corporation when omitted
referencestringNoComment attached to the transfer
recipientobjectConditionalBeneficiary identity. Required for ACH
recipient_accountobjectConditionalBeneficiary 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"
    }
  ]
}
FieldDescription
idThe estimation id. Pass it to the execute call as estimation_id
amountAmount that will be sent, in currency
currencyCurrency of the bank account
expires_atUnix 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_amountPer-token fee. Empty when the estimator reports no per-token fee

The estimate's field is id; the execute call's field is estimation_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"
  }
}
FieldTypeRequiredDescription
account_idstringYesThe composite account identifier. Determines the rail
token_addressstringYesToken to debit from the corporation wallet
recipientobjectYesBeneficiary identity
recipient_accountobjectYesBeneficiary bank details. Fields required depend on the rail
estimation_idstringConditionalRequired when amount is omitted. Uses the quoted rate
amountnumberConditionalRequired when estimation_id is omitted. Executes at current market rates
referencestringNoComment attached to the transfer

Response:

{
  "id": "64120850-73a1-4df5-a074-d463258c9deb"
}
FieldDescription
idIdentifier of the executed transaction. It is the activity feed item's id

Send estimation_id, not amount, unless you accept the market rate. With amount the 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 with 400 ErrorMissingField.

Beneficiary Identity

FieldRequired when
recipient.first_name, recipient.last_nameThe beneficiary is a person
recipient.company_nameThe 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:

RailRequired in recipient_account
SEPAiban, bic
ACHaccount_number, routing_number, legal_address
Faster Paymentsaccount_number, sort_code

legal_address requires line1, city, zip_code and country; line2 and state are optional.

legal_address is triggered by the ACH field pair, not by the rail. Any request carrying both
account_number and routing_number must also carry legal_address, or it is rejected with
recipient account.legal address is required for ACH.

An account_id whose details type is neither SEPA, ACH nor Faster Payments — a SPEI or SWIFT
account — is rejected with 400 ErrorNotSupported and Invalid account id. Use
Corridor Transfers for those rails.

Capability per Rail

The capability checked is the outbound one for the account's details type:

RailCapability checked
SEPASepaOut1stParty
ACHAchOut1stParty
Faster PaymentsFasterPaymentsOut3rdParty
SPEISpeiOut1stParty — reachable only through the corridor endpoints

The 1stParty capabilities cover transfers to accounts the corporation owns. Faster Payments is
checked against FasterPaymentsOut3rdParty instead. A corporation holding only
SepaOut3rdParty cannot send a SEPA transfer through this endpoint — it needs SepaOut1stParty.

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

  1. The chosen token is debited from the corporation wallet.
  2. The fiat transfer is submitted on the rail.
  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 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 ReasonError DetailsDescriptionResolution
ErrorMissingFieldfield: account_idaccount id is requiredSend the composite id
ErrorInvalidFieldissue: invalid_formatInvalid format for account idThe value must be two UUIDs joined by :
ErrorNotFoundissue: not_found_or_not_ownedInvalid account idThe account does not belong to this corporation
ErrorNotSupportedissue: unsupported_bank_account_typeInvalid account idThe rail is not SEPA, ACH or Faster Payments. Use a corridor transfer
ErrorInvalidFieldfield: token_addresstoken address must be a valid addressSend a token address from the estimate
ErrorMissingFieldfield: amount, field: estimation_idamount is required when estimation id missingSend one of them
ErrorInvalidFieldfield: estimation_idestimation id must be a valid UUIDSend the estimate's id
ErrorMissingFieldfield: recipient_account.ibanSEPA requires an IBANSend iban and bic
ErrorMissingFieldfield: recipient_account.bicSEPA requires a BICAs above
ErrorMissingFieldfield: recipient_account.account_numberACH or Faster Payments requires an account numberSend the rail's required pair
ErrorMissingFieldfield: recipient_account.sort_codeFaster Payments requires a sort codeSend sort_code
ErrorMissingFieldissue: required_for_achrecipient account.legal address is required for ACHSend legal_address with line1, city, zip_code, country
ErrorGeneralCapability is not activeThe outbound capability for this rail is not Active

Permission Errors (403)

Error ReasonDescriptionResolution
ErrorPermissionDeniedUser does not have required permissionsTransfers need TransactionCreate or TransactionSu

Server Errors (500)

Error ReasonDescriptionResolution
ErrorGeneralEstimate failureThe rate service or bank service was unavailable. Retry
ErrorGeneralExecution failureThe transfer was not submitted. Re-estimate before retrying — the previous estimate may have expired

Did this page help you?