This page is optional developer tooling, not the primary integration guide. Read Embedded Wallets first for concepts and per-step detail. Run the interactive tester, then copy the minimal server-side script below it.
Embedded wallets run on Stellar today. Quoting and ordering follow the standard procedure, so this page links out to Onramps and Offramps rather than re-teaching them. The minimal script below is the fastest server-side path; the main guide covers concepts and signing detail.
Sandbox tester
Sandbox keys only. Pasting an API key here sends authenticated requests from your browser to the Etherfuse API, the same pattern as the Swaps tutorial and the API Reference “Try it” playground. Use a sandbox API key only. Production integrations must call these endpoints server-side; never embed a production API key in client-side code.
The tester stores your sandbox API key in session storage for the current browser tab only. It covers the embedded-specific sequence (provision → quote → order → approval signing). For quote and order mechanics shared with any wallet, see the standard ramp guides linked above.
Paste your sandbox API key below and the tester generates an owner keypair in the browser, provisions a wallet, quotes, orders, simulates the fiat deposit, watches for the approval, signs it with the generated owner key, and broadcasts. Every request and response is shown inline so you can check your own payloads.
Minimal sandbox onramp
Copy-paste server-side script for a complete embedded-wallet onramp in sandbox: provision with PEM, quote with walletAddress, order with cryptoWalletId, simulate fiat, poll for the approval, sign, and broadcast. Set ETHERFUSE_API_KEY, BANK_ACCOUNT_ID, and TARGET_ASSET (a Stellar asset identifier from GET /ramp/assets).
import crypto from "node:crypto";
const sleep = (s) => new Promise((r) => setTimeout(r, s * 1e3));
const id = () => crypto.randomUUID();
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) : {};
};
// GET /ramp/me — customerId for quotes
const { id: customerId } = await api("GET", "/ramp/me");
// Generate owner P-256 keypair (public → provision; private → sign approvals)
const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
const pem = privateKey.export({ type: "pkcs8", format: "pem" });
const sign = (msg) => crypto.createSign("SHA256").update(msg).sign(pem).toString("hex");
async function embeddedOnramp() {
// POST /ramp/wallet — provision embedded wallet (signerPublicKeyPem)
const { walletId, publicKey: wallet } = await api("POST", "/ramp/wallet", {
walletId: id(),
signer: { signerPublicKeyPem: publicKey.export({ type: "spki", format: "pem" }) },
});
// POST /ramp/quote — onramp pricing; walletAddress folds reserve + trustline into fee
const quote = {
quoteId: id(), customerId, blockchain: "stellar", walletAddress: wallet, sourceAmount: "100",
quoteAssets: { type: "onramp", sourceAsset: "MXN", targetAsset: process.env.TARGET_ASSET },
};
await api("POST", "/ramp/quote", quote);
// POST /ramp/order — slim body with quoteId + cryptoWalletId
const orderId = id();
await api("POST", "/ramp/order", {
orderId, quoteId: quote.quoteId, bankAccountId: process.env.BANK_ACCOUNT_ID, cryptoWalletId: walletId,
});
// POST /ramp/order/fiat_received — sandbox only (production: your fiat rail fires this)
await api("POST", "/ramp/order/fiat_received", { orderId });
// GET /ramp/order + POST .../approvals — poll until completed; sign when approval pending; production should use registered webhooks or WebSockets
while (true) {
const order = await api("GET", `/ramp/order/${orderId}`);
if (order.status === "completed") break;
if (order.approval?.approvalMessage) {
const { approvalMessageId, approvalMessage } = order.approval;
await api("POST", `/ramp/order/${orderId}/approvals`, {
approvalMessageId, approvalMessage, signature: sign(approvalMessage),
});
}
await sleep(15);
}
}
embeddedOnramp().catch(console.error);
Each commented api() call links to the API reference:
| Call | Reference |
|---|
GET /ramp/me | Get organization identity: use id as customerId |
POST /ramp/wallet | Register wallet: PEM, hex, or JWK |
POST /ramp/quote | Get quote: pass walletAddress for reserve/trustline pricing |
POST /ramp/order | Create order: cryptoWalletId resolves the embedded wallet |
POST /ramp/order/fiat_received | Simulate fiat received: sandbox only; production waits on your fiat rail |
GET /ramp/order/{orderId} | Get order: re-read right before signing |
POST /ramp/order/{orderId}/approvals | Submit order approval: hex signature over approvalMessage |
Offramps follow the same approval loop; there is no fiat_received step and the approval appears once Etherfuse builds the burn transaction. See Offramps.
Worked example (test fixture)
The values below are a documentation fixture only; do not use this private key in production. They show the same approvalMessage signed once, yielding two common hex encodings Etherfuse accepts.
Bytes to sign (approvalMessage, copy verbatim, including quotes and escaping):
{"version":"1","timestamp":"2026-06-18T12:00:00Z","orderId":"4e9b2bd5-af60-4154-cd82-5ebf70913c44"}
Owner private key (PKCS#8 PEM):
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgl0AYSnGsUwCJzf/+
+B78kqY0U1J+0UUUgy2j/u6VWL2hRANCAATfJYawOOCqxGG8ope5LLHdES6LD7zB
n7Uc3rZTsAPHbM3HbOve4wstthHETWeGGD2wKgQz87uyVPnvB72arU9Z
-----END PRIVATE KEY-----
Normalized signerPublicKey (compressed SEC1 hex; what Etherfuse stores after provision, regardless of whether you sent hex, PEM, or JWK):
03df2586b038e0aac461bca297b92cb1dd112e8b0fbcc19fb51cdeb653b003c76c
Corresponding public key PEM (valid as signerPublicKeyPem at provision, or for offline verification):
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3yWGsDjgqsRhvKKXuSyx3REuiw+8
wZ+1HN62U7ADx2zNx2zr3uMLLbYRxE1nhhg9sCoEM/O7slT57we9mq1PWQ==
-----END PUBLIC KEY-----
Signatures over that message (submit either one; same underlying r and s, two hex layouts):
| Encoding | Typical source | Hex length | Value |
|---|
| DER-wrapped | Node, Java, Go, Erlang, OpenSSL | 142 | 304502205d5bfd6a52efdaadcd0ac2cff30fab2461b2484fd8e4dc4c894127aaa6678e86022100e814fc2404b1562a28d90121e540d6996e8484003790cd745fc78527ac07b766 |
Raw r‖s | Browser WebCrypto, JWS ES256 (same bytes as above) | 128 | 5d5bfd6a52efdaadcd0ac2cff30fab2461b2484fd8e4dc4c894127aaa6678e86e814fc2404b1562a28d90121e540d6996e8484003790cd745fc78527ac07b766 |
Both verify the same ECDSA P-256 / SHA-256 signature. Etherfuse normalizes either layout before broadcast. ECDSA is randomized, so signing again yields different hex, but any valid signature over the same message is accepted.
To verify the DER-wrapped fixture offline, paste the message, PEM public key, and DER signature into EncodeAll ECDSA Verify with curve P-256 and algorithm SHA256withECDSA. The result should be Valid. Most online verifiers expect DER, not the 128-character raw layout.
Submit with server-side hex (DER-wrapped, typical Node/Java signer):
{
"approvalMessageId": "5fac3ce6-b071-4265-de93-6fcf81a24d55",
"approvalMessage": "{\"version\":\"1\",\"timestamp\":\"2026-06-18T12:00:00Z\",\"orderId\":\"4e9b2bd5-af60-4154-cd82-5ebf70913c44\"}",
"publicKey": "03df2586b038e0aac461bca297b92cb1dd112e8b0fbcc19fb51cdeb653b003c76c",
"signature": "304502205d5bfd6a52efdaadcd0ac2cff30fab2461b2484fd8e4dc4c894127aaa6678e86022100e814fc2404b1562a28d90121e540d6996e8484003790cd745fc78527ac07b766"
}
Submit with browser hex (raw r‖s, typical WebCrypto signer):
{
"approvalMessageId": "5fac3ce6-b071-4265-de93-6fcf81a24d55",
"approvalMessage": "{\"version\":\"1\",\"timestamp\":\"2026-06-18T12:00:00Z\",\"orderId\":\"4e9b2bd5-af60-4154-cd82-5ebf70913c44\"}",
"publicKey": "03df2586b038e0aac461bca297b92cb1dd112e8b0fbcc19fb51cdeb653b003c76c",
"signature": "5d5bfd6a52efdaadcd0ac2cff30fab2461b2484fd8e4dc4c894127aaa6678e86e814fc2404b1562a28d90121e540d6996e8484003790cd745fc78527ac07b766"
}
In production, approvalMessage and approvalMessageId come from a fresh GET /ramp/order/ read; the fixture above uses a static message for illustration only.