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

> A minimal server-side script that runs an embedded-wallet onramp in sandbox, from wallet registration to the signed claim authorization.

This page is optional developer tooling. Read [Embedded Wallets](/v2/guides/embedded-wallets) first for the concepts and the detail of each step.

<Info>
  Embedded wallets run on **Stellar**. Quoting and ordering follow the standard procedure, so this page links to [Onramps](/v2/guides/testing-onramps) and [Offramps](/v2/guides/testing-offramps) for the details.
</Info>

<Warning>
  **Sandbox API keys only.** Run this script server-side. Never put a production API key in client-side code.
</Warning>

## Minimal sandbox onramp

The script generates a P-256 signer key, registers the embedded wallet, quotes MXN into a Stellar asset, creates the order, simulates the fiat deposit, and waits for the order. When the order becomes `claimable`, it builds the claim transaction, signs the order's `authorization`, submits the approval, and waits for `completed`.

<Note>
  The script generates the signer key in memory so it runs on its own. In a real integration, the key holder generates the key once and keeps the private key. Only the public key reaches us.
</Note>

The script needs Node.js 20 or later for the global `fetch` and WebCrypto. Set these environment variables:

| Variable | Value |
| - | - |
| `ETHERFUSE_API_KEY` | Your sandbox API key. |
| `ORGANIZATION_ID` | The organization the script acts for: your own organization's id from `GET /me`, or a customer organization's id. See [Organizations in the route](/v2/overview#organizations-in-the-route). |
| `BANK_ACCOUNT_ID` | The `id` of a bank payment account on `spei` whose `source` direction is `ready`, registered under `ORGANIZATION_ID`. See [Register payment accounts](/v2/overview#the-flow). |
| `TARGET_ASSET` | A Stellar token's `asset` from [`GET /supported_assets?chain=stellar`](/v2/api-reference/assets/list-supported-assets). |

```javascript theme={null}
const sleep = (s) => new Promise((r) => setTimeout(r, s * 1e3));
const api = async (method, path, body) => {
  const r = await fetch(`https://api.sand.etherfuse.com${path}`, {
    method,
    headers: { Authorization: process.env.ETHERFUSE_API_KEY, "Content-Type": "application/json" },
    body: body !== undefined ? JSON.stringify(body) : undefined,
  });
  if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
  const text = await r.text();
  return text ? JSON.parse(text) : {};
};
const org = `/organization/${process.env.ORGANIZATION_ID}`;
const hex = (buf) => Buffer.from(buf).toString("hex");

async function approve(orderId, authorization, privateKey) {
  const signature = await crypto.subtle.sign(
    { name: "ECDSA", hash: "SHA-256" },
    privateKey,
    new TextEncoder().encode(authorization.method.message),
  );
  // POST /organization/{organizationId}/order/{id}/authorization
  await api("POST", `${org}/order/${orderId}/authorization`, {
    id: authorization.id,
    signature: hex(signature),
  });
}

async function embeddedOnramp() {
  const { publicKey, privateKey } = await crypto.subtle.generateKey(
    { name: "ECDSA", namedCurve: "P-256" },
    true,
    ["sign", "verify"],
  );
  const signerPublicKey = hex(await crypto.subtle.exportKey("raw", publicKey));

  // POST /organization/{organizationId}/payment_account: we create the embedded wallet
  const wallet = await api("POST", `${org}/payment_account`, {
    kind: "embeddedWallet",
    label: "Embedded wallet",
    accountDetails: { kind: "signerPublicKey", chain: "stellar", signerPublicKey },
  });
  console.log("wallet account", wallet.id);

  // POST /organization/{organizationId}/quote: MXN from the bank account into the wallet
  const quote = await api("POST", `${org}/quote`, {
    source: { type: "stored", id: process.env.BANK_ACCOUNT_ID },
    destination: { type: "stored", id: wallet.id },
    sourceAmount: "100",
    sourceAsset: "MXN",
    destinationAsset: process.env.TARGET_ASSET,
  });

  // POST /organization/{organizationId}/order: the order id is the quoteId
  const order = await api("POST", `${org}/order`, { quoteId: quote.quoteId });

  // POST /organization/{organizationId}/order/{id}/fiat_received: sandbox only
  await api("POST", `${org}/order/${order.id}/fiat_received`, { amount: quote.sourceAmount });

  // GET /organization/{organizationId}/order/{id}: in production, subscribe to orderUpdated instead of polling
  let approvedId = null;
  while (true) {
    const current = await api("GET", `${org}/order/${order.id}`);
    if (current.status === "completed") {
      console.log("completed", current.destination);
      return;
    }
    if (["failed", "canceled", "refunded"].includes(current.status)) {
      throw new Error(`order ${current.status}: ${JSON.stringify(current.failure ?? {})}`);
    }
    if (current.status === "claimable" && current.authorization?.id !== approvedId) {
      // POST /organization/{organizationId}/order/{id}/claim: no body
      const claim = await api("POST", `${org}/order/${order.id}/claim`);
      const { authorization } = await api("GET", `${org}/order/${order.id}`);
      if (authorization?.id === claim.authorizationId) {
        try {
          await approve(order.id, authorization, privateKey);
          approvedId = authorization.id;
        } catch (e) {
          // 409 authorization-expired: we replaced it, so the next pass signs the new one
          if (!String(e.message).includes("authorization-expired")) throw e;
        }
      }
    }
    await sleep(15);
  }
}

embeddedOnramp().catch(console.error);
```

Each commented `api()` call links to the API reference:

| Call | Reference |
| - | - |
| `POST /organization/{organizationId}/payment_account` | [Register payment account](/v2/api-reference/payment-accounts/register-payment-account): we create the wallet |
| `POST /organization/{organizationId}/quote` | [Create quote](/v2/api-reference/quotes/create-quote): the quote expires 2 minutes after it is created |
| `POST /organization/{organizationId}/order` | [Create order](/v2/api-reference/orders/create-order): sending the same `quoteId` again returns the same order |
| `POST /organization/{organizationId}/order/{id}/fiat_received` | [Simulate fiat received](/v2/api-reference/sandbox/simulate-fiat-received): **sandbox only**; in production the customer pays the SPEI `fundingInstructions` |
| `GET /organization/{organizationId}/order/{id}` | [Get order](/v2/api-reference/orders/get-order) |
| `POST /organization/{organizationId}/order/{id}/claim` | [Prepare claim](/v2/api-reference/orders/prepare-claim): an embedded wallet gets `kind` `authorization` with an `authorizationId` |
| `POST /organization/{organizationId}/order/{id}/authorization` | [Submit authorization](/v2/api-reference/orders/submit-authorization): returns `409` `authorization-expired` when the authorization was replaced; read the order and sign the new one |

<Info>
  The order becomes `claimable` only when the wallet has no trustline for the asset yet, which is the case for a wallet's first onramp of that asset. Later onramps of the same asset go straight to `completed`.
</Info>

## Related

<CardGroup cols={2}>
  <Card title="Embedded Wallets" icon="wallet" href="/v2/guides/embedded-wallets" />

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