Skip to main content
An account is the address you give someone so they can pay your customer. It comes with real bank coordinates — an IBAN, a sort code and account number, a CLABE, a PIX code — which you hand to whoever is paying. The payer sends money over an ordinary local rail; their bank neither knows nor cares that Endl is involved. What happens to that money the moment it lands is the whole of the difference between the two account types.
This is the conceptual guide. For the endpoints themselves — parameters, schemas and worked bodies — see the Accounts API reference.

One customer, many accounts

An account belongs to exactly one customer and holds exactly one currency. There is no multi-currency account. A customer can hold several accounts in the same currency — useful when you want separate coordinates per payer, per site or per business line. Customers can hold up to 10 accounts per currency, counting active and pending accounts combined. Requests beyond this limit are refused with 409 ERRACC_1018.

The two categories

Every account carries a category on the account object.

FIAT — holds what it receives

Money arrives and stays as that currency. A dirham deposit is a dirham balance. Nothing is converted and nothing moves on by itself.
  • Available for AED only at this time
  • No walletId needed — nothing settles onward
  • The response has no destination block
  • Deposit fees arrive as a display string, not a structure

ONRAMP — converts on arrival

Money arrives as fiat and is converted onward into a stablecoin, which settles into your customer’s wallet. The fiat is a doorway, not a destination.
  • Settles into the wallet you name as walletId — required
  • The response carries destination with asset and chain
  • Deposit fees arrive per rail, as structured numbers
  • Required for every currency except AED FIAT
The payer’s experience is identical either way. The account type decides what the deposit becomes; it does not change how the payer pays.
Today the category is only yours to choose on AED. You can send accountCategory on any currency and it will be accepted — but for USD, EUR, GBP, MXN and BRL it is currently ignored, and the account comes back as ONRAMP regardless. Always read category off the response rather than assuming your request was applied.

Choosing between them

Collecting in several countries? Open one ONRAMP account per currency, all settling into the same wallet. You are not managing five fiat balances.

Where accounts are available

Six currencies can be opened today, across two providers. Which rails a given account exposes is decided by the provider when it is created — treat the rail list as data to iterate, not a constant to hardcode.
Do not hardcode this table. Availability is per customer, not global — it depends on their onboarding state as much as on the currency. Call List currencies available for opening and drive your UI from the answer. It tells you what is openable and, when something is not, why.
Field-by-field, what each currency returns is on Response by currency.

Creating an account

Opening an account is one call, but the call is not always the end of the story.
1

Make sure the customer exists

Accounts hang off a customer. Create one first and keep its cus_… reference id — that is the {userId} in every accounts path. A customer belonging to another partner is a 403, not a 404.
2

Make sure they are eligible

Onboarding requirements are set by the provider behind the currency. AED is the strictest: the customer needs a completed KYC check before an account can open, and attempting one early returns a message naming the missing step.For an ONRAMP account the customer also needs a wallet, because walletId is required and the settlement asset, chain and address are taken from it. A FIAT account needs no wallet.
3

Ask what they can open

Rather than guessing, call available-for-opening. Each row says whether that currency is openable right now and, when it is not, the reason — already open, pending review, action required, or an eligibility check that has not been run.
4

Open it

currency, accountName and accountCategory are always required, and walletId as well when the category is ONRAMP.displayName is worth setting, because it is the one label you control — and it cannot be changed afterwards through this API.
5

Handle both possible answers

You always get a 200, but one of two bodies. If the response has an id, the account exists. If it has no id and status: PENDING, it is still being created — do not treat the missing id as an error.
6

Wait for it to be ready

If the response carried an id, the account already exists and an account.opened webhook is emitted for it.If it did not, poll List accounts until the account appears. A queued open emits no webhook at all — the response returns before the account exists, so there is nothing to fire an event about.For AED, polling Get account is additionally what causes the IBAN to appear, so once the account exists poll that rather than List.
7

Fetch the funding instructions

Call Get account. It is the only endpoint that reliably returns fundingInstructions, and the only one that returns deposit fees at all. Open account may include a partial block, but never the fees — do not build your payer-facing screen from it.
Branch on the presence of id, not on the status code. Both branches are HTTP 200. USD, EUR, GBP, MXN and BRL resolve inline and return an acct_… id. Everything else is queued and returns status: PENDING with no id.

Funding an account

Once an account is active, fundingInstructions.source holds everything a payer needs. It contains a list of payoutRails — one entry per way the account can be paid — and each entry carries only the coordinate fields that rail actually uses. Render rails, do not index them. A USD account typically exposes three rails; a GBP account typically exposes one. Which fields are populated differs per currency: a routing number for USD, an IBAN for EUR and AED, a sort code for GBP, a CLABE for MXN, a BR Code for BRL. Fields that do not apply are omitted entirely, not returned as null — so iterate the array and render whichever keys are present.
Two fields you should not depend on. paymentReference and bank.addressDetails are part of the response model but are not currently populated. Do not build deposit attribution on paymentReference, and read bank.address rather than the structured address.

