> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endl.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Accounts

> Holding money, or converting it — what an account is, the two categories, and the whole lifecycle

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.**

<Info>
  This is the conceptual guide. For the endpoints themselves — parameters,
  schemas and worked bodies — see the
  [Accounts API reference](/api-reference/accounts/introduction).
</Info>

## 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.

<Columns cols={2}>
  <Card title="FIAT — holds what it receives" icon="vault">
    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
  </Card>

  <Card title="ONRAMP — converts on arrival" icon="arrow-right-arrow-left">
    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
  </Card>
</Columns>

The payer's experience is identical either way. The account type decides what the
deposit *becomes*; it does not change how the payer pays.

<Warning>
  **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.
</Warning>

### Choosing between them

| Pick | When |
| - | - |
| **FIAT** | The customer wants to hold and spend the local currency itself — a dirham treasury, a balance they draw down in AED. |
| **ONRAMP** | The local-currency account is just how people pay them, and the stablecoin is the point. |

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.

| Currency | Typical rails | Coordinates you show the payer | Categories |
| - | - | - | - |
| `USD` | ACH push, wire, SWIFT | Account number + routing number on all three; SWIFT adds a BIC | ONRAMP |
| `EUR` | SEPA | IBAN + BIC | ONRAMP |
| `GBP` | Faster Payments | Sort code + account number | ONRAMP |
| `MXN` | SPEI | CLABE | ONRAMP |
| `BRL` | PIX | BR Code | ONRAMP |
| `AED` | UAEFTS, UAEIPP | IBAN + BIC | FIAT **or** ONRAMP |

<Note>
  **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](/api-reference/accounts/list-accounts-available-for-opening)
  and drive your UI from the answer. It tells you what is openable and, when
  something is not, why.
</Note>

Field-by-field, what each currency returns is on
[Response by currency](/api-reference/accounts/response-by-currency).

## Creating an account

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

<Steps>
  <Step title="Make sure the customer exists">
    Accounts hang off a customer.
    [Create one](/api-reference/onboarding/create-customer) 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`.
  </Step>

  <Step title="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](/api-reference/wallets/create-wallet), because `walletId` is required
    and the settlement asset, chain and address are taken from it. A `FIAT` account
    needs no wallet.
  </Step>

  <Step title="Ask what they can open">
    Rather than guessing, call
    [available-for-opening](/api-reference/accounts/list-accounts-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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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](/api-reference/accounts/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](/api-reference/accounts/get-account) is
    additionally what causes the IBAN to appear, so once the account exists poll
    that rather than List.
  </Step>

  <Step title="Fetch the funding instructions">
    Call [Get account](/api-reference/accounts/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.
  </Step>
</Steps>

<Warning>
  **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.
</Warning>

## 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.

<Note>
  **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.
</Note>

### Deposit fees come in two shapes

They are mutually exclusive, and which one you get follows the account type.

| Account | Field | Shape |
| - | - | - |
| `ONRAMP` | `depositFee`, on each rail | A percentage, a flat amount, or both |
| `AED FIAT` | `depositFeeNote`, once on the instructions | A sentence meant for display |

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.**

<Warning>
  **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.
</Warning>

## 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.

| Status | Meaning | Can you change it? |
| - | - | - |
| `ACTIVE` | Usable. Money can move in and out. | Deactivate it |
| `PENDING` | Provisioning has not finished. Deposit instructions may not exist yet. | Wait |
| `DEACTIVATED` | Switched off by request, and reactivatable. | Activate it |
| `FROZEN` | Held by risk or compliance. | No — contact Endl |
| `UNDER_REVIEW` | Enhanced due diligence in progress. | No — wait |
| `DORMANT` | Inactive through disuse. | No |
| `CLOSED` | Terminal. Not reachable through this API — reportable only. | No |

### 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](/api-reference/accounts/get-account) until `status` reaches
`requested`.

<Warning>
  **Two things that surprise integrators.**

  A deactivated account disappears from Get account — it returns `409 ERRACC_1002`,
  while [List accounts](/api-reference/accounts/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.
</Warning>

## Reading balances and history

[Get account balance](/api-reference/accounts/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](/api-reference/accounts/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`.

