SEPA Bank Transfer
Send EUR to external SEPA bank accounts.
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 access tokens |
| Onboarding | User and wallet registration |
| KYC | KYC verification requirements |
| Recipients | Recipient management |
Overview
Users can send EUR from their unified balance to external SEPA bank accounts. The transfer flow consists of two steps:
- Estimate — Get the token amount required for the transfer
- 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
| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | Yes | User's SEPA account ID in {account_id}:{details_id} format |
amount | number | Yes | EUR amount to send |
tokens | array | No | Token addresses to estimate against. If omitted, estimates all available tokens |
recipient | object | Yes | Recipient details |
recipient.first_name | string | Yes* | Recipient first name |
recipient.last_name | string | Yes* | Recipient last name |
recipient.company_name | string | Yes* | Company name (for business recipients) |
recipient_account | object | Yes | Recipient bank account |
recipient_account.iban | string | Yes | Recipient IBAN |
recipient_account.bic | string | Yes | Recipient BIC/SWIFT code |
reference | string | No | Payment 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
| Field | Description |
|---|---|
id | Estimation ID (use in execute request) |
expires_at | Unix timestamp when estimation expires |
amount | Fiat amount to be transferred |
currency | Transfer currency (EUR for SEPA) |
estimated_amounts | Token amounts for each requested token |
estimated_amounts[].amount | Token amount (human-readable) |
estimated_amounts[].precise_amount | Token amount in smallest unit (wei) |
estimated_amounts[].token_address | Token contract address |
estimated_amounts[].token_symbol | Token symbol |
estimated_amounts[].rate | Exchange 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
| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | Yes | User's SEPA account ID |
token_address | string | Yes | Token to charge from user's balance |
estimation_id | string | Yes* | Estimation ID from step 1 |
amount | number | Yes* | EUR amount (if not using estimation) |
recipient | object | Yes | Recipient details (same as estimate) |
recipient_account | object | Yes | Recipient bank account (same as estimate) |
reference | string | No | Payment 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
| Step | Description |
|---|---|
Initiated | Transfer request received |
CryptoOut | Tokens debited from user's wallet |
Review | Compliance review (automatic for most transfers) |
BankOut | Funds sent to recipient's bank account |
Error Handling
Validation Errors
| Error Reason | Field | Description |
|---|---|---|
ErrorMissingField | account_id | Account ID is required |
ErrorInvalidField | account_id | Invalid account ID format |
ErrorNotFound | account_id | Account not found or not owned by user |
ErrorMissingField | recipient | Recipient details required |
ErrorNotSupported | recipient | Capability not active for this transfer type |
ErrorMissingField | recipient_account.iban | IBAN is required |
ErrorMissingField | recipient_account.bic | BIC is required |
ErrorInvalidField | recipient_account.iban | Invalid IBAN format |
ErrorInvalidField | recipient_account.bic | Invalid BIC format |
ErrorMissingField | amount | Amount or estimation_id required |
ErrorInvalidField | token_address | Invalid 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:
| Capability | Description |
|---|---|
SepaOut1stParty | Send SEPA transfers to own accounts (recipient name matches user's verified name) |
SepaOut3rdParty | Send 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.
Updated 8 days ago
