ACH Bank Transfer

Send USD to external ACH bank accounts.

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
OnboardingUser and wallet registration
KYCKYC verification requirements
RecipientsRecipient management

Overview

Users can send USD from their unified balance to external ACH bank accounts. The transfer flow consists of two steps:

  1. Estimate — Get the token amount required for the transfer
  2. Execute — Initiate the transfer using the estimation

ACH transfers support both first-party (to user's own accounts) and third-party (to other recipients) transfers, controlled by separate capabilities.


Transfer Flow

Step 1: Estimate Transfer

Get an estimate for the token amount required to send a specific USD amount.

POST /api/v2/bank/transfer/estimate

Request Body

FieldTypeRequiredDescription
account_idstringYesUser's ACH account ID in {account_id}:{details_id} format
amountnumberYesUSD amount to send
tokensarrayNoToken addresses to estimate against. If omitted, estimates all available tokens
recipientobjectYesRecipient details
recipient.first_namestringYes*Recipient first name
recipient.last_namestringYes*Recipient last name
recipient.company_namestringYes*Company name (for business recipients)
recipient_accountobjectYesRecipient bank account
recipient_account.account_numberstringYesRecipient account number
recipient_account.routing_numberstringYesABA routing number
recipient_account.bank_namestringNoBank name
recipient_account.legal_addressobjectYesRecipient's address
referencestringNoPayment reference

*Provide either first_name + last_name for personal recipients, or company_name for business recipients.

Legal Address Fields

FieldTypeRequiredDescription
line1stringYesStreet address
line2stringNoAdditional address line
citystringYesCity
statestringYes (US)State code (required for US addresses)
zip_codestringYesPostal code
countrystringYesISO 3166-1 alpha-2 country code

Code Examples

const response = await fetch(`${baseUrl}/api/v2/bank/transfer/estimate`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'X-User-Address': userEoaAddress,
    'X-Chain-Id': chainId,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    account_id: '1334726cbd7641c09b4124e3e52f53fe:8d5a65eb59d94afea64374d45591fe9f',
    amount: 500.00,
    recipient: {
      first_name: 'Alex',
      last_name: 'Grey'
    },
    recipient_account: {
      account_number: '123456789012',
      routing_number: '026073150',
      bank_name: 'Bank of America',
      legal_address: {
        line1: '123 Main Street',
        city: 'New York',
        state: 'NY',
        zip_code: '10001',
        country: 'US'
      }
    },
    reference: 'Invoice #12345'
  })
});

const estimate = await response.json();
console.log('Estimation ID:', estimate.id);
response = requests.post(
    f"{base_url}/api/v2/bank/transfer/estimate",
    headers={
        "Authorization": f"Bearer {access_token}",
        "X-User-Address": user_eoa_address,
        "X-Chain-Id": chain_id,
        "Content-Type": "application/json"
    },
    json={
        "account_id": "1334726cbd7641c09b4124e3e52f53fe:8d5a65eb59d94afea64374d45591fe9f",
        "amount": 500.00,
        "recipient": {
            "first_name": "Alex",
            "last_name": "Grey"
        },
        "recipient_account": {
            "account_number": "123456789012",
            "routing_number": "026073150",
            "bank_name": "Bank of America",
            "legal_address": {
                "line1": "123 Main Street",
                "city": "New York",
                "state": "NY",
                "zip_code": "10001",
                "country": "US"
            }
        },
        "reference": "Invoice #12345"
    }
)

estimate = response.json()
print("Estimation ID:", estimate["id"])
body := map[string]interface{}{
    "account_id": "1334726cbd7641c09b4124e3e52f53fe:8d5a65eb59d94afea64374d45591fe9f",
    "amount": 500.00,
    "recipient": map[string]string{
        "first_name": "Alex",
        "last_name": "Grey",
    },
    "recipient_account": map[string]interface{}{
        "account_number": "123456789012",
        "routing_number": "026073150",
        "bank_name": "Bank of America",
        "legal_address": map[string]string{
            "line1": "123 Main Street",
            "city": "New York",
            "state": "NY",
            "zip_code": "10001",
            "country": "US",
        },
    },
    "reference": "Invoice #12345",
}

jsonBody, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", baseURL+"/api/v2/bank/transfer/estimate", bytes.NewBuffer(jsonBody))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Address", 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 BankTransferEstimateResponse
json.NewDecoder(resp.Body).Decode(&estimate)

