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

# Embedded Wallets

> Hold and move your users' on-chain assets in wallets we create and run, approved by a signer key you choose.

An embedded wallet is a wallet we create and run for a signer key you register. You use it like any other payment account: you quote and order with its `id`. You never hold gas for it or set it up on the chain. When an order needs to move the wallet's funds, the holder of the signer key approves it with a signature. We can never move the funds without that approval.

It takes three steps:

1. **Register** a signer key. We create the wallet.
2. **Quote and order** with the wallet's payment account `id`, as with any account.
3. **Approve** when an order needs the key holder's approval: sign the order's `authorization` and send us the signature.

Every endpoint takes your API key in the `Authorization` header with no `Bearer` prefix. See [Authentication](https://docs.etherfuse.com/v2/authentication.md).

> [!NOTE]
>
> **In sandbox, embedded wallets are off by default.** Ask your Etherfuse contact to turn them on for your organization. Until then, registering an `embeddedWallet` in sandbox, or reading its requirements, returns `403` `forbidden`.

## Supported chains

| Chain | `chain` |
| - | - |
| Stellar | `stellar` |

Each chain has a few rules of its own. See [Chain details](#chain-details).

## 1. Register the wallet

The key holder generates a P-256 key pair and keeps the private key. You register the public key with [`POST /organization/{organizationId}/payment_account`](https://docs.etherfuse.com/v2/api-reference/payment-accounts/register-payment-account), under the organization that owns the wallet. That is your own organization (its id comes from `GET /me`) or a customer's. See [Organizations in the route](https://docs.etherfuse.com/v2/overview.md#organizations-in-the-route).

```bash theme={null}
curl -X POST https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/payment_account \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a9e-3b4d-4e5f-8a6b-7c8d9e0f1a2b" \
  -d '{
    "kind": "embeddedWallet",
    "label": "Customer wallet",
    "accountDetails": {
      "kind": "signerPublicKey",
      "chain": "stellar",
      "signerPublicKey": "04a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90b1c2d3e4f5061728394a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f901"
    }
  }'
```

* **`chain`** is one of the [supported chains](#supported-chains).
* **`signerPublicKey`** is the P-256 public key in hex SEC1 form, compressed (33 bytes) or uncompressed (65 bytes).
* **`Idempotency-Key`** makes a retry safe. Each registration creates a new wallet, so generate one key per wallet, such as a UUID, and reuse it when you retry. A retry with the same key and body returns the first wallet with `200`. See [Idempotency](https://docs.etherfuse.com/v2/idempotency.md#payment-accounts).
* **The account holder's name** is stored with the wallet. We fill it from the organization's verified identity or business. If we have no verified name, the request fails with `422` `registration-info-required`. Send `registrationInfo.accountHolderName` and retry.

The response is `201` with the account `pending`. When the wallet exists, the account changes to `ready` and we send [`paymentAccountUpdated`](https://docs.etherfuse.com/v2/api-reference/webhook-events/paymentaccountupdated). Keep the account's `id` to quote with.

**You decide who holds the key.** You can hold it for your program, or your customer can hold it so only they can approve their orders. One key can control many wallets, such as one wallet per customer, because each registration creates a new wallet.

> [!NOTE]
>
> Whether holding the key makes you a custodian or money transmitter depends on your facts, activities and jurisdiction. This is not legal advice. Check your obligations with your legal and compliance advisors.

## 2. Quote and order

Quote and order the wallet like any other payment account. Pass its `id` as `destination` to buy into the wallet (onramp), or as `source` to sell or swap out of it (offramp or swap).

```json theme={null}
{
  "source": { "type": "stored", "id": "3f9a2c1e-5b7d-4e8f-9a01-2b3c4d5e6f70" },
  "destination": { "type": "stored", "id": "8c1d2e3f-4a5b-4c6d-8e7f-901a2b3c4d5e" },
  "sourceAmount": "100",
  "sourceAsset": "MXN",
  "destinationAsset": "CETES:GC3CW7EDYRTWQ635VDIGY6S4ZUF5L6TQ7AA4MWS7LEQDBLUSZXV7UPS4"
}
```

Then create the order with [`POST /organization/{organizationId}/order`](https://docs.etherfuse.com/v2/api-reference/orders/create-order) and the `quoteId`. [Onramps](https://docs.etherfuse.com/v2/guides/testing-onramps.md), [Offramps](https://docs.etherfuse.com/v2/guides/testing-offramps.md) and [Swaps](https://docs.etherfuse.com/v2/guides/testing-swaps.md) cover the rest of each direction.

## 3. Approve the authorization

When an order needs to move the wallet's funds, the order's `authorization` field holds the request for the key holder. We send `orderUpdated` when it appears.

```json theme={null}
{
  "status": "created",
  "authorization": {
    "id": "5fac3ce6-b071-4265-8e93-6fcf81a24d55",
    "reason": "funding",
    "method": { "kind": "signature", "algorithm": "es256", "message": "<string to sign>" },
    "expiresAt": "2026-09-28T17:09:11.482Z"
  }
}
```

| `reason` | When |
| - | - |
| `funding` | An offramp or swap from the wallet. We move the funds out after the key holder approves. |
| `claim` | An order into the wallet, such as an onramp or a swap, when the order is `claimable`. See [Claims](#claims). |

**Sign** the UTF-8 bytes of `method.message` exactly as we return them, with ECDSA over P-256 and SHA-256. Hex-encode the signature, either as DER or as the 64-byte `r||s` that WebCrypto produces:

```javascript theme={null}
const signature = await crypto.subtle.sign(
  { name: "ECDSA", hash: "SHA-256" },
  privateKey,
  new TextEncoder().encode(authorization.method.message),
);
const signatureHex = Buffer.from(signature).toString("hex");
```

**Submit** the authorization's `id` and the signature with [`POST /organization/{organizationId}/order/{id}/authorization`](https://docs.etherfuse.com/v2/api-reference/orders/submit-authorization). We return the order and move the funds. If you send an `id` we already accepted, we return `200` with the order.

```bash theme={null}
curl -X POST https://api.sand.etherfuse.com/organization/$ORGANIZATION_ID/order/$ORDER_ID/authorization \
  -H "Authorization: <api_key>" \
  -H "Content-Type: application/json" \
  -d '{ "id": "5fac3ce6-b071-4265-8e93-6fcf81a24d55", "signature": "<hex signature>" }'
```

An authorization expires at `expiresAt`. We can also replace it before then, for example when another transaction from the wallet lands first. A replacement has a new `id` and `message`, and we send `orderUpdated`. Sign the authorization you read most recently.

| Status | `type` | What to do |
| - | - | - |
| `409` | `authorization-expired` | The `id` is not the order's current authorization, or the authorization is past `expiresAt`. Read the order and sign the new one. For a claim, call `/claim` again first, because we do not replace an expired claim authorization. |
| `400` | `invalid-signature` | The signature does not match the wallet's `signerPublicKey` and the message. Check the key, the message bytes and the encoding. |
| `409` | `authorization-not-required` | The order has nothing to approve. Read the order. |

### Claims

On some chains, a wallet can't receive an asset until the wallet is set up for that asset. When an onramp pays a wallet that isn't set up yet, we hold the payout for the wallet to claim, and the order changes to `claimable`. [Chain details](#chain-details) lists when this happens on each chain.

To claim, call [`POST /organization/{organizationId}/order/{id}/claim`](https://docs.etherfuse.com/v2/api-reference/orders/prepare-claim) with no body. It returns `202` with no body. A few seconds later the order's `authorization` has `reason` `claim`, and we send `orderUpdated`. Sign and submit it as above. We set up the wallet and claim the payout in one transaction, and the order changes to `completed`.

> [!WARNING]
>
> A claim authorization lasts up to an hour and never past the claim deadline. We do not replace an expired claim authorization. Call `/claim` again to get a new one. Each call replaces a claim authorization that is still open, so sign the one you read most recently.

## Fees and billing

An embedded wallet order has the same quote fees as any other order. When a new wallet needs setup on the chain to receive an asset, the onramp's quote adds a `walletSponsor` line for that setup.

We also charge two fees for each transaction we send for the wallet:

| Fee line `type` | What it is | Amount |
| - | - | - |
| `networkFee` | The chain's network fee for the transaction. We pay it, so the wallet never needs funds for fees. | The fee we paid, in the chain's native asset (`native`). |
| `walletSigning` | The key holder's approval. | \$0.10 for each signature the wallet makes for the approval, in USD. |

We add these lines to the order's `fees` when the transaction is final, and we send `orderUpdated`. We invoice them separately. We don't take them from the order's funds, and they are not part of `destinationAmount` or `feeAmount`. If a transaction never reaches the chain, the order gets a `walletSigning` line and no `networkFee` line.

## Track the order

Subscribe to [`orderUpdated`](https://docs.etherfuse.com/v2/api-reference/webhook-events/orderupdated) with [`POST /webhook_subscription`](https://docs.etherfuse.com/v2/api-reference/webhook-subscriptions/create-webhook-subscription). Each delivery has the full order, so you see each `authorization`, the change to `claimable`, the fee lines, and the change to `completed`. If you miss a delivery, read the order with [`GET /organization/{organizationId}/order/{id}`](https://docs.etherfuse.com/v2/api-reference/orders/get-order).

## Chain details

### Stellar

* **Setup.** A Stellar wallet needs an account and a trustline for each asset it holds. Both lock up some XLM as a reserve. An onramp to a wallet that doesn't have them yet is quoted with a `walletSponsor` line, and its payout arrives as a claim.
* **Claims.** The order's `claimInstructions` stays null, because the claim is the order's `authorization` with `reason` `claim`. The claim transaction adds the trustline, if the wallet doesn't have it, and claims the balance.
* **Network fee.** `networkFee` lines are in XLM (`native`). The wallet's XLM only covers its reserves.

See the [Stellar guide](https://docs.etherfuse.com/v2/guides/chains/stellar.md) for assets and trustlines.


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