Android eSIM Management: EuiccManager vs EuiccCardManager
Why Android's eSIM API Architecture Matters
If you are building an eSIM feature into an Android app, the first thing you will discover is that the platform gives you two very different APIs to work with. They look similar on the surface — both are system services, both deal with eSIM profiles — but they operate at completely different levels of abstraction and serve completely different callers.、
This distinction matters more than most developers realize. Use the wrong API in the wrong context, and your calls will fail silently or throw security exceptions before you even reach the eUICC chip. Understanding when to use EuiccManager and when to reach for EuiccCardManager is the difference between a working integration and a week of debugging.
Roami's eSIM platform abstracts much of this complexity away for developers, providing a REST API that handles the entire profile provisioning workflow. But if you are building a carrier app, an LPA, or a travel app with deep eSIM integration, understanding Android's native eUICC APIs is essential.
The Two-Layer Architecture
Android's eUICC support is built on a clear separation of concerns.
EuiccManager is the public-facing API, available in the android.telephony.euicc package. It is designed for carrier apps — applications that need to download, switch, or delete eSIM profiles on behalf of a mobile carrier. Carrier apps do not need to be system apps, but they must have carrier privileges granted by the eUICC profile itself. These privileges are embedded in the profile metadata and enforced by the platform.
EuiccCardManager is a system API, accessible only to applications with system privileges. It provides ES10x functions based on GSMA RSP v2.0 — the low-level commands that actually talk to the eUICC chip. The caller of EuiccCardManager must be an LPA (Local Profile Assistant), and this is enforced by the Android framework. An LPA is the software component that handles the actual communication with the eUICC, including profile download, installation, and notification handling.
The relationship between the two is straightforward: EuiccManager calls go through the LPA, which in turn uses EuiccCardManager to issue commands to the eUICC chip. A carrier app using EuiccManager.downloadSubscription() is essentially delegating the profile download to the active LPA, which handles the SM-DP+ interaction and eUICC communication on the app's behalf.
Working with EuiccManager
Getting Started
EuiccManager is obtained through the standard Android system service mechanism:
EuiccManager mgr = (EuiccManager)
context.getSystemService(Context.EUICC_SERVICE);
Before calling any eUICC APIs, you should check whether embedded subscriptions are enabled:
if (!mgr.isEnabled()) {
// eUICC is not available or has been disabled
// (e.g., due to carrier restrictions from a physical SIM)
return;
}
This check is important because even devices with the FEATURE_TELEPHONY_EUICC feature may have embedded subscriptions turned off at runtime.
Checking Carrier Privileges
Carrier privileges are the gatekeeper for EuiccManager operations. They are granted by the mobile carrier through profile metadata, and the eUICC API enforces these rules automatically.
For profile download operations, the carrier app must have privileges that match the target profile. If the profile's BF76 metadata tag is missing or does not match the calling app's signature, the download will be rejected.
Downloading a Profile
The core operation for carrier apps is downloadSubscription(). This method takes a DownloadableSubscription object and a boolean indicating whether to switch to the profile after download:
DownloadableSubscription sub = DownloadableSubscription
.forActivationCode("LPA:1$SMDP_ADDRESS$ACTIVATION_TOKEN");
Intent intent = new Intent(action);
PendingIntent callbackIntent = PendingIntent.getBroadcast(
getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
mgr.downloadSubscription(sub, true /* switchAfterDownload */, callbackIntent);
Because this operation can take seconds or even minutes, it uses a PendingIntent callback rather than a synchronous return. You register a BroadcastReceiver to handle the result:
String action = "download_subscription";
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!action.equals(intent.getAction())) return;
int resultCode = getResultCode();
int detailedCode = intent.getIntExtra(
EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, 0);
// Handle result
}
};
context.registerReceiver(receiver, new IntentFilter(action),
"example.broadcast.permission", null);
Switching Between Profiles
Switching to an already-installed profile follows the same asynchronous pattern:
mgr.switchToSubscription(1 /* subscriptionId */,
callbackIntent);
Starting with Android 13, you can specify a port index when switching, enabling support for Multiple Enabled Profiles (MEP) — the ability to have more than one eSIM profile active simultaneously:
mgr.switchToSubscription(1 /* subscriptionId */,
0 /* portIndex */, callbackIntent);
This is a significant capability for dual-SIM use cases, where a user might want a travel eSIM active alongside their home carrier profile.
Working with EuiccCardManager
Who Can Use It
EuiccCardManager is strictly for LPA applications — system apps that implement the EuiccService and are declared in the device's system image. If your app is not a system app, you cannot use this API, full stop.
The LPA is responsible for far more than just calling EuiccCardManager. It must implement:
An SM-DP+ client to authenticate with and download profiles from the carrier's server
Notification handling to update the server on profile state changes
Optional slot management for switching between eSIM and physical SIM
eSIM OTA update handling
The ES10x Command Set
EuiccCardManager exposes the GSMA RSP v2.0 ES10x functions. The key methods include:
prepareDownload()— prepares the eUICC to receive a profile downloadloadBoundProfilePackage()— loads an encrypted profile package onto the chiprequestAllProfiles()— retrieves the list of all installed profilesswitchToProfile()— activates a specific profile by ICCIDlistNotifications()— retrieves pending notification eventsresetMemory()— clears the eUICC's profile memory
The Usage Pattern
Most EuiccCardManager APIs follow the same asynchronous pattern. You provide an Executor for the callback thread, and a ResultCallback to receive the result:
EuiccCardManager cardMgr = (EuiccCardManager)
context.getSystemService(Context.EUICC_CARD_SERVICE);
ResultCallback<EuiccProfileInfo[]> callback =
new ResultCallback<EuiccProfileInfo[]>() {
@Override
public void onComplete(int resultCode, EuiccProfileInfo[] result) {
if (resultCode == EuiccCardManagerReflector.RESULT_OK) {
// Process profiles
} else {
// Handle error
}
}
};
cardMgr.requestAllProfiles(eid, AsyncTask.THREAD_POOL_EXECUTOR, callback);
Internally, EuiccCardManager binds to the EuiccCardController running in the telephony process via AIDL, and each method receives callbacks through a dedicated AIDL interface.
To load a bound profile package — the encrypted, EID-bound profile that the SM-DP+ generates:
cardMgr.loadBoundProfilePackage(eid, boundProfilePackage,
AsyncTask.THREAD_POOL_EXECUTOR, callback);
And to switch to a specific profile by ICCID:
cardMgr.switchToProfile(eid, iccid, true /* refresh */,
AsyncTask.THREAD_POOL_EXECUTOR, callback);
Handling Errors and Edge Cases
The eUICC APIs provide detailed error codes to help diagnose failures. Starting from Android 11, the result intent includes four keys for granular error handling:
OPERATION_CODE — what operation was attempted (download, delete, switch)
ERROR_CODE — why the operation failed (timeout, carrier locked, etc.)
SMDX_SUBJECT_CODE and SMDX_REASON_CODE — GSMA SGP.22 v2.2 Subject and Reason codes for SM-DP+/SM-DS errors
A typical error handler might look like this:
int operationCode = intent.getIntExtra(
EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_OPERATION_CODE, 0);
int errorCode = intent.getIntExtra(
EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_ERROR_CODE, 0);
String smdxSubjectCode = intent.getStringExtra(
EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_SMDX_SUBJECT_CODE);
String smdxReasonCode = intent.getStringExtra(
EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_SMDX_REASON_CODE);
if (operationCode == OPERATION_DOWNLOAD && errorCode == ERROR_CARRIER_LOCKED) {
// Device is carrier locked
} else if (operationCode == OPERATION_SMDX) {
// SM-DP+ or SM-DS error — inspect smdxSubjectCode/smdxReasonCode
} else if (errorCode == ERROR_TIME_OUT) {
// Network timeout — suggest retry
}
Some errors are resolvable — for example, when a confirmation code is required. In these cases, the callback returns EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR, and the carrier app can call startResolutionActivity() to trigger the LPA UI for user input.
Roami's platform handles the majority of these error conditions server-side before they ever reach the device — activation codes are validated, SM-DP+ addresses are pre-configured, and profile compatibility is checked before the LPA even initiates the download. This reduces the surface area for resolvable errors on the client side.
Compatibility Considerations
Several practical issues affect eSIM API implementations across Android versions and device manufacturers.
Android 13 and switchToSubscription. There was a known issue where compiling an app targeting API level 33 caused switchToSubscription with INVALID_SUBSCRIPTION_ID to fail, even though the same code worked when targeting API level 32. This highlights the importance of testing against multiple target SDK versions.
Multiple LPA instances. If a device has more than one LPA app installed, the system selects the active LPA based on the intent filter priority declared in each app's manifest. The LPA with the highest priority wins. If no LPA is found, eUICC support is disabled entirely.
Device feature detection. Not all Android devices with eSIM hardware expose the same capabilities. Before attempting any eUICC operation, check:
mgr.isEnabled()— embedded subscriptions are enabledPackageManager.hasSystemFeature(FEATURE_TELEPHONY_EUICC)— hardware supports eSIMmgr.getEid()— the eUICC is ready and the app has carrier privileges to access it
Summary: Which API for Which Use Case
| EuiccManager | EuiccCardManager | |
|---|---|---|
| Target caller | Carrier apps | LPA (system apps) |
| Permission model | Carrier privileges from profile metadata | System-level (must be LPA) |
| Abstract level | High — subscription management | Low — ES10x APDU commands |
| Typical operations | Download, switch, delete profiles | prepareDownload, loadBoundProfilePackage |
| Android version | Android 9+ | Android 9+ (system API) |
| Best for | Third-party carrier or travel apps | OEM LPA implementations |
The two APIs are not interchangeable — they serve different callers with different privilege models. For most app developers, EuiccManager is the only accessible path, and even then, carrier privileges are required. For LPA developers, EuiccCardManager is the bridge between the Android telephony framework and the eUICC chip's GSMA-compliant command set.
Understanding this layered architecture is the foundation for any serious Android eSIM integration.
