What Is NFC Card Emulation Mode? Secure Element vs Host Card Emulation (HCE)

Mobile Payments: What is NFC Card Emulation Mode?

In NFC card emulation mode, a phone, watch or other NFC device behaves like a contactless smart card. A payment terminal, transit gate or door reader talks to it with the same commands it would send to a plastic card. The reader powers up its field and polls, the device answers as an ISO/IEC 14443-4 (ISO-DEP) card, and the two then exchange ISO/IEC 7816-4 command and response APDUs. The rest of this article covers the second question developers ask: who inside the phone answers those APDUs? The answer is either a tamper-resistant secure element (an embedded chip or the SIM) or an ordinary app running on the main processor, which is called host card emulation (HCE).

Scope: this is a platform-level explainer based on public documentation from Android, Apple, Microsoft, the NFC Forum, NIST and the GSMA. Payment-network kernels, contactless transaction flows and payment tokenisation formats are defined in EMVCo and card-network specifications. We name those specifications but do not reproduce them. The code example is a learning aid that uses an invented test AID. It is not a payment application.

The three NFC modes: reader/writer vs peer-to-peer vs card emulation

The NFC Forum defines several operating modes. Three of them are the classic modes, and a fourth, newer one handles wireless charging of up to 1 W. The mode decides which side behaves as the reader and which side generates the 13.56 MHz RF field. In ECMA-340, the freely available equivalent of ISO/IEC 18092 (NFCIP-1), the device that starts the exchange is the initiator. In passive mode the initiator generates the field and the other side, the target, answers by load-modulating it. In active mode the two sides take turns generating their own fields.

Reader/writer vs peer-to-peer vs card emulation on Android

Reader/writer mode Peer-to-peer mode Card emulation mode
What the phone acts as The reader: it polls for tags and cards, then reads or writes them One of two equal NFC devices exchanging data A contactless card that an external reader talks to
Who is the reader The phone Neither, in the card sense: one device is the NFC-DEP initiator and the other the target The external terminal, gate, door reader or another phone in reader mode
Who powers the RF field The phone. A passive tag needs no battery of its own The initiator in passive mode; both devices in turn in active mode The external reader. The phone listens and answers within the reader’s field
Protocol the app sees Tag technologies (NDEF, NfcA, IsoDep and others); with IsoDep the app sends its own APDUs NDEF messages over NFC-DEP (ISO/IEC 18092) ISO-DEP (ISO/IEC 14443-4) carrying ISO/IEC 7816-4 APDUs, routed by AID
Android API Tag dispatch (ACTION_NDEF_DISCOVERED and related intents) and NfcAdapter.enableReaderMode() with IsoDep; see NFC basics Android Beam (NfcAdapter.CreateNdefMessageCallback and related APIs), deprecated in Android 10 / API level 29; see Android 10 behaviour changes HostApduService (HCE, app answers) or OffHostApduService (secure element answers); see Host-based card emulation overview
Since IsoDep from API level 10; enableReaderMode() from API level 19 Deprecated since API level 29 HCE from Android 4.4 (API level 19)
Typical use Reading NDEF tags (URLs, contact details), reading a contactless card, acting as the test reader for an HCE app Formerly sharing contacts and links between phones Payments, transit tickets, access badges, car and hotel keys, loyalty cards

Reader mode vs card emulation mode: the practical difference on Android

The two modes are opposite roles. In reader mode your app drives the conversation: it finds a tag, opens IsoDep and calls transceive() with commands it builds itself. In card emulation the external reader drives the conversation, and your HostApduService only replies to the commands it receives. On one phone the modes also exclude each other. Android’s reference for enableReaderMode() says that while the calling activity is in the foreground, the NFC controller acts only as a reader/writer, which disables peer-to-peer and card emulation on that phone. That is why the phone you use as a test reader cannot also emulate a card. For reading another phone’s HCE service, Android recommends the flags FLAG_READER_NFC_A and FLAG_READER_SKIP_NDEF_CHECK.

Peer-to-peer has largely disappeared from phone apps. Android 10 deprecated Android Beam and the related APIs, leaving it only as an option for device makers, and today’s NfcAdapter reference says the feature has been removed. Tag reading and payments were not affected. On iPhone, Core NFC (iOS 11 and later) provides reader sessions, and card emulation needs one of the entitlement programs described below.

