SEPA Bank Details

Get SEPA bank account details for receiving EUR transfers.

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

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

StatusDescriptionWhat to do
ActivationNotStartedActivation is available and not yet startedActivate the account
ExternalProviderVerificationRequiredWallet ownership must be proven to an external provider firstRun the wallet linking flow
ExternalProviderRegistrationPendingRegistration with the external provider is still in progressWait, then re-check
InProgressActivation requested, waiting for provisioningWait for the bank account webhook
ActiveAccount is provisioned and ready to useRetrieve details
NotFulfilledUser needs to complete additional verification firstComplete the required verification level
NotRequiredCapability does not apply to this userNothing
NotAvailableNot available for the user's country of residenceNothing

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 statusPathCalls
ActivationNotStartedStandard activationPOST /api/v1/bank/accounts
ExternalProviderVerificationRequiredActivation with wallet linkingPOST /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"
}
FieldTypeRequiredDescription
account_typestringYesSepa 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"
}
FieldTypeRequiredDescription
chainstringYesThe chain ID of the wallet to link, as a numeric string

Despite the field name and the "polygon" example in the API reference, chain must be a numeric chain
ID sent as a string — "8453", not "base". A non-numeric value returns 400 ErrorInvalidField with
Chain id should be a number. Note also that the error names the field chain_id while the request
field is chain.

Response:

{
  "challenge": "Sign this message to verify ownership of 0xAAFF...874",
  "wallet_address": "0xAAFF0821A09A1Aac28B72dD3Ff410A7ea5FEb874",
  "chain": "8453"
}
FieldDescription
challengeThe message the wallet must sign
wallet_addressThe Primary Smart Wallet that must produce the signature
chainEchoes 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..."
}
FieldTypeRequiredDescription
chainstringYesSame numeric chain ID string sent to init
signed_challengestringYesWallet 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

ParameterTypeDescription
page_numberintegerPage number, 0-indexed (default: 0)
page_sizeintegerItems 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

FieldDescription
idComposite 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.
currencyAccount currency (EUR for SEPA)
statusAccount status: Active, Pending, Blocked, Closed
account_holderName on the account
account_typeSepa for SEPA accounts
details.ibanIBAN for receiving transfers
details.bicBIC/SWIFT code

Account Status

StatusDescription
PendingAccount details being provisioned
ActiveReady to receive transfers
BlockedTemporarily suspended
ClosedPermanently closed

Receiving SEPA Transfers

To receive a SEPA transfer:

  1. Retrieve the user's SEPA account details using the endpoint above
  2. Provide the IBAN and BIC to the sender
  3. 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 expected detail reads Ach|Spei and is out of date — Sepa and FasterPayment are 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 DescriptionCause
Failed to get userAccount service unavailable
Failed to get bank accountsBanking service unavailable
Failed to build capability contextCapability evaluation failed
Failed to get wallet challenge from external providerExternal provider unavailable during init
Failed to link wallet with external providerExternal provider unavailable during complete

Did this page help you?