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

> Move on-chain assets for your users through non-custodial embedded wallets, without anyone touching gas, trustlines, or transaction construction.

An embedded wallet lets you and your users hold and move on-chain assets without learning the chain underneath. Nobody constructs a transaction, manages a sequence number, holds gas, or opens a trustline. We build every transaction. The network gas and account reserves are part of the quote fee: they come out of the assets you ramp and show up as fee lines in the quote, so the wallet never holds a balance for fees.

<Info>
  **Integrate server-side.** Run registration, quoting and ordering from your backend with your API key. The signature that approves an order comes from whoever holds the signer key. For a copy-paste sandbox script, see the [sandbox onramp](/v2/guides/embedded-wallets-walkthrough).
</Info>

An embedded wallet is non-custodial. Each wallet has a single signer key, a P-256 key. When an order needs to move the wallet's funds, we put an **authorization** on the order. The key holder signs it, and only then do we move the funds. We can never sign or move funds on our own, and we can refuse to relay a transaction.

<Info>
  Embedded wallets run on **Stellar**, so the wallet's chain is `stellar`. All endpoints use the raw API key in the `Authorization` header (no `Bearer` prefix); see [Authentication](/v2/authentication). You do not need Stellar's rules (trustlines, reserves, transaction expiry) to use an embedded wallet; we handle them for you.
</Info>

## Who holds the key

You, the integrating partner, choose who holds the signer key:

* You hold the signer key yourself, on behalf of your program; or
* Your end customer holds and controls the signer key, so only the customer can approve an order and you never hold or see the customer's private key.

Control of the funds follows control of the key. A party that cannot move funds without the keyholder does not control them, so only the holder of the signer key can approve moving the wallet's assets.

<Note>
  Whether holding the key makes you a custodian or money transmitter depends on your own facts, activities, and jurisdiction. This is not legal advice; determine your own obligations with your legal and compliance advisors.
</Note>

## Register the wallet