What card emulation looks like on the wire

From the reader’s side, an emulating phone is just another ISO-DEP card. Android’s HCE documentation sets out the layering. The phone emulates ISO-DEP (ISO/IEC 14443-4) on top of NFC-A (ISO/IEC 14443-3 Type A), and support for NFC-B is optional. The application data travels in ISO/IEC 7816-4 APDUs. Windows 10 Mobile documented the same model for its HCE apps: ISO-DEP over Type A, with APDUs per ISO/IEC 7816-4.

  Reader / terminal                         Phone in card emulation
  -----------------                         -----------------------
  APDU commands (ISO/IEC 7816-4)   <---->   app or applet answering APDUs
  ISO-DEP      (ISO/IEC 14443-4)   <---->   NFC controller (RATS/ATS, framing)
  NFC-A        (ISO/IEC 14443-3 A) <---->   NFC controller (anticollision, UID)
  13.56 MHz RF field (reader polls) --->    antenna (listens)

Android’s documentation of the activation sequence has some practical consequences for anyone building a reader:

  • The UID is random. During NFC-A anticollision an Android HCE device should be assumed to present a new random UID on every tap, so a reader must not use the UID for identification or authentication.
  • Check one bit, not one byte. The device’s SEL_RES has bit 6 (0x20) set to signal ISO-DEP support. Other bits may also be set, so a reader should test only that bit rather than compare the whole byte with 0x20.
  • The app does not control the ATS. The NFC controller generates the ATS answer to the reader’s RATS. HCE services cannot configure it, but Android requires the parameters to stay within NFC Forum limits (for example, the ATS length must not exceed 20 bytes).
  • Then come APDUs, one at a time. The exchange is half-duplex: the reader sends a command APDU and waits for a response APDU. Android HCE supports a single logical channel. The first command is normally a SELECT by AID.

A SELECT by AID is a short command with class 00, instruction A4, P1 04 (“select by name”) and P2 00, followed by Lc, the AID bytes and an optional Le. NIST’s PIV card interface specification, SP 800-73 Part 2, publishes exactly this layout with the status words 90 00 (successful execution) and 6A 82 (application not found). It is one of the few freely available normative descriptions of the command. Our APDU commands reference covers the other common commands.

Two architectures: secure element or host card emulation

Every card-emulating phone has an NFC controller that terminates the radio protocol. The architectures differ in where the controller sends the APDUs.

Secure-element card emulation

In this model the card application, often called an applet, lives in a secure element. That is a separate tamper-resistant chip, either embedded in the phone (eSE) or built into an NFC-capable SIM (UICC). When the phone is held to a terminal, the controller forwards the reader’s APDUs straight to the secure element, and no Android app takes part in the exchange. The NFC Forum describes this as offering the same security level as a contactless smart card.

On Android, an app that owns an applet in a secure element declares an OffHostApduService. This only tells the system which AIDs live in that secure element so the controller can route them. Android never starts or binds to an off-host service. The android:secureElementName attribute names the target, and the CardEmulation reference points to GSMA TS.26 handset requirements for the naming scheme (SIM1, SIM2… for UICCs and eSE1, eSE2… for embedded elements).

The weakness of the SIM model was commercial rather than technical. Microsoft’s documentation for Windows 10 Mobile said the earlier SIM-based model on Windows Phone 8.1 required secure payment apps to be “tightly coupled with mobile-network operators”, which limited who could build them. That was the reason Microsoft gave for adding HCE.

Host card emulation (HCE)

With HCE the controller forwards APDUs to the host CPU, where an ordinary app answers them. Android has supported this since Android 4.4 (API level 19) through HostApduService. Windows 10 Mobile offered it through the Windows.Devices.SmartCards APIs such as SmartCardEmulator, although Microsoft’s own guide says card emulation was available only on Windows 10 Mobile and not on other editions of Windows 10.