Deposit fees come in two shapes

They are mutually exclusive, and which one you get follows the account type. Note the deliberate asymmetry inside depositFee: the percentage is trimmed ("0.5" means 0.5%) while the flat amount keeps its decimals ("10.00"). depositFeeNote is free text — show it verbatim and do not try to parse a number out of it.
The beneficiary name may not be your customer. For USD, MXN and BRL, beneficiaryName is the provider’s own legal entity — the payer is genuinely paying that company. For EUR and GBP it is your customer’s real name. Always display the value you are given, or the payment will be rejected on a name mismatch.

Lifecycle

An account reports one of seven statuses. They are deliberately not collapsed into fewer, because only one of them is something you can undo yourself.

Turning an account off and on

Deactivating is a soft switch, not a deletion — there is no delete on this API at all. The change is queued rather than applied inline, so the response tells you what you asked for (requested) alongside what the account currently says (status). They differ until the change lands. Poll Get account until status reaches requested.
Two things that surprise integrators.A deactivated account disappears from Get account — it returns 409 ERRACC_1002, while List accounts still shows it with status: DEACTIVATED. List is your only way to find one and watch it come back.AED accounts cannot be activated or deactivated at all. Both calls return 409 ERRACC_1003 rather than silently doing nothing.

Reading balances and history

Get account balance gives you one number plus an asOf timestamp saying when it was accurate. For AED that value is served from storage and refreshed behind the scenes, so it can lag; every other currency is fetched live on each call. Respect asOf rather than assuming “now”. Get account activity lists the account’s movements, newest first. Two things to plan for:
  • No pagination and no date filter, so a busy account returns its whole history in one response.
  • Amounts have no sign — a credit and a debit of the same size look identical except for type.
Activity status uses mixed casing and a hyphen — Completed, Pending, In-Progress — and unrecognised values pass through unchanged. Compare case-insensitively.

Error handling

Every error returns the same envelope, so you only need one parser. Branch on errors[0].code, not on the message text — the codes are frozen and will never be renumbered or reused, while messages may be reworded. When the failure is about one specific input, errors[0].field names it, so you can bind the message straight to a form field. When it is not about an input — authentication, rate limits, a provider outage — it is null rather than filled with prose. Every code, with a worked body for each, is on Accounts errors.

Webhooks

The accounts surface produces exactly two events. Delivery, signing, replay protection and the envelope shape are covered in the Webhooks overview — this is only what is specific to accounts. Reading an account — list, get, balance, activity, available-for-opening — emits nothing. Money arriving is a deposit.received event, not an account event.
The platform event catalogue also lists iban.assigned against an acct_ id. Handle it alongside these two if you subscribe to account activity.
Do not act on data.status. The payload carries Endl’s internal state name, not the value Get account returns — activated where the API says ACTIVE. It can also be the pre-change value, because account.updated fires when your request is accepted rather than when it lands. Treat the event as “something changed, go look”, and call Get account for the real state.

What never produces an event

  • A queued open never fires account.opened. If Open account returned no id, nothing emits an event when the account later appears — poll List accounts.
  • Status changes Endl makes on its own emit nothing. An account closed or frozen by the banking partner or by compliance sends no account.updated. Re-read accounts you care about on a schedule.
  • Funding instructions becoming available emits nothing. An AED account whose IBAN is issued after opening produces no event. Poll Get account.
Absence of an event is not proof that nothing happened. Anything you must be certain of should be confirmed by reading the account.

Common pitfalls

Success responses are not wrapped. A 200 body is the object itself — there is no data key to unwrap. Only errors use the five-key envelope.
Path ids are reference ids, not UUIDs. Customers are cus_…, accounts are acct_…. A wrongly-shaped id is a 400; a well-formed but unknown one is a 404.
That is the queued branch — expected for any currency outside USD, EUR, GBP, MXN and BRL. Poll List accounts until it appears.
If the Open account response had no id, the account was queued and no webhook is emitted for it — the response returned before the account existed. Poll GET /api/v0/accounts/{userId} until it appears. account.opened fires only for accounts created during the call itself.
Poll Get account, not List. For AED, that call is what triggers the re-fetch that fills in the deposit instructions.
Expected today: only AED honours accountCategory. Always read category off the response.
It is deactivated. It still exists and is still listed by List; Get account refuses deactivated accounts. Reactivate it, or read it from List.
Without a status filter, balances covers only active accounts even though deactivated ones appear in data. Pass status explicitly so the two agree.

Glossary

Written against Api-Version: 2026-09.1. Rail sets and bank details are supplied by the banking partner behind each currency and can change — read them from the API rather than hardcoding them.