DEV Community

devhop
devhop

Posted on

Building an eSIM Provisioning Flow, From Order to Installed Profile

If you've ever bought a travel data plan and had it working before the plane landed, you've used eSIM provisioning. From the outside it looks like magic: scan a QR code, tap "Add", done. From the inside it's a surprisingly well specified pipeline that any developer can plug into.

This post walks through how a consumer eSIM provisioning flow actually works, what the moving parts are, and how you'd design one in your own product whether that's a travel app, an IoT dashboard, or an internal tool for issuing connectivity to a distributed team.

The three actors you need to know

Everything in consumer eSIM is defined by the GSMA SGP.22 specification (the "Remote SIM Provisioning" architecture for consumer devices). Three components matter:

  • eUICC: the embedded chip inside the phone, tablet or laptop. It can hold multiple carrier profiles and switch between them. Basically a small, secure SIM that can hold several carriers at once.
  • SM-DP+ (Subscription Manager Data Preparation, "plus") the carrier-side server that stores prepared profiles and hands them out. This is the endpoint the device talks to when downloading a plan.
  • LPA (Local Profile Assistant) the client on the device that speaks to the SM-DP+, verifies certificates, downloads the profile and installs it on the eUICC. On iOS and Android this is built into the OS.

The whole flow: your backend asks a provider to prepare a profile, the provider's SM-DP+ holds it, the device's LPA fetches it, and the eUICC installs it.

What's actually inside a QR code

The thing the user scans is just a string with a fixed format:

LPA:1$smdp.example.com$ABC123-MATCHING-ID
Enter fullscreen mode Exit fullscreen mode

Three fields, $-separated:

  1. LPA:1, the format version.
  2. The SM-DP+ address the device should contact.
  3. A matching ID, a one-time token that identifies the prepared profile waiting for this user.

There is no secret key in the QR code. The security comes from mutual TLS and certificate chains between the LPA and SM-DP+, rooted in the GSMA CI (Certificate Issuer). The matching ID is essentially a claim ticket.

Practical consequence for you as a developer: you don't need to render a QR image server-side. Ship the activation string and let the client render it, or better, on modern devices use the OS deep-link / universal-link installers that consume the same string without a camera.

Designing the provisioning flow

Here's the architecture most eSIM-enabled products converge on. Nothing unusual here, it's a normal order, fulfilment and delivery pipeline with a telecom step in the middle.

┌──────────┐   1. order    ┌──────────────┐   2. create   ┌──────────────┐
│  Client  │ ────────────► │ Your backend │ ────────────► │ eSIM provider│
│ (web/app)│               │              │               │   (API)      │
└──────────┘               └──────────────┘               └──────┬───────┘
      ▲                           │                              │
      │ 4. activation code        │ 3. webhook: profile ready    │
      │    + install button       │◄─────────────────────────────┘
      │                           ▼
      └───────────────────── deliver (in-app, email, SMS)

┌──────────┐   5. LPA fetches profile   ┌──────────────┐
│  Device  │ ─────────────────────────► │   SM-DP+     │
└──────────┘                            └──────────────┘
Enter fullscreen mode Exit fullscreen mode

Step 1: Model the product, not the SIM

Your catalogue entity is a plan: region or country coverage, data allowance, validity window, whether it's data-only, price. The eSIM itself is a fulfilment artefact, not something the user browses. Keep them separate in your schema:

type Plan = {
  id: string;
  coverage: string[];      // ISO country codes
  dataMb: number;
  validityDays: number;
  dataOnly: boolean;
};