Response

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "expires_at": 1704110400,
  "amount": 500.00,
  "currency": "USD",
  "estimated_amounts": [
    {
      "amount": 500.00,
      "precise_amount": "500000000",
      "token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
      "token_symbol": "WUSD",
      "rate": 1.0
    },
    {
      "amount": 460.83,
      "precise_amount": "460830000000000000000",
      "token_address": "0x5c55F314624718019A326F16a62A05D6C6d8C8A2",
      "token_symbol": "WEUR",
      "rate": 0.9217
    }
  ]
}

Response Fields

FieldDescription
idEstimation ID (use in execute request)
expires_atUnix timestamp when estimation expires
amountFiat amount to be transferred
currencyTransfer currency (USD for ACH)
estimated_amountsToken amounts for each requested token
estimated_amounts[].amountToken amount (human-readable)
estimated_amounts[].precise_amountToken amount in smallest unit (wei)
estimated_amounts[].token_addressToken contract address
estimated_amounts[].token_symbolToken symbol
estimated_amounts[].rateExchange rate (token per USD)

Step 2: Execute Transfer

Execute the transfer using the estimation ID or specify the amount directly.

POST /api/v1/bank/transfer

Request Body

FieldTypeRequiredDescription
account_idstringYesUser's ACH account ID
token_addressstringYesToken to charge from user's balance
estimation_idstringYes*Estimation ID from step 1
amountnumberYes*USD amount (if not using estimation)
recipientobjectYesRecipient details (same as estimate)
recipient_accountobjectYesRecipient bank account (same as estimate)
referencestringNoPayment reference

*Provide either estimation_id or amount, not both.

Code Examples

const response = await fetch(`${baseUrl}/api/v1/bank/transfer`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'X-User-Address': userEoaAddress,
    'X-Chain-Id': chainId,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    account_id: '1334726cbd7641c09b4124e3e52f53fe:8d5a65eb59d94afea64374d45591fe9f',
    token_address: '0x0774164DC20524Bb239b39D1DC42573C3E4C6976',
    estimation_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
    recipient: {
      first_name: 'Alex',
      last_name: 'Grey'
    },
    recipient_account: {
      account_number: '123456789012',
      routing_number: '026073150',
      bank_name: 'Bank of America',
      legal_address: {
        line1: '123 Main Street',
        city: 'New York',
        state: 'NY',
        zip_code: '10001',
        country: 'US'
      }
    },
    reference: 'Invoice #12345'
  })
});

const result = await response.json();
console.log('Transfer ID:', result.id);
response = requests.post(
    f"{base_url}/api/v1/bank/transfer",
    headers={
        "Authorization": f"Bearer {access_token}",
        "X-User-Address": user_eoa_address,
        "X-Chain-Id": chain_id,
        "Content-Type": "application/json"
    },
    json={
        "account_id": "1334726cbd7641c09b4124e3e52f53fe:8d5a65eb59d94afea64374d45591fe9f",
        "token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
        "estimation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "recipient": {
            "first_name": "Alex",
            "last_name": "Grey"
        },
        "recipient_account": {
            "account_number": "123456789012",
            "routing_number": "026073150",
            "bank_name": "Bank of America",
            "legal_address": {
                "line1": "123 Main Street",
                "city": "New York",
                "state": "NY",
                "zip_code": "10001",
                "country": "US"
            }
        },
        "reference": "Invoice #12345"
    }
)

result = response.json()
print("Transfer ID:", result["id"])
body := map[string]interface{}{
    "account_id": "1334726cbd7641c09b4124e3e52f53fe:8d5a65eb59d94afea64374d45591fe9f",
    "token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
    "estimation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "recipient": map[string]string{
        "first_name": "Alex",
        "last_name": "Grey",
    },
    "recipient_account": map[string]interface{}{
        "account_number": "123456789012",
        "routing_number": "026073150",
        "bank_name": "Bank of America",
        "legal_address": map[string]string{
            "line1": "123 Main Street",
            "city": "New York",
            "state": "NY",
            "zip_code": "10001",
            "country": "US",
        },
    },
    "reference": "Invoice #12345",
}