On iPhone, Apple added the HCE API CardSession in iOS 17.4. It is available to entitled apps in the European Economic Area for listed use cases such as payments, transit, badges and keys, and requires an iPhone XS or later. Developers can test outside the EEA on iOS 18.2 or later. A separate program, the NFC & SE Platform (iOS 18.1 and later), stores the credential in the iPhone’s Secure Element and is offered in a published list of territories, including India from iOS 26.0. Both programs require an Apple entitlement, and payment apps must commit to PCI DSS and EMVCo security standards. Core NFC’s reader sessions (iOS 11 and later) are for reading tags, and NFCTagReaderSession cannot select payment-related AIDs.

Aspect Secure element (eSE or UICC) Host card emulation
Who answers the reader An applet in the secure element; no app is involved during the tap An app service on the main processor (Android HostApduService, iOS CardSession)
Where secrets live Inside tamper-resistant hardware In the app’s storage, protected by the OS sandbox; payment designs add limited-use credentials and tokenisation
Getting the card onto the phone Provisioned into the secure element; with a SIM this has historically meant working with the mobile operator Install an app from an app store; the GSMA paper notes that no operator or other third party is needed
Android declaration OffHostApduService with <offhost-apdu-service>; never bound by Android HostApduService with <host-apdu-service>; bound by Android when an AID routes to it
Locked phone (Android) Android’s HCE guide says requireDeviceUnlock does not apply, because the host CPU takes no part in the transaction; Secure NFC (Android 10 and later) still makes off-host emulation unavailable with the screen off requireDeviceUnlock can force an unlock first (see the version table below)

The two models can coexist on one phone, and the GSMA’s 2014 analysis (written with Consult Hyperion) argued they should not be seen as mutually exclusive. It suggested hybrids in which, for example, the SIM covers authentication gaps in HCE.

How the phone decides who answers: AID routing

Coexistence depends on a routing table inside the NFC controller. Each entry pairs an AID with a destination, which is either the host or one of the secure elements. When a SELECT arrives, the controller looks up the AID. If it finds a match, it sends that SELECT and every following APDU to the destination until another SELECT arrives or the field is lost. If there is no match, a default route applies. Android configures the table itself from the installed services and the user’s preferences, so apps never edit it directly. Windows 10 Mobile also documented fallback routes by protocol (for ISO-DEP) and by technology (NFC-A/B/F). Non-ISO-DEP traffic, such as MIFARE, went to the SIM by default.

                         +-------------------------------+
  reader --SELECT AID--> |        NFC controller         |
                         |  routing table:               |
                         |   AID F0414D42490001 -> host  |----> HostApduService (app A)
                         |   AID (wallet AIDs)  -> host  |----> default wallet's service
                         |   AID (transit AID)  -> eSE1  |----> applet in embedded SE
                         |   AID (SIM applet)   -> SIM1  |----> applet in UICC
                         |   no match -> default route   |
                         +-------------------------------+

Android adds three concepts on top of the raw table:

  • AID groups. A service declares its AIDs in groups. For each group, Android routes either all of the AIDs to that service or none of them, never a mixture. Android matches AIDs exactly, not by the partial matching that ISO/IEC 7816-4 also defines. Some devices additionally allow prefix registration, which apps can detect with CardEmulation.supportsAidPrefixRegistration().
  • Categories. Each group is either payment or other. Only one payment AID group can be enabled at a time. Android 15 replaced the old “default payment app” setting with a wallet role that the user assigns. A payment-category service can transact only if it belongs to that role holder or if its app is in the foreground and has called setPreferredService(). Android recommends the other category for closed-loop cards that work at only one merchant.
  • Conflict resolution. When several services register the same AID, Android uses the default wallet’s service if that app registered the AID. Otherwise it uses the only service that registered it, and if more than one did, it asks the user. A foreground activity can override this with setPreferredService().

A session ends when the reader selects an AID that resolves to a different service, or when the link is lost. Android reports both cases to onDeactivated() as DEACTIVATION_DESELECTED or DEACTIVATION_LINK_LOSS. For how payment terminals choose among card applications, see our conceptual guide to EMV application selection and the implementation walkthrough.

Choosing an AID

Readers in established systems look for well-known, registered AIDs. Android recommends registering your own AID under ISO/IEC 7816-5 if you are deploying new readers. The HostApduService reference also allows the proprietary range: AIDs whose first byte has bits 8–5 all set (that is, Fx). It recommends making such an AID at least 6 bytes long to reduce the risk of collisions. Our AID reference list shows what registered AIDs look like.

