SEPA Bank Transfer

Send EUR to external SEPA 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 EUR from their unified balance to external SEPA 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

SEPA 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 EUR amount.

POST /api/v2/bank/transfer/estimate

Request Body

FieldTypeRequiredDescription
account_idstringYesUser's SEPA account ID in {account_id}:{details_id} format
amountnumberYesEUR 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.ibanstringYesRecipient IBAN
recipient_account.bicstringYesRecipient BIC/SWIFT code
referencestringNoPayment reference (max 140 characters)

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

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: 100.00,
    recipient: {
      first_name: 'Alex',
      last_name: 'Grey'
    },
    recipient_account: {
      iban: 'DE89370400440532013000',
      bic: 'COBADEFFXXX'
    },
    reference: 'Invoice payment'
  })
});

const estimate = await response.json();
console.log('Estimation ID:', estimate.id);
console.log('Expires at:', new Date(estimate.expires_at * 1000));
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": 100.00,
        "recipient": {
            "first_name": "Alex",
            "last_name": "Grey"
        },
        "recipient_account": {
            "iban": "DE89370400440532013000",
            "bic": "COBADEFFXXX"
        },
        "reference": "Invoice payment"
    }
)

estimate = response.json()
print("Estimation ID:", estimate["id"])
body := map[string]interface{}{
    "account_id": "1334726cbd7641c09b4124e3e52f53fe:8d5a65eb59d94afea64374d45591fe9f",
    "amount": 100.00,
    "recipient": map[string]string{
        "first_name": "Alex",
        "last_name": "Grey",
    },
    "recipient_account": map[string]string{
        "iban": "DE89370400440532013000",
        "bic": "COBADEFFXXX",
    },
    "reference": "Invoice payment",
}

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": 100.00,
  "currency": "EUR",
  "estimated_amounts": [
    {
      "amount": 109.41,
      "precise_amount": "109410000000000000000",
      "token_address": "0x5c55F314624718019A326F16a62A05D6C6d8C8A2",
      "token_symbol": "WEUR",
      "rate": 1.0
    },
    {
      "amount": 118.52,
      "precise_amount": "118520000",
      "token_address": "0x0774164DC20524Bb239b39D1DC42573C3E4C6976",
      "token_symbol": "WUSD",
      "rate": 1.0852
    }
  ]
}

Response Fields

FieldDescription
idEstimation ID (use in execute request)
expires_atUnix timestamp when estimation expires
amountFiat amount to be transferred
currencyTransfer currency (EUR for SEPA)
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 EUR)

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 SEPA account ID
token_addressstringYesToken to charge from user's balance
estimation_idstringYes*Estimation ID from step 1
amountnumberYes*EUR 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: '0x5c55F314624718019A326F16a62A05D6C6d8C8A2',
    estimation_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
    recipient: {
      first_name: 'Alex',
      last_name: 'Grey'
    },
    recipient_account: {
      iban: 'DE89370400440532013000',
      bic: 'COBADEFFXXX'
    },
    reference: 'Invoice payment'
  })
});

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": "0x5c55F314624718019A326F16a62A05D6C6d8C8A2",
        "estimation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "recipient": {
            "first_name": "Alex",
            "last_name": "Grey"
        },
        "recipient_account": {
            "iban": "DE89370400440532013000",
            "bic": "COBADEFFXXX"
        },
        "reference": "Invoice payment"
    }
)

result = response.json()
print("Transfer ID:", result["id"])
body := map[string]interface{}{
    "account_id": "1334726cbd7641c09b4124e3e52f53fe:8d5a65eb59d94afea64374d45591fe9f",
    "token_address": "0x5c55F314624718019A326F16a62A05D6C6d8C8A2",
    "estimation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "recipient": map[string]string{
        "first_name": "Alex",
        "last_name": "Grey",
    },
    "recipient_account": map[string]string{
        "iban": "DE89370400440532013000",
        "bic": "COBADEFFXXX",
    },
    "reference": "Invoice payment",
}

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": "c3d4e5f6-a7b8-9012-cdef-234567890123"
}

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