jsonBody, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", baseURL+"/api/v1/bank/transfer", bytes.NewBuffer(jsonBody))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Address", 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 result BankTransferExecuteResponse
json.NewDecoder(resp.Body).Decode(&result)

Response

{
  "id": "8b4f6e59-4287-4079-a3a3-3742557d07fd"
}

The id is the activity ID. Use this to track the transfer status via webhooks.


Webhooks

Outgoing ACH Transfer

Endpoint: POST {your_webhook_base_url}/v2/webhooks/activities

{
  "id": "8b4f6e59-4287-4079-a3a3-3742557d07fd",
  "user_address": "0x7e1Cc1685D68D486b22D3880030C24434E3b90a9",
  "type": "AchPush",
  "status": "Completed",
  "direction": "Outbound",
  "source": {
    "type": "Wallet",
    "wallet": {
      "address": "0xAAFF0821A09A1Aac28B72dD3Ff410A7ea5FEb874"
    }
  },
  "destination": {
    "type": "AchBankAccount",
    "bank_account": {
      "account_number": "8310931284",
      "routing_number": "026073150",
      "owner_name": "Alex Grey",
      "is_business": false
    }
  },
  "source_amount": {
    "amount": 34.64,
    "token_symbol": "WUSD",
    "token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976"
  },
  "destination_amount": {
    "amount": 34.64,
    "currency": "USD"
  },
  "operations": [
    {
      "hash": "0xdfe7dfe5633580cf9b34aa2f0021d9a3e53e8d97b99003cf20e3dce9ebba8dea",
      "operation_amount": {
        "amount": -34.64,
        "token_symbol": "WUSD",
        "token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976"
      },
      "rate": {
        "ticker": "USD/WUSD",
        "rate": 1
      }
    }
  ],
  "created_at": "2024-01-01T08:37:35.000Z",
  "activity_steps": [
    {
      "type": "Initiated",
      "status": "Completed",
      "created_at": "2024-01-01T08:37:35.000Z",
      "completed_at": "2024-01-01T08:37:35.000Z"
    },
    {
      "type": "CryptoOut",
      "status": "Completed",
      "created_at": "2024-01-01T08:37:35.000Z",
      "completed_at": "2024-01-01T08:37:35.000Z"
    },
    {
      "type": "BankOut",
      "status": "Completed",
      "created_at": "2024-01-01T08:37:35.000Z",
      "completed_at": "2024-01-01T08:37:47.000Z"
    }
  ]
}

Activity Steps

StepDescription
InitiatedTransfer request received
CryptoOutTokens debited from user's wallet
BankOutFunds sent to recipient's bank account

Error Handling

Validation Errors

Error ReasonFieldDescription
ErrorMissingFieldaccount_idAccount ID is required
ErrorInvalidFieldaccount_idInvalid account ID format
ErrorNotFoundaccount_idAccount not found or not owned by user
ErrorMissingFieldrecipientRecipient details required
ErrorMissingFieldrecipient_account.account_numberAccount number is required
ErrorMissingFieldrecipient_account.routing_numberRouting number is required
ErrorMissingFieldrecipient_account.legal_addressLegal address is required for ACH
ErrorMissingFieldlegal_address.stateState is required for US addresses
ErrorInvalidFieldrecipient_account.account_numberInvalid account number format
ErrorInvalidFieldrecipient_account.routing_numberInvalid routing number format
ErrorMissingFieldamountAmount or estimation_id required
ErrorInvalidFieldtoken_addressInvalid token address
ErrorNotSupportedrecipientCapability not active for this transfer type

Error Response Format

{
  "error_reason": "ErrorMissingField",
  "error_description": "recipient account.legal address is required for ACH",
  "error_category": {
    "category": "CategoryValidationFailure",
    "http_status_code": 400
  },
  "error_details": [
    {
      "key": "field",
      "details": "recipient_account.legal_address"
    },
    {
      "key": "issue",
      "details": "required_for_ach"
    }
  ]
}

Capabilities

The following capabilities control ACH transfer access:

CapabilityDescription
AchOut1stPartySend ACH transfers to own accounts (recipient name matches user's verified name)
AchOut3rdPartySend ACH transfers to third-party accounts (any recipient)

The system automatically determines which capability applies based on whether the recipient name matches the user's verified name.

Check capabilities via GET /api/v2/user before initiating transfers.