Locked phones, screen-off taps and Observe Mode

When card emulation is allowed depends on the Android version and the user’s settings. These are the documented rules:

Android version Documented behaviour
9 and lower Screen off means the NFC controller and application processor are off, so HCE does not work. On the lock screen, HCE works unless the service sets requireDeviceUnlock="true". In that case the user is asked to unlock and then tap again.
10 and higher Secure NFC setting: when it is on, every card emulator (host and off-host) is unavailable with the screen off, and HCE cannot run from the lock screen whatever requireDeviceUnlock says. When it is off, off-host services work with the screen off. Apps can check support with isSecureNfcSupported().
12 and higher Apps targeting API 31 or later can set requireDeviceScreenOn="false" to allow screen-off payments.
15 and higher Wallet role and, on devices that support it (isObserveModeSupported()), Observe Mode: a service can tell the controller to only watch reader polling loops, without transacting, and receive matched polling frames in processPollingFrames(). When Observe Mode is in use, the wallet app decides when a transaction may proceed, overriding the unlock and screen-off rules.

On iPhone, the default contactless app can be launched by bringing the phone near a compatible terminal (“field detect”) or by double-clicking the side button. Both require user authentication if the phone is locked. This applies to the HCE program and to the NFC & SE Platform alike.

HCE for payments: limited-use credentials and tokenisation

Taking the secure element out of a payment transaction removes the hardware that protected the long-lived card keys. The GSMA/Consult Hyperion paper from June 2014, published shortly after Android 4.4 introduced HCE, set out the design response that the industry built on:

  • No long-term secrets on the handset. PANs, PINs and keys should not be stored long-term in the handset operating system. Account-level keys stay in a server-side “secure element in the cloud”.
  • Limited-use credentials. The phone holds single-use or limited-use credentials, such as session keys, that are delivered in advance, so a compromise has limited value. The paper notes the usability cost: a phone that has been offline for too long cannot top up its credentials.
  • Tokenisation. A token service provider replaces the card PAN with a surrogate “tokenised PAN” for the device. When a transaction passes through the payment network, the token is mapped back to the PAN so the issuer can process it normally.
  • Device and user risk management. Without a secure element the issuer must judge whether a legitimate device and user are behind each request, using measures such as device fingerprinting, risk-based authentication and detection of rooted devices.

The paper also cautioned that HCE might be less suited to some transaction types, such as offline or high-value payments. The detailed formats, including how a token is structured, how cryptograms are produced and how a contactless kernel uses them, are defined in EMVCo and card-network specifications, which this article does not reproduce. For a related but different idea, see our explainer on token-based authentication. That page covers login tokens rather than payment tokens.

Security trade-offs to design for

  • Only the OS can talk to your HCE service. The BIND_NFC_SERVICE system permission on the service means any APDU you receive came from the NFC controller through the OS, and every response goes back the same way. Android describes this as HCE’s one core built-in protection.
  • Your data at rest is your responsibility. The HCE design deliberately leaves out how your app stores the data it sends. Android points to the application sandbox and its security guidance. For anything valuable, follow the limited-use pattern above rather than storing a long-lived secret.
  • Decide on locking deliberately. Set requireDeviceUnlock for anything that grants access or moves value. Do not rely on the UID for anything, because it is random on every tap.
  • Avoid AID collisions. Use a registered AID, or a proprietary Fx AID of at least 6 bytes. Put each set of AIDs that must stay together in one AID group.
  • Be fast. processCommandApdu() runs on the app’s main thread. Return immediately, or return null and call sendResponseApdu() from another thread. Android suggests keeping a whole exchange to about 1 KB, which can usually be transferred in about 300 ms. If your service cannot complete a transaction, for example because it needs connectivity it does not have, notifyUnhandled() lets the OS offer the user another app.

A minimal Android HCE service

Android needs four pieces: the NFC permission (plus an optional <uses-feature> for android.hardware.nfc.hce, the value of FEATURE_NFC_HOST_CARD_EMULATION), an exported <service> protected by BIND_NFC_SERVICE with the HOST_APDU_SERVICE intent filter, an XML resource that lists AID groups (each with a description, a category and hex AIDs of even length), and a HostApduService subclass.

