Selling the eSIM is half the product — the other half is what happens after: showing customers their remaining data, selling top-ups at the right moment, and reacting the moment an eSIM is installed, activated or running low — without polling. This guide covers all three.
New here? Start with part one: Sell eSIMs in Your App with Node.js — it sets up the esimfly client used below.
Query any eSIM by ICCID for its consumption and validity. Use it when the customer opens their eSIM in your app; for "running low" notifications let the esim.usage.threshold webhook (step 3) tell you instead of polling every eSIM.
// esimfly = the client from part one
const { data, validity } = await esimfly.esims.usage({ iccid: '8910300001234567890' });
console.log(data);
// { total_mb: 1024, used_mb: 256, remaining_mb: 768,
// usage_percentage: 25, is_unlimited: false }
console.log(validity.expires_at); // 2026-02-05T10:30:00Z
// Call it when the customer opens their eSIM and cache it for 5-15 minutes.
// For "running low" alerts you don't need to poll at all - see webhooks below.Top-ups are the highest-margin repeat sale in the eSIM business — the customer already has the profile installed, so there's zero onboarding friction. Always fetch compatible packages per ICCID first: not every package can top up every eSIM.
// 1. Which packages can THIS eSIM be topped up with? (they depend on the eSIM)
const { packages } = await esimfly.topups.packages({ iccid, limit: 100 });
// 2. Apply a top-up - charged from your balance, active immediately
const topup = await esimfly.topups.create({ iccid, packageCode: packages[0].package_code });
console.log(topup.esimData); // { newTotalVolumeGB: 8, newRemainingVolumeGB: 8, expiredTime: ... }
console.log(topup.newBalance);
// Top-ups are not idempotent: on a network timeout, check
// esimfly.esims.usage({ iccid }) before trying again.eSIMfly pushes a signed HTTP POST to your server when something changes on an eSIM: the profile is installed on a phone (esim.installed, seconds), the plan becomes active or expires (esim.status.changed), or remaining data crosses 500 / 200 / 100 / 50 MB (esim.usage.threshold). Register your URL once and pick the events you want.
// One-time setup: register your endpoint and pick the events you want
const { webhook } = await esimfly.webhooks.set({
webhookUrl: 'https://your-server.com/api/esimfly-webhook',
events: ['esim.installed', 'esim.status.changed', 'esim.usage.threshold'],
});
// SAVE webhook.secret (whsec_...) - shown once, needed to verify deliveries.
// Events you can subscribe to:
// esim.installed profile enabled on a phone for the first time (seconds)
// esim.status.changed NEW -> ACTIVE -> DEPLETED / EXPIRED (<= 30 min)
// esim.usage.threshold 500 / 200 / 100 / 50 MB remaining (seconds)
// esim.profile.updated every SM-DP+ state change (chatty - usually skip)
// esim.provisioned an asynchronously provisioned order is readyEvery delivery is signed with HMAC-SHA256 (header X-Webhook-Signature). Verify against the raw request body with a timing-safe comparison — never process an unsigned payload, and never verify against re-serialized JSON.
import { constructWebhookEvent, ESIMflyError } from '@esimfly/sdk';
// Express: keep the RAW body - the signature covers the exact bytes
app.post('/api/esimfly-webhook',
express.raw({ type: 'application/json' }),
async (req, res) => {
let event;
try {
event = constructWebhookEvent(
req.body, // raw Buffer
req.header('X-Webhook-Signature'), // sha256=<hex>
process.env.ESIMFLY_WEBHOOK_SECRET,
);
} catch (err) {
const badSignature = err instanceof ESIMflyError && err.code === 'INVALID_SIGNATURE';
return res.status(badSignature ? 401 : 400).end();
}
// Deduplicate: retries reuse the same X-Webhook-Id
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, data: event.data });
res.json({ received: true }); // ack within 10 s, then do the work
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 });
break;
case 'esim.usage.threshold':
if (event.data.threshold_remaining_mb <= 200) await sendLowDataPush(event.data.iccid);
break;
}
});X-Webhook-Id header so retries never double-deliver an eSIM.Suspend, cancel, SMS delivery, network events and more — the complete endpoint reference is in the documentation.