@esimfly/sdk
v0.1.0
Published
Official Node.js / TypeScript SDK for the eSIMfly Business API — sell eSIMs, manage usage and receive webhooks
Maintainers
Readme
@esimfly/sdk — Node.js / TypeScript SDK for the eSIMfly Business API
Sell eSIMs from your own app: browse the catalogue, order, top up, track usage and receive signed webhooks — with request signing, retries and error handling done for you.
- Zero runtime dependencies, Node.js 18+ (uses the built-in
fetch) - TypeScript types for every request and response, ESM + CommonJS
- HMAC-SHA256 signing with a fresh request id per call
- Idempotency keys on orders, paced catalogue sync, pending-order polling
verifyWebhookSignature/constructWebhookEventfor deliveries
Full API reference: https://docs.esimfly.net · Get credentials: Business Dashboard → Settings → API Keys.
Keep the SDK on your server. The secret key must never be shipped to a browser or mobile app.
Install
npm install @esimfly/sdkQuick start
import { ESIMfly } from '@esimfly/sdk';
const esimfly = new ESIMfly({
accessCode: process.env.ESIMFLY_ACCESS_CODE!, // esf_...
secretKey: process.env.ESIMFLY_SECRET_KEY!, // sk_...
});
const { balance, currency } = await esimfly.balance.get();
console.log(`Balance: ${balance} ${currency}`);Sell an eSIM in three steps
1. Sync the catalogue into your database (scheduled, every 6–12 h)
Do not call the catalogue per customer request — copy it and serve your storefront from your own
tables. listAll pages with limit=100 and pauses 1 s between pages to stay inside the rate limit.
const runStartedAt = new Date();
await esimfly.packages.sync(async (packages) => {
await db.packages.upsertMany(
packages.map((p) => ({
packageCode: p.package_code, // opaque — store verbatim
name: p.name,
region: p.region,
type: p.type, // local | regional | global
dataGb: p.data_amount_gb,
validityDays: p.validity_days,
cost: p.cost, // your buy price
currency: p.currency,
sellPrice: p.cost * 1.3, // your margin, your rules
countries: p.countries ?? [],
lastSeenAt: runStartedAt,
isActive: true,
})),
);
});
// Only after a fully successful run: hide packages that disappeared (never delete them —
// your orders reference them).
await db.packages.updateMany({ where: { lastSeenAt: { lt: runStartedAt } }, data: { isActive: false } });2. Create the order (inside your checkout)
import { ESIMflyError } from '@esimfly/sdk';
try {
const order = await esimfly.orders.create({
packageCode: cart.packageCode,
quantity: 1,
idempotencyKey: cart.id, // your own id — a retry can never charge twice
});
await db.orders.update(cart.id, {
orderReference: order.orderReference,
amount: order.amount,
currency: order.currency,
status: order.status, // 'completed' | 'pending_details'
});
for (const esim of order.esims) {
await db.esims.create({
iccid: esim.iccid,
lpaString: esim.lpaString, // render your own QR code from this
appleInstallUrl: esim.directAppleInstallUrl,
androidInstallUrl: esim.directAndroidInstallUrl,
expiresAt: esim.expired_time,
totalBytes: esim.total_volume,
});
}
if (order.status === 'pending_details') {
// Rare (asynchronously provisioned packages). Poll up to 10 minutes, then hand to support.
const ready = await esimfly.orders.waitForEsim(order.orderReference);
console.log('eSIM ready:', ready.esim.iccid);
}
} catch (err) {
if (err instanceof ESIMflyError && err.code === 'INSUFFICIENT_BALANCE') {
const { needToLoad } = err.response as { needToLoad: number };
alertOps(`Top up the eSIMfly balance: ${needToLoad}`);
} else {
throw err;
}
}3. Deliver and support
// "My eSIM" screen — cheap, cache 5–15 min
const usage = await esimfly.esims.usage({ iccid });
console.log(`${usage.data.remaining_mb} MB left, expires ${usage.validity.expires_at}`);
// Top-up screen
const { packages } = await esimfly.topups.packages({ iccid, limit: 100 });
await esimfly.topups.create({ iccid, packageCode: packages[0].package_code });
// Support console — live from the network, throttle per eSIM
const live = await esimfly.esims.status({ iccid });
console.log(live.last_network.operator, live.device.model, live.data_usage.used_gb);
const events = await esimfly.esims.networkEvents({ iccid }); // events[].is_allowed === false → wrong network
// Actions
await esimfly.esims.suspend({ iccid }); // eSIMfly-network eSIMs only
await esimfly.esims.activate({ iccid });
await esimfly.esims.sendSms({ iccid }, 'Your eSIM is ready. Enable Data Roaming to connect.');
await esimfly.esims.cancel({ iccid }); // only before installation — refunds to your balanceWebhooks
Subscribe once, then react to events instead of polling.
const { webhook } = await esimfly.webhooks.set({
webhookUrl: 'https://your-server.com/api/esimfly-webhook',
events: ['esim.installed', 'esim.status.changed', 'esim.usage.threshold'],
});
await secrets.save('ESIMFLY_WEBHOOK_SECRET', webhook.secret); // shown onceReceiver (Express) — verify on the raw body, dedupe on X-Webhook-Id, answer within 10 s:
import express from 'express';
import { constructWebhookEvent, ESIMflyError } from '@esimfly/sdk';
app.post('/api/esimfly-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = constructWebhookEvent(req.body, req.header('X-Webhook-Signature'), process.env.ESIMFLY_WEBHOOK_SECRET!);
} catch (err) {
return res.status(err instanceof ESIMflyError && err.code === 'INVALID_SIGNATURE' ? 401 : 400).end();
}
const deliveryId = req.header('X-Webhook-Id')!;
if (await db.webhookDeliveries.exists(deliveryId)) return res.json({ received: true });
await db.webhookDeliveries.insert({ id: deliveryId, event: event.event, payload: event.data });
res.json({ received: true });
// process after responding
switch (event.event) {
case 'esim.installed':
await db.esims.update({ iccid: event.data.iccid }, { installedAt: event.data.installed_at });
break;
case 'esim.status.changed':
await db.esims.update({ iccid: event.data.iccid }, { status: event.data.new_status, expiresAt: event.data.expiry_date });
break;
case 'esim.usage.threshold':
if ((event.data.threshold_remaining_mb ?? Infinity) <= 200) await notifyLowData(event.data.iccid);
break;
}
});| Event | When | Latency |
|---|---|---|
| esim.installed | profile enabled on a device for the first time | seconds |
| esim.profile.updated | every SM-DP+ state change (chatty — usually skip) | seconds |
| esim.usage.threshold | 500 / 200 / 100 / 50 MB remaining | seconds |
| esim.status.changed | NEW → ACTIVE → DEPLETED / EXPIRED / CANCELLED | ≤ 30 min |
| esim.provisioned | asynchronously provisioned order ready | seconds |
Errors
Every failure is an ESIMflyError. Branch on code, never on the message.
import { ESIMflyError } from '@esimfly/sdk';
try {
await esimfly.topups.create({ iccid, packageCode });
} catch (err) {
if (err instanceof ESIMflyError) {
err.code; // 'ESIM_NOT_TOPPABLE' | 'INSUFFICIENT_BALANCE' | 'RATE_LIMIT_EXCEEDED' | ...
err.status; // HTTP status
err.response; // parsed API body (extra fields such as needToLoad, details.ineligibleEsims)
err.requestId; // the RT-RequestID that was sent — quote it to support
err.isRetryable; // network / timeout / 5xx
}
}Retries: GETs and orders that carry an idempotencyKey are retried up to maxRetries (default 2)
on network errors, timeouts and 5xx, always with a fresh request id. Top-ups and orders without a
key are never retried automatically — on a timeout, check esims.usage({ iccid }) before retrying.
Rate limits are per API key (typically 100/min, 1,000/h, 10,000/day). The last response's headers
are on esimfly.rateLimit (limit, remaining, reset); a rejection surfaces as RATE_LIMIT_EXCEEDED.
Configuration
new ESIMfly({
accessCode: 'esf_...',
secretKey: 'sk_...',
baseUrl: 'https://esimfly.net/api/v1/business', // default
timeoutMs: 30_000, // default
maxRetries: 2, // default; 0 disables
fetch: customFetch, // proxies, tests
userAgent: 'my-shop/2.1', // appended to the SDK user agent
});API surface
| Resource | Methods |
|---|---|
| balance | get() |
| packages | list(params), listAll(options) (async iterator), sync(handler, options) |
| orders | create(params), get(orderReference), waitForEsim(orderReference, options), list(params) |
| esims | list(params), find(iccid), usage({ iccid } \| { orderId }), status(id), networkEvents(id), usageReport(id, days), suspend(id), activate(id), cancel(id), sendSms(id, message) |
| topups | packages({ iccid }), create({ iccid, packageCode }) |
| webhooks | get(), set({ webhookUrl, events }) |
| helpers | verifyWebhookSignature(rawBody, header, secret), constructWebhookEvent(rawBody, header, secret), signRequest(...) |
id is { iccid } or { esimId }.
Support
[email protected] · https://docs.esimfly.net