Our example uses an invented test AID, F0 41 4D 42 49 00 01. The leading F0 puts it in the proprietary range, 41 4D 42 49 is ASCII for “AMBI”, and at 7 bytes it meets the 6-byte recommendation. It is not registered to anyone, so use it only for testing.

Manifest entries

<!-- Inside <manifest>: -->
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc.hce" android:required="true" />

<!-- Inside <application>: -->
<service
    android:name=".AmbiTestHceService"
    android:exported="true"
    android:permission="android.permission.BIND_NFC_SERVICE">
    <intent-filter>
        <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
    </intent-filter>
    <meta-data
        android:name="android.nfc.cardemulation.host_apdu_service"
        android:resource="@xml/apduservice" />
</service>

res/xml/apduservice.xml

<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/hce_service_desc"
    android:requireDeviceUnlock="true">
    <aid-group
        android:description="@string/hce_aid_group_desc"
        android:category="other">
        <aid-filter android:name="F0414D42490001" />
    </aid-group>
</host-apdu-service>

We set requireDeviceUnlock="true" because a test service should not answer from a locked phone. Change it only if you have a reason. The two @string resources are user-facing descriptions that Android may show in settings.

The service and its APDU logic (Kotlin)

The service is a thin wrapper. All decisions are made in a pure function with no Android dependencies, so the function can be compiled and checked on an ordinary JVM and mirrored in other languages.

package com.example.hcetest

import android.nfc.cardemulation.HostApduService
import android.os.Bundle
import android.util.Log

// Minimal HCE service: answers SELECT for the test AID with 90 00, anything else per HceLogic.
// Compiled against android.jar (API 36); NOT tested on a phone or against a reader.
class AmbiTestHceService : HostApduService() {

    // Runs on the main thread: return quickly, or return null and call sendResponseApdu() later.
    override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray =
        HceLogic.respond(commandApdu)

    override fun onDeactivated(reason: Int) {
        val why = if (reason == DEACTIVATION_LINK_LOSS) "link lost" else "another AID selected"
        Log.d("AmbiTestHce", "deactivated: $why")
    }
}
package com.example.hcetest

// Pure APDU logic, no Android imports: compiled and checked on a plain JVM.
// Mirrors respond() in hce-logic.mjs.
object HceLogic {
    // Invented proprietary test AID: F0 = proprietary range, 41 4D 42 49 = "AMBI".
    val TEST_AID = byteArrayOf(0xF0.toByte(), 0x41, 0x4D, 0x42, 0x49, 0x00, 0x01)

    private val OK = byteArrayOf(0x90.toByte(), 0x00)
    private val WRONG_LENGTH = byteArrayOf(0x67, 0x00)
    private val WRONG_P1P2 = byteArrayOf(0x6A, 0x86.toByte())
    private val NOT_FOUND = byteArrayOf(0x6A, 0x82.toByte())
    private val INS_NOT_SUPPORTED = byteArrayOf(0x6D, 0x00)
    private val CLA_NOT_SUPPORTED = byteArrayOf(0x6E, 0x00)

    fun respond(apdu: ByteArray, aid: ByteArray = TEST_AID): ByteArray {
        if (apdu.size < 4) return WRONG_LENGTH.copyOf()
        if (apdu[0] != 0x00.toByte()) return CLA_NOT_SUPPORTED.copyOf()
        if (apdu[1] != 0xA4.toByte()) return INS_NOT_SUPPORTED.copyOf()
        if (apdu[2] != 0x04.toByte()) return WRONG_P1P2.copyOf() // only SELECT by name (AID)
        if (apdu.size < 6) return WRONG_LENGTH.copyOf()
        val lc = apdu[4].toInt() and 0xFF
        val rest = apdu.size - 5 - lc // 0 = no Le, 1 = Le present
        if (lc == 0 || (rest != 0 && rest != 1)) return WRONG_LENGTH.copyOf()
        val selected = apdu.copyOfRange(5, 5 + lc)
        return if (selected.contentEquals(aid)) OK.copyOf() else NOT_FOUND.copyOf()
    }
}