<Note>
  Activity `status` uses mixed casing and a hyphen — `Completed`, `Pending`,
  `In-Progress` — and unrecognised values pass through unchanged. Compare
  case-insensitively.
</Note>

## 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.

| You get | It means | Do this |
| - | - | - |
| `400` | Something in your request is wrong | Fix it and resend. **Never retry unchanged.** |
| `401` | Key missing, wrong, or its partner is inactive | Check the `API-KEY` header. Do not retry. |
| `403` | Valid key, but not allowed | Check the key has `accounts`, and the customer is yours. |
| `404` | The customer or account does not exist | Do not retry. Re-check the reference id. |
| `409` | Conflicts with current state | Read the code: already open, deactivated, or an unsupported provider. |
| `429` | Too many requests | Back off. The window is one minute. |
| `502` / `503` | A provider is unavailable | **Safe to retry** with backoff — nothing changed. |
| `500` | Unexpected server error | Report it with the `X-Request-ID` response header. |

Every code, with a worked body for each, is on
[Accounts errors](/api-reference/accounts/errors).

## Webhooks

The accounts surface produces exactly two events. Delivery, signing, replay
protection and the envelope shape are covered in the
[Webhooks overview](/webhooks/overview) — this is only what is specific to
accounts.

| Event | When it fires |
| - | - |
| `account.opened` | An account was created **during** a call to Open account. Fires only when the account actually exists by the time that call returns. |
| `account.updated` | You called Activate or Deactivate and the change was accepted. |

Reading an account — list, get, balance, activity, available-for-opening — emits
nothing. Money arriving is a `deposit.received` event, not an account event.

<Note>
  The platform [event catalogue](/webhooks/reference#event-catalogue) also lists
  `iban.assigned` against an `acct_` id. Handle it alongside these two if you
  subscribe to account activity.
</Note>

<Warning>
  **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.
</Warning>

### 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

<AccordionGroup>
  <Accordion title="I am parsing data from a successful response and getting nothing">
    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.
  </Accordion>

  <Accordion title="I passed a UUID as {userId} and got a 404">
    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`.
  </Accordion>

  <Accordion title="Open account returned 200 but there is no id">
    That is the queued branch — expected for any currency outside USD, EUR, GBP,
    MXN and BRL. Poll List accounts until it appears.
  </Accordion>

  <Accordion title="I am waiting on account.opened and it never arrives">
    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.
  </Accordion>

  <Accordion title="My AED account is stuck on PENDING with no IBAN">
    Poll [Get account](/api-reference/accounts/get-account), not List. For AED,
    that call is what triggers the re-fetch that fills in the deposit instructions.
  </Accordion>

  <Accordion title="I asked for a FIAT GBP account and got ONRAMP back">
    Expected today: only AED honours `accountCategory`. Always read `category` off
    the response.
  </Accordion>

  <Accordion title="An account vanished — Get account returns 409">
    It is deactivated. It still exists and is still listed by List; Get account
    refuses deactivated accounts. Reactivate it, or read it from List.
  </Accordion>

  <Accordion title="My balances do not add up to the accounts I can see">
    Without a `status` filter, `balances` covers only **active** accounts even
    though deactivated ones appear in `data`. Pass `status` explicitly so the two
    agree.
  </Accordion>
</AccordionGroup>

## Glossary

| Term | Meaning |
| - | - |
| Account | Where a customer receives money, in one currency, with real bank coordinates. |
| Category | What the account does with a deposit: hold it (`FIAT`) or convert it (`ONRAMP`). |
| Rail | A payment scheme the account can be funded over — SEPA, Faster Payments, PIX. |
| Funding instructions | The block you show a payer: beneficiary, rails and coordinates. |
| Destination | On an ONRAMP account, the asset and chain a deposit settles into. **Never an address.** |
| Reference id | A prefixed public identifier — `cus_`, `acct_`, `wlt_`, `txn_`. |
| Wallet | Where an ONRAMP account's converted stablecoin lands. Named by `walletId` when the account is opened; it supplies the settlement asset, chain and address. |

<Note>
  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.
</Note>
