Firsty

Search...

Search...

Core concepts

eSIM installation

Deep links, QR codes and in-app install on iOS and Android.

Ordering an eSIM gets you an activationCode: the LPA string a device uses to download its profile. Installing it is your user's step, on their own device. Which methods you can offer depends on the platform and its OS version.

Whichever method you lead with, offer manual install alongside it. The other methods can fail on a particular device or OS version. Manual install works on every eSIM-capable device.

MethodiOSAndroid
Deep link17.4 and up10 and up
Manual installAlwaysAlways
In-app installNeeds Apple's eSIM entitlementNeeds your app's signing certificate hash on the profile (talk to sales)

Checking the device

Before you offer an eSIM, check that the device can take one:

GET /catalog/devices/compatibility
requests.get(
    "https://connect.test.firsty.app/api/v3/catalog/devices/compatibility",
    headers={
      "Authorization": "Bearer YOUR_ACCESS_TOKEN"
    },
    params={
      "brand": "Apple",
      "model": "iPhone 14 Pro"
    }
)
Response · 200
{
  "data": {
    "compatible": true
  }
}

GET /catalog/devices returns the whole list by brand, if you'd rather show it. On the CLI that's firsty catalog devices.

From iOS 17.4 and Android 10 (API 29), a link opens the system's own eSIM setup. Put the activationCode in carddata:

https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=<activationCode>
https://esimsetup.android.com/esim_qrcode_provisioning?carddata=<activationCode>

On iOS that's Apple's own install sheet, and it needs no entitlement. Below those versions the link doesn't work, so check the OS version before you show it and offer manual install instead.

Manual install

The user has two ways in:

  • Scan a QR code. Render the activationCode as a QR code on a second screen, since a device cannot scan its own display.
  • Type it in. On iOS that's Settings → Mobile Service → Add eSIM (Cellular in US English). On Android the path varies by manufacturer.

Typing takes two values, and both are inside the activationCode:

LPA:1$<SM-DP+ address>$<matching ID>

Show them as two separate fields the user can copy. The device's form labels the matching ID Activation Code, which is easy to confuse with the whole activationCode string.

In-app install

Your app can also install the eSIM itself, without sending the user to Settings. That needs approval on each platform first: Apple's eSIM entitlement on iOS, and your app's signing certificate hash on our profiles on Android.

iOS

In-app install uses Apple's CTCellularPlanProvisioning API. Apple only allows it for apps that hold the com.apple.CommCenter.fine-grained entitlement, with the value public-cellular-plan. To get it:

  1. Request it on Apple's eSIM access entitlement form, signed in with your Apple Developer account. The form asks for details of the carrier you work with, including that carrier's Apple Team ID.
  2. Once Apple approves it, add the entitlement to your app's provisioning profile and entitlements file.

Apple gives no processing time, and its advice is to push the request through the carrier's contact at Apple. Plan for a long lead time, and one you don't control.

At runtime, check supportsCellularPlan() before you call addPlan(with:). If it returns false, fall back to the deep link on iOS 17.4 and up, or to manual install.

Android

Android's eSIM API (EuiccManager) only installs a profile for an app whose signing certificate hash is in that profile's access rules. We put those hashes on the profiles. Talk to sales to send us the SHA-256 of your Play App Signing certificate, plus your upload or debug certificate for testing.

The same hash does a second job. Once the eSIM is installed, your app has carrier privileges on it. That's enough to set data roaming and the APN from your app, with no extra manifest permissions and without sending the user into Settings. Start from the eSIM's subscription:

val sub = context.getSystemService(SubscriptionManager::class.java)
    .activeSubscriptionInfoList.first()
val tm = context.getSystemService(TelephonyManager::class.java)
    .createForSubscriptionId(sub.subscriptionId)

Data roaming goes through setDataRoamingEnabled. It accepts carrier privileges but isn't in the public SDK, hence the reflection:

TelephonyManager::class.java
    .getDeclaredMethod("setDataRoamingEnabled", Boolean::class.javaPrimitiveType)
    .invoke(tm, true)

tm.isDataRoamingEnabled // read it back

Add the APN as a row in Telephony.Carriers:

val values = ContentValues().apply {
    put(Telephony.Carriers.NAME, "Firsty")
    put(Telephony.Carriers.APN, "<apn>")
    put(Telephony.Carriers.MCC, sub.mccString)
    put(Telephony.Carriers.MNC, sub.mncString)
    put(Telephony.Carriers.NUMERIC, sub.mccString + sub.mncString)
    put(Telephony.Carriers.TYPE, "default")
    put(Telephony.Carriers.SUBSCRIPTION_ID, sub.subscriptionId)
}
context.contentResolver.insert(Telephony.Carriers.CONTENT_URI, values)

Replace <apn> with the APN we give you; talk to sales for it. Update and delete go through the same ContentResolver and URI.

Following the install

Subscribe to the eSIM webhooks to follow the install from your backend:

EventWhat it means
esim.downloadedThe device has the profile. The install is underway.
esim.installedThe install finished.
esim.enabledThe profile is enabled on the device. The user has working data.
esim.faultedThe download or install failed. reason says why.

A fault is usually not the end of the attempt. The device can retry, and an esim.installed that follows supersedes it. The exception is download_order_expired: the download ran out of attempts, and the user needs a replacement eSIM.

Each one is a status change on the profile, covered with the rest in eSIM lifecycle.