Implementing eSIM Profile Activation in Mobile App
eSIM activation is the most painful telecom user experience if implemented wrong. User enters activation code, sees spinner 30 seconds, gets "Activation error." What went wrong — app doesn't know. SM-DP+ server returned SGP.22 error code, LPA processed it, platform wrapped it in opaque resultCode. Task — dig to real cause and show human-readable message.
Activation Code: Formats and Sources
SGP.22 defines two ways to deliver activation code to user:
QR code — standard format: LPA:1$smdp-plus.operator.com$ABC123XYZ. Scanning — via native platform capabilities or built-in app scanner. On iOS can open system activation directly.
Manual Entry — same string code, entered manually. Validate format before sending: LPA:1$ presence, correct SM-DP+ FQDN, Matching ID without forbidden characters.
// Validation of activation code format
fun validateActivationCode(code: String): Boolean {
val pattern = Regex("""^LPA:1\$[a-zA-Z0-9\-.]+\$[a-zA-Z0-9\-]+(\$[a-zA-Z0-9.]+(\$[01])?)?$""")
return pattern.matches(code.trim())
}
Android: Step-by-Step Activation with Error Handling
EuiccManager.downloadSubscription() is the main method for carrier-privileged apps:
private fun activateEsimProfile(activationCode: String) {
val subscription = DownloadableSubscription.forActivationCode(activationCode)
// switchAfterDownload flag: true — profile activates immediately
// false — loads in disabled state, activated separately
euiccManager.downloadSubscription(
subscription,
switchAfterDownload = true,
cancellationSignal = CancellationSignal(),
executor = mainExecutor
) { resultCode, extras ->
when (resultCode) {
EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK -> {
val subscriptionId = extras?.getInt(EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_ID) ?: -1
onActivationSuccess(subscriptionId)
}
EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR -> {
// Requires user confirmation
val intent = extras?.getParcelable<PendingIntent>(
EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_RESOLUTION_INTENT
)
intent?.send() // Opens system consent dialog
}
EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_ERROR -> {
val detailedCode = extras?.getInt(
EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, -1
) ?: -1
val operationCode = detailedCode and 0xFF
val errorCode = (detailedCode shr 8) and 0xFF
showActivationError(operationCode, errorCode)
}
}
}
}
Error details via EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE:
| Operation Code | Error Code | Cause | Tell User |
|---|---|---|---|
| 1 (DOWNLOAD) | 2 | Invalid activation code | "Check activation code" |
| 1 (DOWNLOAD) | 3 | Code already used | "This code already activated" |
| 2 (INSTALL) | 8 | No eUICC space | "Delete unused profile" |
| 3 (SWITCH) | 1 | SM-DP+ network error | "Check internet connection" |
iOS: System UI and QR
On iOS without carrier entitlement — only system activation dialog. Opens via URL:
func activateViaNativeUI(activationCode: String) {
// Form URL for system activation
let urlString = "com.apple.esim://\(activationCode)"
if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url) { success in
if success {
// System UI opened, wait for result via Universal Link callback
startPollingForNewProfile()
}
}
}
}
After activation via system UI — no direct callback to app. Need to poll CTSubscriptionManager or listen for CTSubscriberTokenRefreshed notification.
For carrier apps with entitlement — CTSubscriptionManagerDelegate:
import CoreTelephony
class ESIMActivationManager: NSObject, CTSubscriptionManagerDelegate {
let subscriptionManager = CTSubscriptionManager()
func activateProfile(activationCode: String) {
subscriptionManager.delegate = self
subscriptionManager.setAccountIdentifier(activationCode)
}
func subscriptionManagerCompleted(_ manager: CTSubscriptionManager) {
DispatchQueue.main.async {
self.handleActivationComplete()
}
}
}
States and Loading UX
eSIM activation takes 15 seconds to 3 minutes — SM-DP+ generates profile, LPA downloads and installs (usually 300–500 KB encrypted data). During this user shouldn't see static spinner.
Proper UX: steps with current state:
- Check activation code → Format validation
- Connect to operator → SM-DP+ request
- Download profile → Progress bar (if LPA provides progress)
- Install → Write to eUICC
- Activate → Switch to new profile
On Android downloadSubscription doesn't provide step-by-step progress. Fake progress with time estimation looks better than static spinner. Real progress only if using custom LPA via TelephonyManager.setPreferredOpportunisticDataSubscription.
Timeline
eSIM activation via Intent/system UI with error code handling: 1–2 weeks. Full EuiccManager integration for carrier app (Android) + CoreTelephony for iOS: 1–3 months including operator coordination and access setup.