type Esim = {
  id: string;
  planId: string;
  orderId: string;
  iccid?: string;          // known after provisioning
  activationCode?: string; // LPA:1$...
  status: "pending" | "ready" | "installed" | "active" | "expired";
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Provision asynchronously

Almost every eSIM provider exposes a REST API where you POST an order and get back an eSIM record with an activation code. Some return it synchronously, many finalise it via webhook a few seconds later. Design for the async case from day one, even if your first provider happens to be synchronous.

// pseudo-code, provider-agnostic
const order = await provider.createEsim({ planId, externalRef: orderId });

await db.esims.insert({
  id: order.id,
  planId,
  orderId,
  status: order.activationCode ? "ready" : "pending",
  activationCode: order.activationCode,
});
Enter fullscreen mode Exit fullscreen mode

And the webhook handler:

app.post("/webhooks/esim", verifySignature, async (req, res) => {
  const { esimId, iccid, activationCode, event } = req.body;

  if (event === "profile.ready") {
    await db.esims.update(esimId, { iccid, activationCode, status: "ready" });
    await notifyUser(esimId);
  }
  if (event === "profile.installed") {
    await db.esims.update(esimId, { status: "installed" });
  }
  res.sendStatus(200);
});
Enter fullscreen mode Exit fullscreen mode

Two things that bite people here: make the handler idempotent (providers retry), and verify the webhook signature before trusting anything in the body.

Step 3: Deliver the activation code the right way

You have three delivery options, in decreasing order of user friction:

  1. QR code. Universal, but requires a second device to scan from (you can't scan your own screen). Fine for desktop purchases.
  2. Manual entry. Show the SM-DP+ address and matching ID separately. Painful, but it's the fallback that always works.
  3. One-tap install. On iOS 17.4+ you can open a universal link of the form https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA:1$..., and Android supports an LPA: intent handled by the system. This removes the camera entirely.

Detect the platform and default to option 3, keep 1 and 2 visible as fallbacks. Whatever you do, also email the code, because users install eSIMs at airports with 4% battery and no patience.

Step 4: Track lifecycle, not just delivery

An eSIM has a lifecycle beyond "sent": installed on device, activated on first network attach, consuming data, then expired or exhausted. Providers expose usage endpoints (typically remaining data + expiry). Poll them or subscribe to usage webhooks and surface the numbers in your UI. This is where most of the support tickets come from, so invest here.

Gotchas from the field

  • One activation code, one install. Once a profile is downloaded, the matching ID is consumed. If the user deletes the profile from their phone, it's gone and you'll need to issue a new eSIM. Warn users loudly before they delete.
  • Device compatibility isn't binary. Some phones have eSIM hardware but are carrier-locked. Some regional variants of a model lack eSIM entirely. Ship a compatibility checker based on the model, but tell the user it's a best guess.
  • Data-only plans have no phone number. Users expect to receive SMS 2FA codes on their new eSIM. Set expectations in the product copy.
  • Roaming is not local. Many travel eSIMs are technically roaming on a partner network, which affects speed caps and 5G availability. Show the underlying network operator per country when the provider gives you that data.
  • APN settings. Most profiles configure the APN automatically, but not all. Keep the APN in your delivery email as a fallback.

A minimal end-to-end checklist

  1. Plan catalogue with coverage, allowance, validity.
  2. Order comes in, you call the provider API and store a pending eSIM.
  3. Webhook handler (idempotent, signature-verified) flips it to ready.
  4. Platform-aware delivery: one-tap link, QR, manual, plus email.
  5. Usage polling or webhooks to show remaining data and expiry.
  6. Support tooling: look up an eSIM by ICCID, re-send activation details, see install status.

If you can build a webhook-driven order pipeline, you can build an eSIM product. The telecom part is abstracted away behind the SM-DP+. Your job is the state machine and the UX around it.

Resources

  • GSMA SGP.22 RSP Technical Specification: the consumer eSIM spec. The section on the activation code format is the one to read first.
  • GSMA eSIM overview: high-level architecture and terminology.
  • Android EuiccManager: the Android API surface for LPA interactions if you're building a native client.
  • eSIM Center: a live example of a consumer travel eSIM product built on this exact flow: plan catalogue by country/region, instant activation-code delivery, and usage tracking. Useful to click through if you want to see how the UX decisions above look in production.

Have you shipped an eSIM integration? I'd be curious which provider APIs you found easiest to work with and what surprised you, drop it in the comments.

Top comments (0)