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: authenticate with HMAC-SHA256, pull the wholesale package catalog, create orders from your account balance, and hand your user a scannable QR code — all in plain Node.js with no SDK required.

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+ (built-in fetch) and the uuid package

1. Authenticate with HMAC-SHA256

Every request carries four headers: your access code, a unique request ID (UUID v4), a millisecond timestamp, and an HMAC-SHA256 signature of timestamp + requestId + accessCode + requestBody computed with your secret key. This prevents tampering and replay attacks — requests older than 5 minutes are rejected.

const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');

const BASE_URL = 'https://esimfly.net';
const ACCESS_CODE = process.env.ESIMFLY_ACCESS_CODE; // esf_...
const SECRET_KEY = process.env.ESIMFLY_SECRET_KEY;   // sk_...

function hmacHeaders(requestBody = '') {
  const timestamp = Date.now().toString();
  const requestId = uuidv4();

  // signData = Timestamp + RequestID + AccessCode + RequestBody
  const signData = timestamp + requestId + ACCESS_CODE + requestBody;

  const signature = crypto
    .createHmac('sha256', SECRET_KEY)
    .update(signData)
    .digest('hex')
    .toUpperCase();

  return {
    'RT-AccessCode': ACCESS_CODE,
    'RT-RequestID': requestId,
    'RT-Timestamp': timestamp,
    'RT-Signature': signature,
    'Content-Type': 'application/json'
  };
}

2. Browse the package catalog

The catalog returns every package you can sell with your wholesale price — coverage spans 200 countries and 450+ networks. Cache it and refresh periodically; prices update as carriers change rates.

async function getPackages(country) {
  const query = country ? '?country=' + encodeURIComponent(country) : '';
  const res = await fetch(BASE_URL + '/api/v1/business/esims/packages' + query, {
    headers: hmacHeaders() // GET request: empty body in the signature
  });
  const data = await res.json();
  return data.data.packages;
}

// Example: list Turkey packages with your wholesale prices
const packages = await getPackages('Turkey');
console.log(packages[0]);
// { package_code: 'TR1GB7D', name: 'Turkey 1 GB 7 Days',
//   price: 2.72, data_amount: 1, duration: 7, ... }

3. Create an order

One POST 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. For most providers this is synchronous; a few (like KDDI Japan) return status: "pending_details" and deliver the eSIM via webhook a few minutes later — covered in part two.

async function createOrder(packageCode, quantity = 1) {
  const body = JSON.stringify({ packageCode, quantity });

  const res = await fetch(BASE_URL + '/api/v1/business/esims/order', {
    method: 'POST',
    headers: hmacHeaders(body), // body is part of the HMAC signature
    body
  });
  return res.json();
}

const order = await createOrder('TR1GB7D');
console.log(order.orderReference); // order_1692123456_ab7cd
console.log(order.lpaString);      // LPA:1$rsp-3104.idemia.io$DOAZJ-...
console.log(order.newBalance);     // 47.28 - charged from your balance

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.

// The order response contains everything your user needs:
//
// order.qrCodeUrl              -> base64 PNG, render it directly:
//                                 <img src={order.qrCodeUrl} alt="Scan to install eSIM" />
//
// order.directAppleInstallUrl  -> one-tap install on iOS 17.4+
//                                 (universal link, no QR scanning needed)
//
// order.lpaString              -> manual entry fallback
//                                 (Settings > Cellular > Add eSIM > Enter manually)

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

5. Go to production

  • 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 — 1,000 requests/hour per account, and each request's timestamp must be within 5 minutes (keep server clocks in sync via NTP).
  • Set up webhooks for async providers — KDDI Japan provisions in the background and delivers the eSIM via webhook; everything else (status, usage) is a simple polled query. 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.