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 handling the one provider (KDDI Japan) that delivers eSIMs asynchronously via webhook - everything else is served by simple polled queries. This guide covers all three.

New here? Start with part one: Sell eSIMs in Your App with Node.js — it builds the hmacHeaders() helper used below.

1. Track live data usage

Query any eSIM by ICCID for real-time consumption. The interesting business logic lives on top of this: notify at 80% usage, surface remaining data in your app, or trigger an automatic top-up offer before the customer runs out mid-trip.

// hmacHeaders() from part one of this tutorial
async function getUsage(iccid) {
  const query = '?iccid=' + encodeURIComponent(iccid);
  const res = await fetch(
    BASE_URL + '/api/v1/business/esims/usage/query' + query,
    { headers: hmacHeaders() }
  );
  return res.json();
}

const usage = await getUsage('8910300001234567890');
console.log(usage.data.data);
// { total_mb: 1024, used_mb: 256, remaining_mb: 768,
//   usage_percentage: 25, is_unlimited: false }

// Perfect for "80% used - top up now?" push notifications

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?
async function getTopupPackages(iccid) {
  const query = '?iccid=' + encodeURIComponent(iccid);
  const res = await fetch(
    BASE_URL + '/api/v1/business/topup/packages' + query,
    { headers: hmacHeaders() }
  );
  const data = await res.json();
  return data.data.packages;
}

// 2. Apply a top-up - charged from your balance, active immediately
async function topUp(iccid, packageCode) {
  const body = JSON.stringify({ iccid, packageCode });
  const res = await fetch(BASE_URL + '/api/v1/business/topup/order', {
    method: 'POST',
    headers: hmacHeaders(body),
    body
  });
  return res.json();
}

3. Receive webhooks (no polling)

Some providers (like KDDI Japan) provision asynchronously: your order returns status: "pending_details" and the eSIM details arrive by webhook 1–5 minutes later. Register a global webhook URL once, or pass callbackUrl per order.

// One-time setup: register your webhook endpoint
const body = JSON.stringify({
  webhook_url: 'https://your-server.com/api/esim-callback',
  events: ['esim.provisioned']
});

const res = await fetch(BASE_URL + '/api/v1/business/webhooks', {
  method: 'PUT',
  headers: hmacHeaders(body),
  body
});

const { webhook } = await res.json();
// SAVE webhook.secret (whsec_...) - you need it to verify signatures.
// Alternative: pass callbackUrl per-order in the order request instead.

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.

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signatureHeader, webhookSecret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', webhookSecret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}

// Express: use the RAW body for verification, not the parsed JSON
app.post('/api/esim-callback',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-webhook-signature'];
    const rawBody = req.body.toString();

    if (!verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(rawBody);
    if (event.event === 'esim.provisioned') {
      const { order_reference, iccid, qr_code_url, lpa_string } = event.data;
      // The eSIM is ready - deliver it to your customer now
    }

    res.status(200).send('ok'); // always ack fast, process async
  });

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.
  • Poll usage politely — usage data doesn't change by the second; cache per-ICCID for a few minutes and stay well inside the 1,000 req/hour limit.

Build the full lifecycle

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