eSimfly Logo
NODE.JS TUTORIAL · ~30 MINUTES

How to Sell eSIMs in Your App with Node.js

Build a working eSIM storefront on the eSIMfly API with the official @esimfly/sdk: authenticate, mirror the wholesale package catalog, create orders from your account balance, and hand your user a scannable QR code — no request signing to write.

Prerequisites

  • An eSIMfly business account with balance — get API access here
  • API credentials (access code + secret key) from Business Dashboard → Settings → API Keys
  • Node.js 18+ and the SDK: npm install @esimfly/sdk (zero dependencies, TypeScript types included)

1. Install the SDK and authenticate

Every request needs four headers — your access code, a unique request ID, a timestamp and an HMAC-SHA256 signature computed with your secret key. The SDK builds them on every call, so authentication is one constructor. Keep the client on your server: the secret key must never reach a browser or mobile app.

// $ npm install @esimfly/sdk

// esimfly.js - one client for your whole backend (server-side only!)
import { ESIMfly } from '@esimfly/sdk';

export const esimfly = new ESIMfly({
  accessCode: process.env.ESIMFLY_ACCESS_CODE, // esf_...
  secretKey: process.env.ESIMFLY_SECRET_KEY,   // sk_...
});

// Every call is signed for you: a fresh request ID (UUID v4), a millisecond
// timestamp and an HMAC-SHA256 signature of
//   timestamp + requestId + accessCode + requestBody
// computed with your secret key. Requests older than 5 minutes are rejected.
const { balance, currency } = await esimfly.balance.get();
console.log(balance, currency); // 250.00 'USD'

2. Browse the package catalog

The catalog returns every package you can sell with your wholesale price — coverage spans 200 countries and 450+ networks. Look packages up ad hoc while you build, but in production mirror the catalog into your own database on a schedule and serve your storefront from there; prices update as carriers change rates, and the order endpoint always charges the current price.

// Ad-hoc lookup: packages for one destination, with YOUR wholesale price
const { packages } = await esimfly.packages.list({ search: 'Turkey', type: 'local', limit: 50 });
console.log(packages[0]);
// { package_code: '1648812', name: 'Turkey 1 GB 7 Days', type: 'local',
//   data_amount_gb: 1, validity_days: 7, cost: 2.72, currency: 'USD', ... }

// Production: mirror the whole catalog into your database on a schedule
// (every 6-12 h) and serve your storefront from it. listAll()/sync() page
// with limit=100 and pause 1 s between pages, so a full sync stays inside
// the rate limit.
await esimfly.packages.sync(async (batch) => {
  await db.packages.upsertMany(batch.map((p) => ({
    packageCode: p.package_code,   // opaque - store it verbatim
    name: p.name,
    cost: p.cost,                  // your buy price
    sellPrice: p.cost * 1.3,       // your margin, your rules
    dataGb: p.data_amount_gb,
    validityDays: p.validity_days,
  })));
});

3. Create an order

One call with a package code buys the eSIM from your account balance and returns the complete installation payload — LPA string, QR code image, and one-tap install links. Always pass an idempotencyKey (your own order id): if the request times out, retrying with the same key returns the original order instead of charging you twice. Most orders complete synchronously; a few packages return status: "pending_details" and waitForEsim() polls until they are ready.

const order = await esimfly.orders.create({
  packageCode: '1648812',
  quantity: 1,
  idempotencyKey: cart.id, // your own id: a retry can never charge you twice
});

console.log(order.orderReference); // order_1692123456_ab7cd
console.log(order.newBalance);     // 47.28 - charged from your balance

const esim = order.esims[0];
console.log(esim.iccid);           // 8932042000010078801
console.log(esim.lpaString);       // LPA:1$rsp-3104.idemia.io$DOAZJ-...

// A few packages are provisioned asynchronously (status: 'pending_details').
// waitForEsim() polls the order every 20 s for up to 10 minutes:
if (order.status === 'pending_details') {
  const ready = await esimfly.orders.waitForEsim(order.orderReference);
  console.log(ready.esim.iccid);
}

4. Deliver the eSIM to your user

You get three delivery mechanisms per order — show all of them: the QR code for cross-device installs, the Apple universal link for one-tap iOS installation, and the raw LPA string as a manual fallback.

// Everything your user needs is in order.esims[i]:
//
// esim.lpaString               -> render your own QR code from it (any QR library)
//                                 and show it for manual entry
//                                 (Settings > Cellular > Add eSIM > Enter manually)
// esim.directAppleInstallUrl   -> one-tap install on iOS 17.4+ (universal link)
// esim.directAndroidInstallUrl -> one-tap install on supported Android devices
// esim.qrCodeUrl               -> ready-made base64 PNG if you prefer not to render

await db.esims.create({
  orderReference: order.orderReference,
  iccid: esim.iccid,
  lpaString: esim.lpaString,
  appleUrl: esim.directAppleInstallUrl,
  androidUrl: esim.directAndroidInstallUrl,
  expiresAt: esim.expired_time,
});

app.get('/my-esim/:orderRef', async (req, res) => {
  const esim = await db.esims.findByOrder(req.params.orderRef);
  res.render('esim', { lpa: esim.lpaString, appleUrl: esim.appleUrl, androidUrl: esim.androidUrl });
});

5. Go to production

Every failure is an ESIMflyError with the API's code — branch on it, never on the message:

import { ESIMflyError } from '@esimfly/sdk';

try {
  await esimfly.orders.create({ packageCode, idempotencyKey: cart.id });
} catch (err) {
  if (err instanceof ESIMflyError) {
    switch (err.code) {
      case 'INSUFFICIENT_BALANCE': // err.response.needToLoad tells you how much
        return alertOps(err.response);
      case 'INVALID_PACKAGE':      // catalog changed - resync
        return resyncCatalog();
      case 'RATE_LIMIT_EXCEEDED':
        return retryLater();
    }
  }
  throw err;
}
  • Store the order response — persist orderReference, ICCID and the install payload so users can re-open their eSIM later.
  • Handle balance errors — orders fail cleanly when balance is insufficient; monitor newBalance and alert yourself before it runs dry.
  • Respect the limits — limits are per API key (typically 100 requests/minute, 1,000/hour, 10,000/day); the SDK's paced catalog sync and cached usage lookups keep a normal storefront far below them. Keep server clocks in sync via NTP: signatures older than 5 minutes are rejected.
  • Subscribe to webhooks — get told the moment an eSIM is installed, becomes active or runs low on data instead of polling. Continue with Top-ups, Usage Tracking & Webhooks.

Ready to build?

The full endpoint reference, Python and PHP examples, and an interactive playground live in the documentation.