A Virtual IBAN is a dedicated EUR IBAN opened in the name of one of your companies. SEPA payments sent to it land on a separate EUR bank balance. From there you pay out by SEPA to any IBAN or move the money to your own EUR account.

Before You Start #

Virtual IBAN is set up individually for each merchant. Your account manager agrees the setup with you and then sends you:

  • companyId — the company the IBAN is enabled for;
  • accountId — your account used for Virtual IBAN: it pays the issue fee, if your tariff has one, and receives transfers from the bank balance.

Your tariff (issue, incoming and payout fees) and the operations available to you are also agreed individually; you can always read them with GET …/account. Until the setup is done, every call returns 403 with capability_disabled.

All endpoints use the usual x-client-id / x-client-secret headers. Amounts are EUR with at most two decimals and are sent and returned as strings.

Integration Flow #

  1. Read the state: what is enabled, your tariff and, once issued, the IBAN and its balance.
  2. Issue the IBAN and poll until it is active.
  3. Give the IBAN to your payers. Incoming payments are credited automatically.
  4. Pay out by SEPA: quote the fee, then create the payout with that fee.
  5. Or move money to your own EUR account.
Step Endpoint
Read state GET /v1/corporate-banking/{companyId}/account
Issue POST /v1/corporate-banking/{companyId}/account
Quote a payout POST /v1/corporate-banking/{companyId}/quote
Pay out POST /v1/corporate-banking/{companyId}/payouts
Follow a payout GET /v1/corporate-banking/{companyId}/payouts/{payoutId}
Move to your EUR account POST /v1/corporate-banking/{companyId}/transfers

1. Read the State #

GET /v1/corporate-banking/{companyId}/account?fundingAccountId={yourEurAccountId}

The response shows:

  • capabilities — which of issue, deposit and payout are enabled for the company;
  • tariff — the issue, deposit and payout fees, each a percentage plus a fixed amount;
  • minimumDeposit — the smallest incoming payment that still credits something after the deposit fee;
  • purposeCodes — the values accepted as reason in payouts;
  • account — the IBAN with its bank balance, or null until one has been issued.

fundingAccountId is optional: pass the accountId from your account manager to see its balance before the IBAN is issued.

2. Issue the IBAN #

POST /v1/corporate-banking/{companyId}/account
Content-Type: application/json

{
  "accountId": "d290f1ee-6c54-4b01-90e6-d701748f0851",
  "owner": {
    "name": "Example Trading Ltd",
    "addressLine1": "12 King Street",
    "city": "Toronto",
    "region": "Ontario",
    "postcode": "M5H1A1",
    "country": "CA",
    "nationality": "CA",
    "registrationNumber": "1234567890"
  }
}
  • accountId is the account from your account manager. If your tariff has an issue fee and the account cannot cover it, nothing is issued and the call returns insufficient_issuance_funds.
  • owner is the legal company the IBAN is opened for. Text fields accept Latin letters, digits, spaces and / - ? : ( ) . ' + (addresses also ,), without leading or trailing spaces. country and nationality are ISO 3166-1 alpha-2 codes.
  • One IBAN per company: repeating the call returns the same IBAN.

Issuance is asynchronous. The response is 202 with status:

status Meaning
pending Being issued. Poll GET …/account again.
active Ready to receive payments. iban, bic and ownerName are filled in.
failed Not issued, and the issue fee was returned. You can issue again.

3. Receive Payments #

Give your payers the iban, bic and ownerName from the account object. No payment reference is required.

Each incoming SEPA payment is credited to the bank balance automatically, net of the deposit fee, and announced with the usual account webhook (see Webhook Balance Effects). The bank balance is account.balance in the state response.

4. Pay Out by SEPA #

Quote the fee first:

POST /v1/corporate-banking/{companyId}/quote
Content-Type: application/json

{ "amount": "100.00" }
{ "amount": "100.00", "fee": "2.50", "totalDebit": "102.50", "currency": "EUR" }

Then create the payout with the quoted fee as expectedFee:

POST /v1/corporate-banking/{companyId}/payouts
Idempotency-Key: payout-2026-104
Content-Type: application/json

{
  "virtualAccountId": "6b0f2b1e-6f1c-4c5e-9a55-0f2a5b7c1d10",
  "amount": "100.00",
  "expectedFee": "2.50",
  "destination": {
    "name": "Supplier GmbH",
    "iban": "DE89370400440532013000",
    "bic": "COBADEFFXXX",
    "country": "DE",
    "addressLine1": "Hauptstrasse 1",
    "city": "Berlin",
    "postcode": "10115",
    "reason": "SUPP",
    "reference": "Invoice 2026-104",
    "scheme": "AUTO"
  }
}
  • virtualAccountId is account.id from the state response.
  • If the tariff changed after the quote, the payout is refused with fee_changed instead of charging a different fee. Quote again.
  • amount plus the fee is reserved on the bank balance at once. The reserve is spent when the payout settles and released if it fails.

The response is 202; its correlationId is the payout ID. Follow the payout with GET …/payouts/{payoutId}:

status Meaning
settled Sent. Final.
failed Rejected; amount and fee returned to the bank balance. Final.
unknown The outcome is being confirmed with the bank. Do not resend — keep polling.
any other In progress, money held.

Retries #

Retries are safe with the same Idempotency-Key: the same key returns the same payout, while a different body under that key is idempotency_conflict. Never change the key to retry a payout whose outcome you do not know — look it up with GET …/payouts/{payoutId} instead.

5. Move Money to Your EUR Account #

POST /v1/corporate-banking/{companyId}/transfers
Idempotency-Key: sweep-2026-10-07
Content-Type: application/json

{ "toAccountId": "d290f1ee-6c54-4b01-90e6-d701748f0851", "amount": "500.00" }

The transfer is immediate and free. Only this direction is possible: the bank balance is funded by incoming SEPA payments, not by transfers. Not enough money on the bank balance is banking_rejected.

Errors #

Errors are returned as {"code": "..."}. A wrong or missing x-client-id / x-client-secret is a 401 without a body, as on every other endpoint.

HTTP code What to do
400 invalid_request Fix the request.
400 banking_rejected The bank refused the operation, or the bank balance is too low.
400 insufficient_issuance_funds The account cannot cover the issue fee. Contact your account manager.
403 capability_disabled Virtual IBAN, or this operation, is not enabled for the company. Contact your account manager.
404 not_found Unknown company, IBAN or payout.
409 fee_changed The tariff changed. Quote again.
409 idempotency_conflict The Idempotency-Key was already used with a different body.
409 previous_issue_recovering A previous issuance is still being resolved. Retry later.
409 bank_account_migration_required Contact your account manager.
503 banking_unavailable The bank is temporarily unavailable. Retry later.