The logic answers 90 00 to a SELECT whose data field is exactly the test AID, with or without a trailing Le, and 6A 82 to a SELECT for any other AID, including a shorter prefix or a longer extension of ours. For malformed or unsupported commands it uses the conventional status words: 67 00 wrong length, 6A 86 incorrect P1/P2, 6D 00 instruction not supported and 6E 00 class not supported. On a real phone Android passes this service a SELECT only when it names our AID, and then forwards every later command until the link drops or another service is selected. The 6A 82 branch is therefore defensive, but the other branches can be reached after a successful SELECT. A real card would also usually return file control information with the 90 00; we return the status word alone to keep the example minimal.

The same logic in JavaScript, with tests

To test the decision logic without hardware, we ported it to a Node.js module and drove both versions from one shared file of test vectors:

# Shared test vectors: <command APDU hex | -> <expected SW hex>  # what it checks
# Read by hce-logic.test.mjs (Node) and HceLogicCheck.kt (JVM). One vector per line.
00A4040007F0414D4249000100   9000  # SELECT our AID, with Le
00A4040007F0414D42490001     9000  # SELECT our AID, no Le
00A4040C07F0414D42490001     9000  # P2=0C (no FCI wanted) is accepted: P2 is not checked
00A4040007F0414D4249000200   6A82  # SELECT a different AID of the same length
00A4040006F0414D42490000     6A82  # Lc=6 + Le: a 6-byte prefix of our AID is NOT a match
00A4040008F0414D424900010100 6A82  # 8-byte AID that starts with our AID is NOT a match
00A404000E325041592E5359532E444446303100 6A82  # SELECT the contactless payment directory: not ours
00A4040007F0414D42490001000000 6700  # Le plus two extra bytes after the AID
00A4040007F0414D4249       6700  # Lc says 7 but only 5 AID bytes arrived
00A4040000                 6700  # Lc=0: nothing to select
00A40400                   6700  # header only
00A4                       6700  # too short to be a command
-                          6700  # empty APDU
00A4000C023F00             6A86  # SELECT by file ID (P1=00): not supported here
00B0000000                 6D00  # READ BINARY: not supported
80CA9F7F00                 6E00  # proprietary class byte
// Minimal HCE command handler, written as a pure function so it can be tested without a phone.
// It mirrors HceLogic.respond() in HceLogic.kt byte for byte.
//
// TEST_AID is an invented proprietary AID (first byte 0xF0 = proprietary range;
// 41 4D 42 49 = ASCII "AMBI"). It is not registered and is for testing only.

export const TEST_AID = Uint8Array.from([0xf0, 0x41, 0x4d, 0x42, 0x49, 0x00, 0x01]);

export const SW = {
  OK: [0x90, 0x00], //                 successful execution
  WRONG_LENGTH: [0x67, 0x00], //       Lc does not match the bytes present
  WRONG_P1P2: [0x6a, 0x86], //         SELECT with a P1 we do not support
  NOT_FOUND: [0x6a, 0x82], //          application not found
  INS_NOT_SUPPORTED: [0x6d, 0x00], //  any instruction other than SELECT
  CLA_NOT_SUPPORTED: [0x6e, 0x00], //  any class byte other than 0x00
};

// Takes one command APDU, returns one response APDU (here: status word only).
export function respond(apdu, aid = TEST_AID) {
  if (apdu.length < 4) return Uint8Array.from(SW.WRONG_LENGTH);
  const [cla, ins, p1] = apdu;
  if (cla !== 0x00) return Uint8Array.from(SW.CLA_NOT_SUPPORTED);
  if (ins !== 0xa4) return Uint8Array.from(SW.INS_NOT_SUPPORTED);
  if (p1 !== 0x04) return Uint8Array.from(SW.WRONG_P1P2); // only "select by name" (AID)
  // Short APDU: CLA INS P1 P2 Lc data[Lc] [Le]. P2 is not checked: one application, one answer.
  if (apdu.length < 6) return Uint8Array.from(SW.WRONG_LENGTH);
  const lc = apdu[4];
  const rest = apdu.length - 5 - lc; // 0 = no Le, 1 = Le present
  if (lc === 0 || (rest !== 0 && rest !== 1)) return Uint8Array.from(SW.WRONG_LENGTH);
  const selected = apdu.subarray(5, 5 + lc);
  const match = selected.length === aid.length && selected.every((b, i) => b === aid[i]);
  return Uint8Array.from(match ? SW.OK : SW.NOT_FOUND);
}

