SEPA Bank Details
Get SEPA bank account details for receiving EUR transfers.
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
Each verified user can receive EUR transfers via SEPA. The system provides dedicated IBAN and BIC details for each user. Incoming SEPA transfers are automatically converted to the user's unified balance (WEUR).
SEPA accounts require activation. Verification alone does not provision an account — call Activate SEPA Account once the SepaAccount capability reports that activation is available.
Changed: SEPA accounts were previously provisioned automatically when verification completed. They now follow the same activation model as ACH. An integration that assumed an IBAN would simply appear after KYC will find no account until it activates one.
Checking SEPA Availability
Before activating or retrieving bank details, check the user's SepaAccount capability. Its status decides which activation path applies.
GET /api/v2/user
The response includes the SepaAccount capability:
{
"capabilities": [
{
"type": "SepaAccount",
"status": "ActivationNotStarted"
}
]
}Capability Status
| Status | Description | What to do |
|---|---|---|
ActivationNotStarted | Activation is available and not yet started | Activate the account |
ExternalProviderVerificationRequired | Wallet ownership must be proven to an external provider first | Run the wallet linking flow |
ExternalProviderRegistrationPending | Registration with the external provider is still in progress | Wait, then re-check |
InProgress | Activation requested, waiting for provisioning | Wait for the bank account webhook |
Active | Account is provisioned and ready to use | Retrieve details |
NotFulfilled | User needs to complete additional verification first | Complete the required verification level |
NotRequired | Capability does not apply to this user | Nothing |
NotAvailable | Not available for the user's country of residence | Nothing |
See Bank Account Availability for supported countries, and ACH Bank Details for the equivalent USD flow.
Activate SEPA Account
There are two activation paths. The SepaAccount capability status tells you which one applies — do not
guess, and do not call both.
| Capability status | Path | Calls |
|---|---|---|
ActivationNotStarted | Standard activation | POST /api/v1/bank/accounts |
ExternalProviderVerificationRequired | Activation with wallet linking | POST /api/v1/bank/accounts/init → sign → POST /api/v1/bank/accounts/complete |
Both paths return the same response and leave the capability in InProgress. The account is ready when the
bank account webhook reports the SEPA details.
Standard Activation
POST /api/v1/bank/accounts
Request body:
{
"account_type": "Sepa"
}| Field | Type | Required | Description |
|---|---|---|---|
account_type | string | Yes | Sepa for a EUR account. Also accepts Ach, FasterPayment, Spei |
const response = await fetch(`${baseUrl}/api/v1/bank/accounts`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Wallet': userEoaAddress,
'X-Chain-Id': chainId,
'Content-Type': 'application/json'
},
body: JSON.stringify({ account_type: 'Sepa' })
});
const result = await response.json();
console.log('Details ID:', result.details_id);response = requests.post(
f"{base_url}/api/v1/bank/accounts",
headers={
"Authorization": f"Bearer {access_token}",
"X-User-Wallet": user_eoa_address,
"X-Chain-Id": chain_id,
"Content-Type": "application/json"
},
json={"account_type": "Sepa"}
)
result = response.json()
print("Details ID:", result["details_id"])body, _ := json.Marshal(map[string]string{"account_type": "Sepa"})
req, _ := http.NewRequest("POST", baseURL+"/api/v1/bank/accounts", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Wallet", 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 BankAccountActivateResponse
json.NewDecoder(resp.Body).Decode(&result)Response:
{
"details_id": "8d5a65eb59d94afea64374d45591fe9f"
}Calling this endpoint when the capability does not need activation returns 400 with
Account capability does not need activation or is not available.
Activation with Wallet Linking
When the capability reports ExternalProviderVerificationRequired, the user's Smart Wallet must prove
ownership to an external provider before the SEPA account can be ordered. This is a three-step flow.
Step 1: Request the challenge
POST /api/v1/bank/accounts/init
Request body:
{
"chain": "8453"
}| Field | Type | Required | Description |
|---|---|---|---|
chain | string | Yes | The chain ID of the wallet to link, as a numeric string |
Despite the field name and the
"polygon"example in the API reference,chainmust be a numeric chain
ID sent as a string —"8453", not"base". A non-numeric value returns400 ErrorInvalidFieldwith
Chain id should be a number. Note also that the error names the fieldchain_idwhile the request
field ischain.
Response:
{
"challenge": "Sign this message to verify ownership of 0xAAFF...874",
"wallet_address": "0xAAFF0821A09A1Aac28B72dD3Ff410A7ea5FEb874",
"chain": "8453"
}| Field | Description |
|---|---|
challenge | The message the wallet must sign |
wallet_address | The Primary Smart Wallet that must produce the signature |
chain | Echoes the requested chain |
The wallet is resolved server-side as the user's Primary wallet. If the user has none, the call returns
400 with Wallet not found.
Step 2: Sign the challenge
Sign challenge with the wallet identified by wallet_address. Keep the signature.
Step 3: Complete activation
POST /api/v1/bank/accounts/complete
Request body:
{
"chain": "8453",
"signed_challenge": "0x1234abcd..."
}| Field | Type | Required | Description |
|---|---|---|---|
chain | string | Yes | Same numeric chain ID string sent to init |
signed_challenge | string | Yes | Wallet signature over the challenge from Step 1 |
Response: identical to standard activation.
{
"details_id": "8d5a65eb59d94afea64374d45591fe9f"
}// Step 1
const init = await fetch(`${baseUrl}/api/v1/bank/accounts/init`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}`, 'X-User-Wallet': userEoaAddress, 'X-Chain-Id': chainId, 'Content-Type': 'application/json' },
body: JSON.stringify({ chain: String(chainId) })
});
const { challenge } = await init.json();
// Step 2: sign `challenge` with the user's Primary wallet
const signedChallenge = await signMessage(challenge);
// Step 3
const done = await fetch(`${baseUrl}/api/v1/bank/accounts/complete`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${accessToken}`, 'X-User-Wallet': userEoaAddress, 'X-Chain-Id': chainId, 'Content-Type': 'application/json' },
body: JSON.stringify({ chain: String(chainId), signed_challenge: signedChallenge })
});
const { details_id: detailsId } = await done.json();headers = {
"Authorization": f"Bearer {access_token}",
"X-User-Wallet": user_eoa_address,
"X-Chain-Id": chain_id,
"Content-Type": "application/json",
}
# Step 1
init = requests.post(f"{base_url}/api/v1/bank/accounts/init", headers=headers, json={"chain": str(chain_id)})
challenge = init.json()["challenge"]
# Step 2: sign `challenge` with the user's Primary wallet
signed_challenge = sign_message(challenge)
# Step 3
done = requests.post(
f"{base_url}/api/v1/bank/accounts/complete",
headers=headers,
json={"chain": str(chain_id), "signed_challenge": signed_challenge},
)
details_id = done.json()["details_id"]// Step 1
initBody, _ := json.Marshal(map[string]string{"chain": chainId})
initReq, _ := http.NewRequest("POST", baseURL+"/api/v1/bank/accounts/init", bytes.NewBuffer(initBody))
initReq.Header.Set("Authorization", "Bearer "+accessToken)
initReq.Header.Set("X-User-Wallet", userEoaAddress)
initReq.Header.Set("X-Chain-Id", chainId)
initReq.Header.Set("Content-Type", "application/json")
initResp, _ := http.DefaultClient.Do(initReq)
defer initResp.Body.Close()
var challengeData BankAccountInitResponse
json.NewDecoder(initResp.Body).Decode(&challengeData)
// Step 2: sign challengeData.Challenge with the user's Primary wallet
signedChallenge := signMessage(challengeData.Challenge)
// Step 3
body, _ := json.Marshal(map[string]string{"chain": chainId, "signed_challenge": signedChallenge})
req, _ := http.NewRequest("POST", baseURL+"/api/v1/bank/accounts/complete", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Wallet", 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 BankAccountActivateResponse
json.NewDecoder(resp.Body).Decode(&result)Both init and complete are rejected with 400 and External provider wallet verification is not required for this account when the SepaAccount capability is in any other status.
Get Bank Accounts
Retrieve the user's bank account details including SEPA information.
GET /api/v1/bank/accounts
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page_number | integer | Page number, 0-indexed (default: 0) |
page_size | integer | Items per page (default: 25) |
Code Examples
const response = await fetch(`${baseUrl}/api/v1/bank/accounts`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-User-Wallet': userEoaAddress,
'X-Chain-Id': chainId
}
});
const { accounts } = await response.json();
const sepaAccount = accounts.find(a => a.account_type === 'Sepa');
if (sepaAccount) {
console.log('IBAN:', sepaAccount.details.iban);
console.log('BIC:', sepaAccount.details.bic);
}response = requests.get(
f"{base_url}/api/v1/bank/accounts",
headers={
"Authorization": f"Bearer {access_token}",
"X-User-Wallet": user_eoa_address,
"X-Chain-Id": chain_id
}
)
accounts = response.json()["accounts"]
sepa_account = next((a for a in accounts if a["account_type"] == "Sepa"), None)
if sepa_account:
print("IBAN:", sepa_account["details"]["iban"])
print("BIC:", sepa_account["details"]["bic"])req, _ := http.NewRequest("GET", baseURL+"/api/v1/bank/accounts", nil)
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("X-User-Wallet", userEoaAddress)
req.Header.Set("X-Chain-Id", chainId)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result struct {
Accounts []BankAccountResponse `json:"accounts"`
}
json.NewDecoder(resp.Body).Decode(&result)
for _, account := range result.Accounts {
if account.AccountType == "Sepa" {
fmt.Println("IBAN:", account.Details.IBAN)
fmt.Println("BIC:", account.Details.BIC)
}
}Response
{
"accounts": [
{
"id": "1334726cbd7641c09b4124e3e52f53fe:8d5a65eb59d94afea64374d45591fe9f",
"currency": "EUR",
"status": "Active",
"account_holder": "Alex Grey",
"account_type": "Sepa",
"details": {
"iban": "DE89370400440532013000",
"bic": "COBADEFFXXX"
}
}
]
}Response Fields
| Field | Description |
|---|---|
id | Composite identifier in format {account_id}:{details_id}. The account_id corresponds to the account id from webhooks, and details_id corresponds to the details id from webhooks. |
currency | Account currency (EUR for SEPA) |
status | Account status: Active, Pending, Blocked, Closed |
account_holder | Name on the account |
account_type | Sepa for SEPA accounts |
details.iban | IBAN for receiving transfers |
details.bic | BIC/SWIFT code |
Account Status
| Status | Description |
|---|---|
Pending | Account details being provisioned |
Active | Ready to receive transfers |
Blocked | Temporarily suspended |
Closed | Permanently closed |
Receiving SEPA Transfers
To receive a SEPA transfer:
- Retrieve the user's SEPA account details using the endpoint above
- Provide the IBAN and BIC to the sender
- Monitor incoming transfers via the Activities webhook
Important Notes
- Incoming EUR is automatically converted to WEUR in the user's unified balance
- SEPA transfers typically settle within 1-2 business days
Webhooks
Bank Account Updates
Wirex sends webhook notifications when bank account details are created or updated.
Endpoint: POST {your_webhook_base_url}/webhook/accounts/fiat
Account Created
{
"change_type": "Created",
"id": "1334726cbd7641c09b4124e3e52f53fe",
"account_type": "Fiat",
"currency": "EUR",
"status": "Active",
"balance": {
"amount": 0,
"available_amount": 0
},
"details": [],
"owner_type": "Personal",
"created_at": "2024-01-15T10:00:00Z"
}Details Changed (SEPA Details Added)
{
"change_type": "DetailsChanged",
"id": "1334726cbd7641c09b4124e3e52f53fe",
"account_type": "Fiat",
"currency": "EUR",
"status": "Active",
"balance": {
"amount": 0,
"available_amount": 0
},
"details": [
{
"id": "8d5a65eb59d94afea64374d45591fe9f",
"type": "Sepa",
"status": "Pending",
"transport_currency": "EUR"
}
],
"owner_type": "Personal"
}Incoming SEPA Transfer
When a SEPA transfer is received, you receive an activity webhook.
Endpoint: POST {your_webhook_base_url}/v2/webhooks/activities
{
"id": "ea6fbc2c-b8da-4a7b-99d1-6a2220352d02",
"user_address": "0x1d595bFAc81F231Ebc30950B8B08F2beEb97934B",
"type": "Sepa",
"status": "Completed",
"direction": "Inbound",
"source": {
"type": "SepaBankAccount",
"bank_account": {
"iban": "GB82WEST12345698765432",
"bic": "WESTGB2L",
"owner_name": "Alex Grey",
"is_business": false
}
},
"destination": {
"type": "Wallet",
"wallet": {
"address": "0x6fb0fCA78F4b717fbAaB89c96754200355554832"
}
},
"source_amount": {
"amount": 55.93,
"currency": "EUR"
},
"destination_amount": {
"amount": 55.93,
"token_symbol": "WEUR",
"token_address": "0x5c55F314624718019A326F16a62A05D6C6d8C8A2"
},
"reference": "Payment reference",
"operations": [
{
"hash": "0x456789abcdef123456789abcdef123456789abcdef123456789abcdef12345678",
"operation_amount": {
"amount": 55.93,
"token_symbol": "WEUR",
"token_address": "0x5c55F314624718019A326F16a62A05D6C6d8C8A2"
},
"transaction_amount": {
"amount": 55.93,
"currency": "EUR"
},
"rate": {
"ticker": "EUR/WEUR",
"rate": 1
}
}
],
"created_at": "2024-01-01T09:00:00.000Z",
"activity_steps": [
{
"type": "Initiated",
"status": "Completed",
"created_at": "2024-01-01T09:00:00.000Z",
"completed_at": "2024-01-01T09:00:00.000Z"
},
{
"type": "BankIn",
"status": "Completed",
"created_at": "2024-01-01T09:00:00.000Z",
"completed_at": "2024-01-01T09:00:30.000Z"
},
{
"type": "CryptoIn",
"status": "Completed",
"created_at": "2024-01-01T09:00:30.000Z",
"completed_at": "2024-01-01T09:00:45.000Z"
}
]
}Error Handling
All errors use the standard envelope:
{
"error_reason": "ErrorInvalidField",
"error_description": "Human-readable message",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "account_type" }
]
}error_category.category is one of CategoryValidationFailure (400), CategoryUnauthorized (401),
CategoryTransientFailure (429), CategoryInternalFailure (500), or CategoryTransportFailure (503).
These endpoints never return 404 — a missing or ineligible resource is reported as a 400.
Activation Errors (400)
Capability Does Not Need Activation
Returned by POST /api/v1/bank/accounts when the SepaAccount capability is not in a state that can be
activated — most often because it is already Active or InProgress, or is NotAvailable for the user's
country. Re-read the capability rather than retrying.
{
"error_reason": "ErrorGeneral",
"error_description": "Account capability does not need activation or is not available",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": []
}Missing Account Type
{
"error_reason": "ErrorMissingField",
"error_description": "account type is required",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "account_type" },
{ "key": "issue", "details": "missing" },
{ "key": "account_type", "details": "account type is required" }
]
}Invalid Account Type
Accepted values are Sepa, Ach, FasterPayment, and Spei.
{
"error_reason": "ErrorInvalidField",
"error_description": "Invalid value for account type",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "account_type" },
{ "key": "issue", "details": "invalid_value" },
{ "key": "account_type", "details": "Invalid value for account type" },
{ "key": "expected", "details": "Ach|Spei" }
]
}The
expecteddetail readsAch|Speiand is out of date —SepaandFasterPaymentare also accepted.
Validate against the list above, not against that field.
Wallet Linking Errors (400)
Returned by POST /api/v1/bank/accounts/init and POST /api/v1/bank/accounts/complete.
Verification Not Required
The SepaAccount capability is not ExternalProviderVerificationRequired, so this path does not apply.
Use Standard Activation instead.
{
"error_reason": "ErrorGeneral",
"error_description": "External provider wallet verification is not required for this account",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "capability", "details": "SepaAccount" }
]
}Invalid or Unsupported Chain
chain was not numeric, or names a chain the platform does not support. Note that error_details reports
the field as chain_id while the request field is chain.
{
"error_reason": "ErrorInvalidField",
"error_description": "Chain id should be a number",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "chain_id" },
{ "key": "issue", "details": "invalid_format" },
{ "key": "chain_id", "details": "Chain id should be a number" }
]
}A supported-but-unknown chain returns the same shape with "issue": "invalid_chain_id" and the description
Chain id not supported.
No Primary Wallet
The user has no Primary Smart Wallet to sign the challenge. Complete wallet deployment first — see
On-Chain Registration.
{
"error_reason": "ErrorInvalidField",
"error_description": "Wallet not found",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "wallet_address" },
{ "key": "issue", "details": "wallet_not_found" },
{ "key": "owner_address", "details": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE" }
]
}Signature Not Verified
The external provider rejected the signature. Re-run Step 1 to obtain a
fresh challenge — do not resubmit the same one.
{
"error_reason": "ErrorInvalidField",
"error_description": "Wallet ownership could not be verified by the external provider",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{ "key": "field", "details": "signed_challenge" },
{ "key": "issue", "details": "not_verified" }
]
}Missing Fields
init requires chain; complete requires both chain and signed_challenge. A missing value returns
ErrorMissingField with field set to the offending name and issue set to missing.
Server Errors (500)
| Error Description | Cause |
|---|---|
| Failed to get user | Account service unavailable |
| Failed to get bank accounts | Banking service unavailable |
| Failed to build capability context | Capability evaluation failed |
| Failed to get wallet challenge from external provider | External provider unavailable during init |
| Failed to link wallet with external provider | External provider unavailable during complete |
Updated 12 days ago

