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.
npm install @esimfly/sdk (zero dependencies, TypeScript types included)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'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,
})));
});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);
}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 });
});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;
}orderReference, ICCID and the install payload so users can re-open their eSIM later.newBalance and alert yourself before it runs dry.The full endpoint reference, Python and PHP examples, and an interactive playground live in the documentation.