eSimfly Logo
NODE.JS TUTORIAL · PART 2

eSIM Top-Ups, Usage Tracking & Webhooks

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.

1. Track live data usage

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.

2. Sell top-ups on existing eSIMs

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.

3. Receive webhooks (no polling)

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 ready

Always verify the signature

Every 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;
    }
  });

Production checklist

  • Ack webhooks fast — return 200 immediately and process the payload async; slow endpoints get retried and you'll see duplicates.
  • Deduplicate by delivery ID — use the X-Webhook-Id header so retries never double-deliver an eSIM.
  • Let webhooks do the watching — subscribe to installed / status / low-data events and only query usage when a customer opens their eSIM (cache it for a few minutes). Limits are per API key, typically 100 requests/minute and 1,000/hour.

Build the full lifecycle

Suspend, cancel, SMS delivery, network events and more — the complete endpoint reference is in the documentation.