export function fromHex(s) {
  const h = s.replace(/\s+/g, '');
  if (!/^([0-9a-fA-F]{2})*$/.test(h)) throw new Error(`not whole hex bytes: ${s}`);
  return Uint8Array.from(h.match(/../g) ?? [], (x) => parseInt(x, 16));
}
export const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, '0').toUpperCase()).join(' ');

A simulated reader session against the JavaScript version prints:

SELECT 2PAY.SYS.DDF01 (a phone routes this elsewhere; shown for the 6A82 branch)
  C-APDU: 00 A4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00
  R-APDU: 6A 82
SELECT test AID F0414D42490001
  C-APDU: 00 A4 04 00 07 F0 41 4D 42 49 00 01 00
  R-APDU: 90 00
READ BINARY (not implemented)
  C-APDU: 00 B0 00 00 00
  R-APDU: 6D 00

What was tested and what was not. The JavaScript module passes its node --test suite, which covers every vector above plus checks on input handling and the AID rules. The Kotlin HceLogic was compiled with kotlinc 2.3 and run on a JVM against the same 16 vectors, all of which passed. The service class was compiled against the Android API 36 android.jar, and the XML resource was processed by aapt2. We have not run this service on a phone or tapped it against a reader. Treat the Android part as compiled but untested on a device.

Testing with a real reader

  • A second Android phone as the reader. Enable reader mode with FLAG_READER_NFC_A | FLAG_READER_SKIP_NDEF_CHECK, then in the callback call IsoDep.get(tag) and connect(), and send the SELECT with transceive(). setTimeout() controls how long transceive() waits for an answer.
  • A PC/SC contactless reader on a computer. The emulating phone appears as a contactless card, and you send the same SELECT with SCardTransmit on Windows or the equivalent PC/SC call elsewhere. If you want to turn reader output into text for logging, see how to turn smart-card reader output into host-friendly text.
  • What to check: 90 00 for the test AID. onDeactivated() with DEACTIVATION_LINK_LOSS when you lift the phone away. The unlock prompt when the phone is locked. On a phone with Secure NFC on, no response with the screen off. On the reader side, parse any response data as BER-TLV. Our TLV parser and tag reference help with that.

Frequently asked questions

What is NFC card emulation mode?

It is the NFC operating mode in which a device behaves like a contactless smart card. An external reader polls it, it answers as an ISO-DEP (ISO/IEC 14443-4) card, and the application data travels in ISO/IEC 7816-4 APDUs. It is the mode used by phone payments, mobile transit tickets and digital keys.

What is the difference between NFC reader mode and card emulation mode on Android?

In reader mode the phone is the reader. It powers the RF field, finds tags or cards and sends them commands, using NfcAdapter.enableReaderMode() and tag classes such as IsoDep. In card emulation mode the phone is the card. An external reader powers the field and sends commands, and a HostApduService (or an applet in a secure element) answers. While an activity has reader mode enabled, Android disables card emulation and peer-to-peer on that phone.

What is the difference between HCE and a secure element?

Both answer a reader’s APDUs. With a secure element, an applet in a dedicated tamper-resistant chip (embedded, or in the SIM) answers, and no app is involved during the tap. With host card emulation, the NFC controller forwards the APDUs to an app on the main processor. The controller’s AID routing table decides which path each tap takes.

Is host card emulation secure enough for payments?

HCE payments rely on a different security model rather than on tamper-resistant hardware. The phone holds only limited-use credentials and tokens, the long-lived keys stay on the server, and the issuer runs device and risk checks. The GSMA analysis suggests that offline or high-value payments may still suit a secure element better.

Does iPhone support host card emulation?

Only for entitled apps in the European Economic Area, through CardSession (iOS 17.4 and later). In many other regions, including India from iOS 26.0, Apple offers the NFC & SE Platform instead, which keeps the credential in the Secure Element. Without one of these entitlements, an app can use Core NFC only to read tags, not to emulate a card.

Does HCE work when the phone is locked or the screen is off?

