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

# Wallets

> Custodial stablecoin wallets — one address per network, who may create them, and what the balance call really returns

A wallet is where your customer's stablecoin actually sits. Endl custodies the keys;
you never handle them, and there is no seed phrase to show anyone. What you get back
is an address on one network, and that address is the thing you give to whoever is
sending crypto.

<Warning>
  **A wallet is bound to exactly one network.** There is no multi-chain wallet and no
  single address that works everywhere. A customer who needs to receive USDC on Base
  *and* on Ethereum needs **two wallets**.
</Warning>

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

## What a wallet is for

Two things, and they pull in different directions.

<Columns cols={2}>
  <Card title="Receiving — the address" icon="arrow-down-to-line">
    Somebody sends stablecoin to the wallet's `address` on its `network`. That is an
    ordinary on-chain transfer; nothing about it is Endl-specific.
  </Card>

  <Card title="Settling — the destination" icon="arrow-right-arrow-left">
    An [`ONRAMP` account](/guides/accounts) converts a fiat deposit and settles the
    stablecoin into a wallet. That is why `walletId` is required when you open one —
    the wallet supplies the settlement asset, chain and address.
  </Card>
</Columns>

The second is the common case. Most partners create one wallet per customer per
network and then point every ONRAMP account at it.

## Who may create one

**The customer must have completed KYC.** Create and create-on-network are both gated
on it; everything else — list, get, balance, update, delete — is not. An incomplete
customer gets `403 ERRWLT_1005`, and the check is the same completion rule the rest of
the platform uses, so a customer who can transact can create a wallet.

<Warning>
  The gate is on **completion**, not on having started. A customer sitting in
  `PENDING` or `UNDER_REVIEW` fails it exactly like one who never began. Create the
  customer and get them through
  [verification](/api-reference/onboarding/individual-kyc) before you try to open a
  wallet for them.
</Warning>

## Networks

Fifteen networks are supported platform-wide: EVM mainnets, their testnets, TRON and
XRP.

| Family | Mainnets | Testnets |
| - | - | - |
| EVM | `Ethereum`, `Base`, `ArbitrumOne`, `Optimism`, `Polygon`, `AvalancheC` | `EthereumSepolia`, `BaseSepolia`, `ArbitrumSepolia`, `OptimismSepolia`, `PolygonAmoy` |
| TRON | `Tron` | `TronNile` |
| XRP | `XrpLedger` | `XrpLedgerTestnet` |

<Warning>
  **`network` is matched case-sensitively when you create a wallet.** `Base` is
  accepted; `base` and `BASE` are rejected with `network must be one of: …`. Copy the
  spelling out of the table above exactly.

  Confusingly, the *same* names are matched case-**in**sensitively everywhere else —
  reading a balance for `:chain`, naming a network on a transfer. Only creation is
  strict. Treat the canonical casing as the only correct form and the inconsistency
  never bites you.
</Warning>

<Note>
  **An environment can expose fewer networks than this table.** Sandbox and QA are
  pointed at test networks and restrict creation to that subset, so a `Base` wallet
  that works in production is refused there. The error message lists the networks the
  environment you are calling actually allows — read it rather than assuming the full
  set.
</Note>