We create the embedded wallet when you register it. First, the key holder generates a P-256 key pair and keeps the private key. Then you register the public key with [`POST /organization/{organizationId}/payment_account`](/v2/api-reference/payment-accounts/register-payment-account). Use the id of the organization that owns the wallet: your own organization's id from `GET /me`, or the customer organization's id. The examples below set it as `$ORGANIZATION_ID`, and the quote and order use the same id. See [Organizations in the route](/v2/overview#organizations-in-the-route).

Send `kind` `embeddedWallet`, and `accountDetails` of kind `signerPublicKey` with the `chain` and the `signerPublicKey`. `signerPublicKey` is the hex-encoded P-256 public key in SEC1 form, compressed (33 bytes) or uncompressed (65 bytes). The chain is `stellar` today.

```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" \
  -d '{
    "kind": "embeddedWallet",
    "label": "Customer wallet",
    "accountDetails": {
      "kind": "signerPublicKey",
      "chain": "stellar",
      "signerPublicKey": "04a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90b1c2d3e4f5061728394a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e8f901"
    }
  }'
```

The response is the payment account. Keep its `id`: you pass it to every quote that moves money into or out of the wallet.

```json theme={null}
{
  "id": "8c1d2e3f-4a5b-4c6d-8e7f-901a2b3c4d5e",
  "status": "ready",
  "label": "Customer wallet",
  "rails": {
    "stellar": {
      "source": { "isDefault": true, "status": "ready" },
      "destination": { "isDefault": true, "status": "ready" }
    }
  },
  "createdAt": "2026-09-28T17:04:11.482Z"
}
```

To find a wallet you registered earlier, list the organization's accounts with [`GET /organization/{organizationId}/payment_accounts`](/v2/api-reference/payment-accounts/list-payment-accounts).

## Quoting and ordering

You quote and order an embedded wallet like any other wallet. Pass the wallet's payment account `id` as `destination` on an onramp [quote](/v2/api-reference/quotes/create-quote), or as `source` on an offramp quote. The other side is the bank account's payment account. Both accounts must belong to the organization in the route.

```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"
}
```

Take `destinationAsset` from a token's `asset` in [`GET /supported_assets?chain=stellar`](/v2/api-reference/assets/list-supported-assets). The quote prices the account reserve and trustline a new wallet needs into its fee lines, so a new wallet gets its first funds from its first onramp and you never send XLM.

Create the order with [`POST /organization/{organizationId}/order`](/v2/api-reference/orders/create-order) and the `quoteId`. The order's `id` is the `quoteId`. For the rest of each direction, see [Onramps](/v2/guides/testing-onramps) and [Offramps](/v2/guides/testing-offramps).

## Order authorization

When an order needs the key holder to approve moving the wallet's funds, the order's `authorization` field holds the request. Otherwise `authorization` is `null`. Read it from [`GET /organization/{organizationId}/order/{id}`](/v2/api-reference/orders/get-order) or from the `orderUpdated` webhook.

```json theme={null}
{
  "id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
  "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` tells you why we need the approval:

| `reason` | When |
| - | - |
| `funding` | An offramp or swap whose source is an embedded wallet. We move the funds out of the wallet once the key holder approves. |
| `claim` | An onramp into an embedded wallet that has become `claimable`. See [Claiming funds](#claiming-funds). |

```mermaid theme={null}
sequenceDiagram
    participant App as Your app
    participant Key as Key holder's device
    participant EF as Etherfuse
    EF-->>App: orderUpdated with authorization
    App->>EF: GET /organization/{organizationId}/order/{id}
    EF-->>App: authorization id and method.message
    App->>Key: Sign method.message
    Key-->>App: P-256 signature, hex
    App->>EF: POST .../order/{id}/authorization with id and signature
    EF-->>App: The order
    EF->>EF: Move the funds
    Note over App,EF: If the authorization expires first, we replace it<br/>with a new id and message and send orderUpdated.<br/>Read the order and sign the new one.
```

The order status does not change while the authorization waits. The order stays `created` (for `funding`) or `claimable` (for `claim`). We send `orderUpdated` when `authorization` appears or changes. An order has at most one pending authorization at a time.

An offramp or swap funded from an embedded wallet has no address to send to. Its `fundingInstructions.destination` carries the authorization id:

```json theme={null}
{
  "fundingInstructions": {
    "destination": {
      "expiresAt": null,
      "authorizationId": "5fac3ce6-b071-4265-8e93-6fcf81a24d55"
    }
  }
}
```

Approve that authorization to fund the order.

### Sign the message

The key holder signs `method.message` with the signer key:

* `es256`: ECDSA over P-256 with SHA-256.
* Sign the UTF-8 bytes of `method.message` exactly as we return it.
* Hex-encode the signature. We accept DER, or the 64-byte `r||s` form (128 hex characters) that WebCrypto produces.

In Node.js or a browser, WebCrypto does this directly:

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

### Submit the approval

Send the authorization `id` and the `signature` to [`POST /organization/{organizationId}/order/{id}/authorization`](/v2/api-reference/orders/submit-authorization):

```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>"
  }'
```

The response is the order. We move the funds, and the order continues through its normal statuses.

### Errors and expiry

| Status | `type` slug | What to do |
| - | - | - |
| `409` | `authorization-expired` | The `id` is not the order's current authorization, or it has expired. Read the order and sign the new authorization. |
| `400` | `invalid-signature` | The signature does not verify against the wallet's `signerPublicKey` and `method.message`. Check the key, the message bytes and the encoding, then sign again. |
| `409` | `authorization-not-required` | The order has no pending authorization. Read the order for its current state. |

Each authorization expires at `expiresAt`. When one expires, we replace it with a new authorization that has a new `id` and a new `message`, and we send `orderUpdated`. Always sign the authorization you just read, and never reuse an old signature. See [Errors](/v2/errors) for the error body format.

## Claiming funds

The first time an embedded wallet receives an asset, it has no trustline for it. The asset arrives as a Stellar claimable balance, and the order moves to `claimable`. This comes from how Stellar works, not from Etherfuse. The order's `claimInstructions` describes the balance:

```json theme={null}
{
  "status": "claimable",
  "claimInstructions": {
    "kind": "stellarClaimableBalance",
    "balanceId": "00000000da0d57da7d4850e7fc10d2a9d0ebc731f7afb40574c03395b17d49149b91f5be",
    "asset": "CETES:GC3CW7EDYRTWQ635VDIGY6S4ZUF5L6TQ7AA4MWS7LEQDBLUSZXV7UPS4",
    "amount": "99.61",
    "claimant": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
    "expiresAt": null
  }
}
```

To accept the funds, call [`POST /organization/{organizationId}/order/{id}/claim`](/v2/api-reference/orders/prepare-claim) with no body. For an embedded wallet we return the id of the order's `claim` authorization:

```json theme={null}
{
  "kind": "authorization",
  "authorizationId": "5fac3ce6-b071-4265-8e93-6fcf81a24d55"
}
```

Read the order, then [sign](#sign-the-message) and [submit](#submit-the-approval) the authorization with that id. Once the key holder approves, we open the trustline and claim the balance in one transaction, and the order moves to `completed`. A self-held wallet gets `kind` `stellarTransaction` with an `xdr` to sign and submit instead.

<Warning>
  **Sign the claim authorization soon after you read it.** It expires at `expiresAt`. If it expires, we replace it with a new one with a new `id`, so read the order again and sign the new `message`.
</Warning>

## Tracking the order

Subscribe to [`orderUpdated`](/v2/api-reference/webhook-events/orderupdated) with [`POST /webhook_subscription`](/v2/api-reference/webhook-subscriptions/create-webhook-subscription). Each delivery carries the full order, so you see a new or replaced `authorization`, the move to `claimable`, and then the move to `completed` without polling. [`GET /organization/{organizationId}/order/{id}`](/v2/api-reference/orders/get-order) is the fallback. See [Webhooks](/v2/webhooks).

## Related

<CardGroup cols={2}>
  <Card title="Sandbox onramp" icon="flask" href="/v2/guides/embedded-wallets-walkthrough">
    Minimal server-side onramp script for sandbox
  </Card>

  <Card title="Stellar" icon="star" href="/v2/guides/chains/stellar" />

  <Card title="Onramps" icon="arrow-up-right-dots" href="/v2/guides/testing-onramps" />

  <Card title="Offramps" icon="arrow-down" href="/v2/guides/testing-offramps" />
</CardGroup>


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