It depends on the Android version and the user’s settings. Android 9 and lower never run HCE with the screen off. On Android 10 and later, Secure NFC blocks it with the screen off and from the lock screen. Android 12 adds a screen-off opt-in, and Android 15’s Observe Mode hands the decision to the wallet app. The table above has the details.

Can I use any AID for my HCE app?

For existing readers, you must use the AIDs those readers already select. For your own readers, either register an AID under ISO/IEC 7816-5 or use the proprietary range (first byte Fx) with an AID of at least 6 bytes to reduce the risk of collisions.

What happened to NFC peer-to-peer mode on Android?

Android Beam, Android’s peer-to-peer sharing feature, was deprecated in Android 10 and the related APIs are marked deprecated from API level 29. Reader/writer mode and card emulation were not affected.

Related reading on Ambimat

Sources

  • Android Developers, Host-based card emulation overview, last updated 2026-02-26; sections “Card emulation with a secure element”, “Supported NFC cards and protocols”, “AID groups and categories”, “AID conflict resolution”, “Screen off and lock-screen behavior”, “Coexistence with secure element cards”, “HCE and security”, “Protocol parameters and details”. developer.android.com
  • Android Developers, HostApduService API reference, last updated 2026-09-16; class overview (proprietary AID range), processCommandApdu, onDeactivated, notifyUnhandled. developer.android.com
  • Android Developers, OffHostApduService API reference, last updated 2026-09-16. developer.android.com
  • Android Developers, CardEmulation API reference, last updated 2026-08-03; supportsAidPrefixRegistration, getRouteDestinationForPreferredPaymentService (GSMA TS.26 secure element names). developer.android.com
  • Android Developers, NfcAdapter API reference, last updated 2026-08-03; enableReaderMode, deprecated Beam interfaces. developer.android.com
  • Android Developers, IsoDep API reference, last updated 2026-08-03. developer.android.com
  • Android Developers, NFC basics (tag dispatch system, NDEF), last updated 2026-07-30. developer.android.com
  • Android Developers, PackageManager API reference, FEATURE_NFC_HOST_CARD_EMULATION. developer.android.com
  • Android Developers, Behavior changes: all apps (Android 10), “Android Beam deprecation”, last updated 2026-10-01. developer.android.com
  • Ecma International, ECMA-340: Near Field Communication – Interface and Protocol (NFCIP-1), 3rd edition, June 2013 (aligned with ISO/IEC 18092:2013), Sec. 4 (definitions) and Sec. 7 (active and passive communication modes). ecma-international.org (PDF)
  • NFC Forum, Technical Overview, “Operating Modes” (public page, retrieved 2026-10-10). nfc-forum.org
  • Apple Developer, HCE-based contactless NFC transactions for apps in the European Economic Area (EEA), retrieved 2026-10-10; “Requirements and availability”, “Testing requirements”. developer.apple.com
  • Apple Developer, NFC & SE Platform for secure contactless transactions, retrieved 2026-10-10; “Requirements and availability”. developer.apple.com
  • Apple Developer Documentation, Core NFC (iOS 11.0+), CardSession (iOS 17.4+) and NFCTagReaderSession. developer.apple.com
  • Microsoft Learn, Create an NFC Smart Card app (Windows 10 Mobile), article dated 2022-10-20, Internet Archive capture of 2023-02-15; sections “App selection and AID routing”, “Coexistence with SIM based NFC applications”. web.archive.org
  • Microsoft Learn, Windows.Devices.SmartCards Namespace and SmartCardEmulator Class. learn.microsoft.com
  • Microsoft Learn, SCardTransmit function (winscard.h). learn.microsoft.com
  • Consult Hyperion with the GSMA, HCE and SIM Secure Element: It’s not black and white, June 2014, pp. 3, 7–9, 11. GSMA (PDF); press release, 11 June 2014. gsma.com
  • NIST, SP 800-73pt2-5: Interfaces for Personal Identity Verification, Part 2 – PIV Card Application Card Command Interface, July 2024, Sec. 3.1.1 (SELECT) and status-word tables. doi.org
  • OpenSC project, src/libopensc/iso7816.c, ISO 7816 status-word table (commit 173c646, 2026-09-16). github.com

Written by the Ambimat engineering team. The APDU logic was tested on Node.js 25.8.2 and, in Kotlin, on a JVM; the Android service was compiled but not tested on a device.