Quotes spell these networks differently — see
[the mapping table](/guides/quotes#chains-and-why-they-are-spelled-differently-here).

## Creating a wallet

<Steps>
  <Step title="Create the customer and get them verified">
    Wallets hang off a customer.
    [Create one](/api-reference/onboarding/create-customer), keep its `cus_…`
    reference id — that is the `{userId}` in every wallets path — and see them
    through KYC.
  </Step>

  <Step title="POST the network">
    `network` is the only required field. `name` is optional but must be a non-empty
    string if you send it at all, and `tags` must be an array of strings if present.

    ```json Request theme={null}
    {
      "network": "Base",
      "name": "Acme Corp settlement",
      "tags": ["production", "settlement"]
    }
    ```

    A `userId` in the body is **stripped and ignored** — the customer is the one in
    the path, and only the one in the path.
  </Step>

  <Step title="Read the id and address off the response">
    The response carries the wallet's `walletId` (a `wlt_…` reference), its
    `address`, its `network` and a `status`. **The address is live from that moment**;
    there is no activation step.
  </Step>

  <Step title="Add further networks, if you need them">
    [Create wallet on network](/api-reference/wallets/create-wallet-on-network) takes
    an existing wallet and derives **another wallet on a different network** from the
    same underlying key material. It is **not** a mutation of the wallet you name.
    The response is a *new* wallet with its own `wlt_` id and its own address — the
    networks are related cryptographically, not merged into one object.
  </Step>
</Steps>

### What comes back

| Field | Where it comes from | Watch out |
| - | - | - |
| `walletId` | Endl's own reference (`wlt_…`) | **Never** the custody provider's id. The provider id is not exposed anywhere. |
| `network`, `address`, `status`, `custodial`, `dateCreated`, `tags` | The custody layer, passed through | `status` is the provider's vocabulary, not Endl's. Compare case-insensitively. |
| `currency` | Endl's own record | Always `USDC` on a newly created wallet — see below. |
| `isPrimary` | Endl's own record | See [The primary wallet](#the-primary-wallet). |

<Warning>
  **`currency` is not a restriction.** Every wallet is recorded locally with `USDC`,
  because that is the default the record is stamped with at creation. It does not
  mean the wallet can only hold USDC, and it is not derived from what is actually
  on-chain. For what the wallet really holds, call
  [Get wallet balance](/api-reference/wallets/get-wallet-balance).
</Warning>

<Note>
  The cryptographic material behind the wallet — public key, scheme, curve — is
  deliberately **not** in the response. If you are looking for a `signingKey` block
  because you saw one in a custody provider's own docs, it is removed on purpose and
  will not be added.
</Note>

### The primary wallet

One wallet per customer can be flagged `isPrimary`. The rule is narrow and worth
knowing exactly:

* It is set **only** on customers created through the partner API.
* It is set **only** on the first wallet, when the customer has no active primary
  wallet yet.
* It is only ever **granted, never revoked**. Creating a second wallet does not move
  the flag, and re-syncing an existing wallet does not clear it.

Treat it as "the first wallet we made for this customer", not as a setting you
control — **there is no endpoint to move it**.

## Reading balances

[Get wallet balance](/api-reference/wallets/get-wallet-balance) returns the wallet's
native asset, its tokens and a USD total.

For most networks the call aggregates across chains in one request. **TRON is the
exception**: TRON balances are queried on their own chain because they are not
covered by the aggregate. You do not have to do anything about this — Endl picks the
right query from the wallet's network — but it does mean a TRON wallet's balance
reflects TRON only, while an EVM wallet's can span the networks its key covers.

<Note>
  The balance body names the wallet under `walletId`, and that value is rewritten to
  your `wlt_…` reference before it reaches you. Everything else in the body —
  address, chain, native, tokens, `totalUsd` — is on-chain data and passes through
  untouched.
</Note>

## Updating and deleting

[Update](/api-reference/wallets/update-wallet) takes `name`, `externalId`, or both.
**Sending neither is a `400`** — a no-op update is treated as a mistake rather than
quietly accepted. Only `name` is mirrored into Endl's own record; `externalId` lives
at the custody layer.

Delete is **soft**, and it is one-way through this API.

<Warning>
  **A deleted wallet does not vanish, and deleting it does not move any funds.**

  * It stops appearing in [List wallets](/api-reference/wallets/list-wallets).
  * [Get wallet](/api-reference/wallets/get-wallet) returns `404 ERRWLT_1000`, even
    though the record still exists.
  * The response is `200` with `status: "deleted"` — **not** an empty `204`.

  **Sweep any balance out before you delete.** There is no undelete endpoint.
</Warning>

## Scoping and reference ids

Every wallets path is `/{userId}/…`, and the customer is resolved from that path
segment alone. A wallet that belongs to a different customer is reported as
`404 ERRWLT_1000` — **never `403`** — so a partner can never probe which wallet ids
exist under another customer.

| You send | Shape | Wrong shape | Well-formed but unknown |
| - | - | - | - |
| `{userId}` | `cus_…` | `400` | `404` |
| `{walletId}` | `wlt_…` | `400` | `404` |

**Raw UUIDs are not accepted in either position.**

## Error handling

Wallet-specific failures carry an `ERRWLT_` code. Everything cross-cutting —
validation, auth, rate limiting — uses the shared `ERRCORE_` codes described in
[Errors and the response envelope](/api-reference/errors).

| Code | Status | Means |
| - | - | - |
| `ERRWLT_1000` | `404` | The wallet does not exist, belongs to another customer, or has been deleted. |
| `ERRWLT_1001` | `400` | A wallet id was required and not supplied. |
| `ERRWLT_1002` | `400` | A customer id was required and not supplied. |
| `ERRWLT_1003` | `400` | The network is not one Endl runs on, or not one this environment allows. |
| `ERRWLT_1004` | `400` | A transfer body was required and not supplied. |
| `ERRWLT_1005` | `403` | The customer has not completed KYC, so no wallet may be created. |

**None of these are retryable.** Branch on the code, never on the message text — the
codes are frozen and will never be renumbered or reused; the wording may be refined
at any time.

## Webhooks

Wallets produce two events. Delivery, signing, replay protection and the envelope
shape are covered in the [Webhooks guide](/guides/webhooks) — this is only what is
specific to wallets.

| Event | When it fires |
| - | - |
| `wallet.created` | A wallet was created, by either Create wallet or Create wallet on network. |
| `wallet.deleted` | A wallet was deleted. |

`data.id` is the wallet's `wlt_…` reference. The payload body is deliberately thin —
`network` and `currency`, nothing else.

<Warning>
  **Both events are best-effort, and both are emitted after the work has already
  succeeded.** If the emit fails the wallet still exists (or is still deleted) — **the
  API response is the authority, not the event.**
</Warning>

There is also **no `wallet.updated`**. Renaming a wallet emits nothing, and so does
money arriving: an incoming on-chain transfer is a deposit event, not a wallet event.

## Common pitfalls

<AccordionGroup>
  <Accordion title="Create wallet returns 400 'network must be one of…' but my network is in the list">
    Check the casing. Creation matches `network` exactly: `Base`, not `base`.
    Everything else in the API is case-insensitive about network names, which is what
    makes this one easy to miss.
  </Accordion>

  <Accordion title="The same create call works in production and fails in sandbox">
    Sandbox and QA restrict creation to test networks. The error lists the networks
    that environment allows — `BaseSepolia` rather than `Base`.
  </Accordion>

  <Accordion title="Create wallet returns 403 and the key definitely has the wallets permission">
    `ERRWLT_1005` is the **KYC gate**, not a permission failure. The customer has not
    completed verification. A permission failure is `ERRCORE_1005`.
  </Accordion>

  <Accordion title="Every wallet says currency USDC, including ones holding USDT">
    `currency` is a local default stamped at creation, not a reading of the chain and
    not a restriction on the wallet. Use Get wallet balance for holdings.
  </Accordion>

  <Accordion title="I called create-on-network and my original wallet is unchanged">
    That is correct. It creates an **additional** wallet on another network from the
    same key material and returns that new wallet, with its own `wlt_` id and
    address. The wallet you named in the path is untouched.
  </Accordion>

  <Accordion title="Get wallet returns 404 for a wallet that is definitely there">
    Three possibilities, all reported identically on purpose: it was deleted, it
    belongs to a different customer, or the `wlt_` reference is well-formed but
    unknown. Check List wallets for that customer.
  </Accordion>

  <Accordion title="Delete returned 200 and I was expecting 204">
    Delete returns a body — `200` with `status: "deleted"`. Nothing in the v0 surface
    returns an empty `204`.
  </Accordion>

  <Accordion title="I deleted a wallet that still had a balance">
    The delete succeeded and the funds did not move. Deletion is a bookkeeping
    operation, not a sweep. Contact Endl — there is no undelete endpoint.
  </Accordion>

  <Accordion title="I want one wallet that receives on every chain">
    There isn't one. A wallet is one network, one address. Create one per network you
    need to receive on, and show the payer the address matching the network they are
    sending from.
  </Accordion>
</AccordionGroup>

## Glossary

| Term | Meaning |
| - | - |
| Wallet | A custodial address on exactly one network, owned by one customer. |
| Network | The chain the wallet lives on — `Base`, `Tron`, `XrpLedger`. Canonically cased. |
| Address | The on-chain address a payer sends to. Live from creation. |
| Custodial | Endl holds the keys. There is no key material in any response. |
| Primary wallet | The first wallet created for a partner-API customer. Granted once, never moved. |
| Create on network | Deriving an additional wallet, on another network, from an existing wallet's key. |
| Reference id | A prefixed public identifier — `cus_`, `wlt_`, `acct_`, `txn_`, `qut_`. |

<Note>
  Written against `Api-Version: 2026-09.1`. The network list is platform-wide; what a
  given environment and a given customer can actually use is narrower, so read errors
  rather than hardcoding this page.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.