Webhooks

Outgoing SEPA Transfer

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

{
  "id": "c3d4e5f6-a7b8-9012-cdef-234567890123",
  "user_address": "0x1d595bFAc81F231Ebc30950B8B08F2beEb97934B",
  "type": "Sepa",
  "status": "Completed",
  "direction": "Outbound",
  "source": {
    "type": "Wallet",
    "wallet": {
      "address": "0x6fb0fCA78F4b717fbAaB89c96754200355554832"
    }
  },
  "destination": {
    "type": "SepaBankAccount",
    "bank_account": {
      "iban": "DE89370400440532013000",
      "bic": "COBADEFFXXX",
      "owner_name": "Alex Grey",
      "is_business": false
    }
  },
  "source_amount": {
    "amount": 200.00,
    "token_symbol": "WEUR",
    "token_address": "0x5c55F314624718019A326F16a62A05D6C6d8C8A2"
  },
  "destination_amount": {
    "amount": 200.00,
    "currency": "EUR"
  },
  "reference": "Invoice payment",
  "operations": [
    {
      "hash": "0x789abcdef123456789abcdef123456789abcdef123456789abcdef1234567890",
      "operation_amount": {
        "amount": -200.00,
        "token_symbol": "WEUR",
        "token_address": "0x5c55F314624718019A326F16a62A05D6C6d8C8A2"
      },
      "transaction_amount": {
        "amount": -200.00,
        "currency": "EUR"
      },
      "rate": {
        "ticker": "EUR/WEUR",
        "rate": 1
      }
    }
  ],
  "created_at": "2024-01-01T14:00:00.000Z",
  "activity_steps": [
    {
      "type": "Initiated",
      "status": "Completed",
      "created_at": "2024-01-01T14:00:00.000Z",
      "completed_at": "2024-01-01T14:00:00.000Z"
    },
    {
      "type": "CryptoOut",
      "status": "Completed",
      "created_at": "2024-01-01T14:00:00.000Z",
      "completed_at": "2024-01-01T14:00:10.000Z"
    },
    {
      "type": "Review",
      "status": "Completed",
      "created_at": "2024-01-01T14:00:10.000Z",
      "completed_at": "2024-01-01T14:00:15.000Z"
    },
    {
      "type": "BankOut",
      "status": "Completed",
      "created_at": "2024-01-01T14:00:15.000Z",
      "completed_at": "2024-01-01T14:01:00.000Z"
    }
  ]
}

Activity Steps

StepDescription
InitiatedTransfer request received
CryptoOutTokens debited from user's wallet
ReviewCompliance review (automatic for most transfers)
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
ErrorNotSupportedrecipientCapability not active for this transfer type
ErrorMissingFieldrecipient_account.ibanIBAN is required
ErrorMissingFieldrecipient_account.bicBIC is required
ErrorInvalidFieldrecipient_account.ibanInvalid IBAN format
ErrorInvalidFieldrecipient_account.bicInvalid BIC format
ErrorMissingFieldamountAmount or estimation_id required
ErrorInvalidFieldtoken_addressInvalid token address

Error Response Format

{
  "error_reason": "ErrorInvalidField",
  "error_description": "Invalid format for recipient account.iban",
  "error_category": {
    "category": "CategoryValidationFailure",
    "http_status_code": 400
  },
  "error_details": [
    {
      "key": "field",
      "details": "recipient_account.iban"
    },
    {
      "key": "issue",
      "details": "invalid_format"
    }
  ]
}

Capabilities

The following capabilities control SEPA transfer access:

CapabilityDescription
SepaOut1stPartySend SEPA transfers to own accounts (recipient name matches user's verified name)
SepaOut3rdPartySend SEPA 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.