Smart Card APDU Commands: A Practical Developer Reference

Smart Card Selected APDU Commands

Every conversation with a smart card is a sequence of APDUs (application protocol data units): the host sends a command APDU, the card answers with a response APDU, and the two status bytes at the end of each response tell you what happened. This reference explains the byte layout of both, the four command cases, short and extended length, a selection of the commands developers meet most often, and the status words worth handling. It ends with a small, tested JavaScript module for building and parsing APDUs, cross-checked against Java’s own implementation.

Scope and sources. The command structure used by smart cards is defined in ISO/IEC 7816-4, which is a paid standard and is not quoted here. Everything below is taken from openly published documents that describe the same mechanisms for their own applications: ETSI TS 102 221 (the SIM/UICC interface), NIST SP 800-73-5 (the US PIV card), Oracle’s Java SE and Java Card API documentation and the Android NFC documentation. Treat this page as a practical guide, not as a normative specification; the specification of the application you talk to always has the final word.

Command APDU structure

A command APDU has a mandatory four-byte header and an optional body:

| CLA | INS | P1 | P2 |  Lc  |  Data (Nc bytes)  |  Le  |
|<------ header ----->|<------------ body ------------->|
   1     1     1    1   0,1,3      0..65535          0..3   bytes
Field Size Meaning
CLA 1 byte Class: whether the command is interindustry or proprietary, plus logical channel, secure messaging and command chaining indicators
INS 1 byte Instruction: which command this is (for example A4 = SELECT)
P1, P2 1 byte each Parameters whose meaning depends on the instruction (selection method, file offset, record number, key reference …)
Lc absent, 1 or 3 bytes Length of the command data, called Nc
Data Nc bytes Command data (an AID, a PIN, bytes to write …)
Le absent, 1, 2 or 3 bytes Maximum number of response data bytes expected, called Ne

Keep the two pairs of names apart: Nc and Ne are the actual numbers of bytes, while Lc and Le are the fields that encode them. The encoding is not always the plain number (an Le byte of 00 means 256, for example), which is why APIs such as Java’s CommandAPDU ask you for Ne rather than for an Le byte.

The four command cases

Whether the body carries data, expects data back, both or neither gives four layouts:

Case Layout Command data? Response data? Example
1 CLA INS P1 P2 No No PIV VERIFY with no PIN, asking only for the PIN status
2 CLA INS P1 P2 Le No Yes READ BINARY, GET RESPONSE
3 CLA INS P1 P2 Lc Data Yes No UPDATE BINARY, VERIFY with a PIN
4 CLA INS P1 P2 Lc Data Le Yes Yes SELECT by AID with a response requested

Short and extended length

In the short form, Lc and Le are one byte each. Lc can describe 1 to 255 data bytes, and an Le of 00 means “up to 256 bytes”. When a command needs more than 255 data bytes or more than 256 response bytes, it switches to the extended form: the length fields grow to two bytes, and a 00 byte marks where the extended length starts.

Case Short form Extended form
2 … Le (Le 00 = 256) … 00 Le1 Le2 (Le 0000 = 65536)
3 … Lc Data (Nc 1–255) … 00 Lc1 Lc2 Data (Nc 1–65535)
4 … Lc Data Le … 00 Lc1 Lc2 Data Le1 Le2

Multi-byte length fields are written most significant byte first (big-endian). Two practical limits matter more than the encoding itself:

  • Not every reader or card accepts extended length. Java’s API documentation says so explicitly, and on Android you can ask a contactless card’s connection with IsoDep.isExtendedLengthApduSupported() before relying on it.
  • The limits depend on the platform. Java’s host-side CommandAPDU accepts Nc up to 65535 and Ne up to 65536. A Java Card applet that implements javacardx.apdu.ExtendedLength can receive and send up to 32767 bytes of APDU data. Without that interface the applet only sees short APDUs.

The CLA byte

  • If bit 8 of CLA is 0 (values 0x00–0x7F), the command is interindustry: its meaning is shared across applications. If bit 8 is 1 (0x80 and above), the command is not interindustry, and its meaning comes from the application’s own specification. Payment applications, for example, define several of their commands with CLA 80.
  • FF is not a valid CLA value.
  • The low bits carry the logical channel number (in the first interindustry form, bits 2 and 1 select channel 0 to 3) and the secure-messaging indication.
  • Bit 5 (0x10) marks command chaining: the command is part of a chain and is not the last one. PIV cards use CLA 10 for every chained command except the last, which uses 00. Not every application supports chaining; ETSI TS 102 221 does not.

Response APDU structure

|  Data (0..Ne bytes)  | SW1 | SW2 |

The response is an optional data field followed by two mandatory status bytes, SW1 and SW2, read together as the status word. 90 00 means normal completion. The status bytes are always the last two bytes: strip them before you parse the data (for example as BER-TLV), or the parser will read them as the start of another object.

Selected APDU commands

The instruction bytes below are published in ETSI TS 102 221 and NIST SP 800-73-5. The table is a starting point, not a complete list: each application defines which commands it supports and what their parameters mean.

Command INS Case P1 / P2 What it does
SELECT A4 3 or 4 P1 04 = select by AID (DF name); P2 chooses first/next occurrence and what to return Makes an application or file current; the response usually describes it
READ BINARY B0 2 Offset into the current file (P1 bit 8 = 0), or a short file identifier in P1 (bit 8 = 1) Reads bytes from a transparent file
UPDATE BINARY D6 3 Same coding as READ BINARY Overwrites bytes in a transparent file
READ RECORD B2 2 P1 = record number, P2 = mode Reads one record from a record-structured file
VERIFY 20 3 (or 1) P2 = key/PIN reference Presents a PIN; with no data, PIV returns the PIN status instead
GET RESPONSE C0 2 00 00 Fetches response data the card is holding back (see 61XX below)
GET DATA (PIV) CB 4 3F FF Reads a PIV data object named by a tag list in the data field

A note on GET DATA: PIV uses INS CB, whereas payment cards are seen in published transaction traces using 80 CA with the requested tag in P1–P2. The same INS value can mean different things in different applications (in ETSI TS 102 221, for instance, CB is RETRIEVE DATA), so look the command up in the specification of the application you have selected.

SELECT by AID in practice

The command most programs send first is SELECT by application identifier. For the PIV application it looks like this:

00 A4 04 00 0B A0 00 00 03 08 00 00 10 00 01 00 00
 |  |  |  |  |  |---------- AID (11 bytes) --------| |
CLA INS P1 P2 Lc                                     Le (00 = up to 256)

P2 also selects which match is chosen when several applications start with the same bytes: 00 for the first or only occurrence and 02 for the next one. A card can accept a right-truncated AID (only its first bytes) and select the first application that matches; PIV cards, for example, accept the PIV AID with or without its two version bytes. What the card returns depends on the application: a UICC answers with an FCP template (tag 62), PIV with an application property template (tag 61), and payment applications, in published transaction traces, with an FCI template (tag 6F). Our guides to application discovery and selection and to implementing and troubleshooting application selection go further.

Status words (SW1 SW2)

SW1 SW2 Meaning What to do
90 00 Normal completion Use the data
61 XX Command accepted, XX response bytes are waiting Send GET RESPONSE with Le = XX (repeat if another 61 XX follows)
6C XX Wrong Le; XX is the exact length available Resend the same command with Le = XX
63 CX VERIFY failed, X further tries allowed Stop retrying automatically before the counter reaches zero
67 00 Wrong length Check Lc, Le and the short/extended form
69 82 Security status not satisfied Authenticate or verify the PIN first
69 83 Authentication method (for example the PIN) blocked Needs an unblock procedure defined by the application
69 85 Conditions of use not satisfied The command is not allowed in the current state
6A 80 Incorrect data in the command data field Check the data encoding
6A 81 Function not supported
6A 82 File not found; after SELECT by AID: application not found Try the next candidate AID
6A 83 Record not found Check the record number
6A 86 Incorrect P1–P2 Check the parameters (some cards answer this to an unsupported selection option)
6A 88 Referenced data not found Check the tag or key reference
6B 00 Wrong P1–P2 Offset or parameter out of range
6D 00 Instruction not supported or invalid The selected application does not implement this INS
6E 00 Class not supported Wrong CLA (for example a proprietary class sent to another application)

61 XX and 6C XX are different in kind from the rest. In ETSI TS 102 221 they are procedure bytes of the T=0 transmission protocol, used only for case 2 and case 4 commands, and Java’s CardChannel.transmit() handles both automatically. Whether your code ever sees them depends on the reader stack you use, so handle them, but do not treat them as application errors.

Building and parsing APDUs in JavaScript

The module below builds command APDUs in short or extended form (choosing the short form whenever it is enough), decodes a command back into its fields and case, and splits a response into data and status bytes. It has no dependencies and runs in Node.js and modern browsers.

// apdu.mjs — build and parse smart-card APDUs (short and extended length).
// Ambimat Electronics, 2026. Dependency-free ES module; works in Node.js 18+ and modern browsers.
//
// Encoding rules follow the case 1-4 layouts described in the Java SE javax.smartcardio.CommandAPDU
// documentation: Nc (data length) 0..65535, Ne (expected response length) 0..65536; short form when
// Nc <= 255 and Ne <= 256, otherwise extended form. Le = 00 means 256 (short) and 0000 means 65536 (extended).

export class ApduError extends Error {}

export function fromHex(hex) {
  if (typeof hex !== 'string') throw new ApduError('hex input must be a string');
  const clean = hex.replace(/[\s:]/g, '');
  if (clean.length % 2 !== 0 || /[^0-9a-fA-F]/.test(clean)) throw new ApduError(`invalid hex: "${hex}"`);
  const out = new Uint8Array(clean.length / 2);
  for (let i = 0; i < out.length; i++) out[i] = parseInt(clean.slice(2 * i, 2 * i + 2), 16);
  return out;
}

export function toHex(bytes, sep = ' ') {
  return Array.from(bytes, (b) => b.toString(16).toUpperCase().padStart(2, '0')).join(sep);
}

function byteField(name, v) {
  if (!Number.isInteger(v) || v < 0 || v > 0xff) throw new ApduError(`${name} must be a byte (0..255), got ${v}`);
  return v;
}

// Build a command APDU. data: Uint8Array or hex string (optional). ne: expected response bytes (0 = none).
export function encodeCommand({ cla, ins, p1, p2, data = new Uint8Array(0), ne = 0 }) {
  const body = typeof data === 'string' ? fromHex(data) : data;
  const nc = body.length;
  if (nc > 65535) throw new ApduError(`command data too long: ${nc} bytes (max 65535)`);
  if (!Number.isInteger(ne) || ne < 0 || ne > 65536) throw new ApduError(`ne must be 0..65536, got ${ne}`);
  const header = [byteField('CLA', cla), byteField('INS', ins), byteField('P1', p1), byteField('P2', p2)];
  const extended = nc > 255 || ne > 256;
  const out = [...header];
  if (nc > 0) out.push(...(extended ? [0x00, nc >> 8, nc & 0xff] : [nc]), ...body);
  if (ne > 0) {
    if (!extended) out.push(ne === 256 ? 0x00 : ne);
    else {
      const le = ne === 65536 ? 0 : ne;
      if (nc === 0) out.push(0x00);           // case 2 extended: 00 then two Le bytes
      out.push(le >> 8, le & 0xff);
    }
  }
  return Uint8Array.from(out);
}

// Parse a command APDU back into its fields; identifies the case (1-4) and short/extended form.
export function decodeCommand(input) {
  const a = typeof input === 'string' ? fromHex(input) : input;
  if (a.length < 4) throw new ApduError(`command APDU needs at least 4 header bytes, got ${a.length}`);
  const [cla, ins, p1, p2] = a;
  const r = { cla, ins, p1, p2, data: a.subarray(4, 4), ne: 0, case: 1, extended: false };
  const n = a.length - 4;
  if (n === 0) return r;
  if (n === 1) return { ...r, case: 2, ne: a[4] === 0 ? 256 : a[4] };                       // case 2 short
  if (a[4] !== 0) {                                                                         // short Lc
    const nc = a[4];
    if (n === 1 + nc) return { ...r, case: 3, data: a.subarray(5, 5 + nc) };
    if (n === 2 + nc) return { ...r, case: 4, data: a.subarray(5, 5 + nc), ne: a[5 + nc] === 0 ? 256 : a[5 + nc] };
    throw new ApduError(`length mismatch: Lc=${nc} but ${n - 1} bytes follow`);
  }
  if (n === 3) {                                                                            // case 2 extended
    const le = (a[5] << 8) | a[6];
    return { ...r, case: 2, extended: true, ne: le === 0 ? 65536 : le };
  }
  const nc = (a[5] << 8) | a[6];                                                            // extended Lc
  if (nc === 0) throw new ApduError('extended Lc of 0000 is not valid');
  if (n === 3 + nc) return { ...r, case: 3, extended: true, data: a.subarray(7, 7 + nc) };
  if (n === 5 + nc) {
    const le = (a[7 + nc] << 8) | a[8 + nc];
    return { ...r, case: 4, extended: true, data: a.subarray(7, 7 + nc), ne: le === 0 ? 65536 : le };
  }
  throw new ApduError(`length mismatch: extended Lc=${nc} but ${n - 3} bytes follow`);
}

// Split a response APDU into data and the two status bytes (SW1 SW2), which are always the last two.
export function parseResponse(input) {
  const r = typeof input === 'string' ? fromHex(input) : input;
  if (r.length < 2) throw new ApduError(`response APDU needs at least SW1 SW2, got ${r.length} byte(s)`);
  const sw1 = r[r.length - 2], sw2 = r[r.length - 1];
  return { data: r.subarray(0, r.length - 2), sw1, sw2, sw: (sw1 << 8) | sw2 };
}

Example: the four cases and extended length

// build-commands.mjs — the four APDU cases, then the same command forced into extended length.
import { encodeCommand, decodeCommand, toHex } from '../apdu.mjs';

const aid = 'A000000308000010000100';                       // the PIV application AID (NIST SP 800-73)
const commands = {
  'case 1  VERIFY, no PIN (status) ': { cla: 0x00, ins: 0x20, p1: 0x00, p2: 0x80 },
  'case 2  READ BINARY, Le=256     ': { cla: 0x00, ins: 0xb0, p1: 0x00, p2: 0x00, ne: 256 },
  'case 3  UPDATE BINARY, 2 bytes  ': { cla: 0x00, ins: 0xd6, p1: 0x00, p2: 0x10, data: 'CAFE' },
  'case 4  SELECT by AID, Le=256   ': { cla: 0x00, ins: 0xa4, p1: 0x04, p2: 0x00, data: aid, ne: 256 },
  'case 2E READ BINARY, Le=1000    ': { cla: 0x00, ins: 0xb0, p1: 0x00, p2: 0x00, ne: 1000 },
  'case 4E SELECT by AID, Le=65536 ': { cla: 0x00, ins: 0xa4, p1: 0x04, p2: 0x00, data: aid, ne: 65536 },
};
for (const [name, c] of Object.entries(commands)) {
  const bytes = encodeCommand(c);
  const d = decodeCommand(bytes);
  console.log(`${name} ${toHex(bytes)}   -> case ${d.case}${d.extended ? ' extended' : ''}, Nc=${d.data.length}, Ne=${d.ne}`);
}

Output:

case 1  VERIFY, no PIN (status)  00 20 00 80   -> case 1, Nc=0, Ne=0
case 2  READ BINARY, Le=256      00 B0 00 00 00   -> case 2, Nc=0, Ne=256
case 3  UPDATE BINARY, 2 bytes   00 D6 00 10 02 CA FE   -> case 3, Nc=2, Ne=0
case 4  SELECT by AID, Le=256    00 A4 04 00 0B A0 00 00 03 08 00 00 10 00 01 00 00   -> case 4, Nc=11, Ne=256
case 2E READ BINARY, Le=1000     00 B0 00 00 00 03 E8   -> case 2 extended, Nc=0, Ne=1000
case 4E SELECT by AID, Le=65536  00 A4 04 00 00 00 0B A0 00 00 03 08 00 00 10 00 01 00 00 00   -> case 4 extended, Nc=11, Ne=65536

Example: reading status words

// read-response.mjs — split responses into data + SW1 SW2 and give the status word a first reading.
// Meanings are those published in ETSI TS 102 221 and NIST SP 800-73-5; an application's own
// specification has the final word, and some codes are reused with other meanings.
import { parseResponse, toHex } from '../apdu.mjs';

function describe(sw) {
  const sw1 = sw >> 8, sw2 = sw & 0xff;
  if (sw === 0x9000) return 'normal completion';
  if (sw1 === 0x61) return `more bytes waiting: send GET RESPONSE with Le=${toHex([sw2])} (T=0 transport signal)`;
  if (sw1 === 0x6c) return `wrong Le: resend the same command with Le=${toHex([sw2])} (T=0 transport signal)`;
  if (sw1 === 0x63 && (sw2 & 0xf0) === 0xc0) return `VERIFY failed, ${sw2 & 0x0f} tries left`;
  return {
    0x6700: 'wrong length', 0x6982: 'security status not satisfied', 0x6983: 'authentication method blocked',
    0x6985: 'conditions of use not satisfied', 0x6a80: 'incorrect data in the command data field',
    0x6a81: 'function not supported', 0x6a82: 'file or application not found', 0x6a83: 'record not found',
    0x6a86: 'incorrect P1-P2', 0x6a88: 'referenced data not found', 0x6b00: 'wrong P1-P2',
    0x6d00: 'instruction (INS) not supported', 0x6e00: 'class (CLA) not supported',
  }[sw] ?? 'not in this table: check the specification of the application you selected';
}

for (const hex of ['6F 07 84 05 A0 00 00 00 01 90 00', '61 1C', '6C 10', '63 C2', '6A 82', '6D 00', '91 00']) {
  const r = parseResponse(hex);
  console.log(`${hex.padEnd(34)} data=${r.data.length} bytes  SW=${toHex([r.sw1, r.sw2], '')}  ${describe(r.sw)}`);
}

Output:

6F 07 84 05 A0 00 00 00 01 90 00   data=9 bytes  SW=9000  normal completion
61 1C                              data=0 bytes  SW=611C  more bytes waiting: send GET RESPONSE with Le=1C (T=0 transport signal)
6C 10                              data=0 bytes  SW=6C10  wrong Le: resend the same command with Le=10 (T=0 transport signal)
63 C2                              data=0 bytes  SW=63C2  VERIFY failed, 2 tries left
6A 82                              data=0 bytes  SW=6A82  file or application not found
6D 00                              data=0 bytes  SW=6D00  instruction (INS) not supported
91 00                              data=0 bytes  SW=9100  not in this table: check the specification of the application you selected

Cross-check in Java

Java’s javax.smartcardio builds the same bytes, which is a quick way to check any APDU library. This needs only a JDK, not a reader:

// BuildSelect.java — the same SELECT command built with Java's javax.smartcardio (JDK 11+), no reader needed.
//   java BuildSelect.java
import javax.smartcardio.CommandAPDU;
import java.util.HexFormat;

public class BuildSelect {
    public static void main(String[] args) {
        byte[] aid = HexFormat.of().parseHex("A000000308000010000100");
        CommandAPDU select = new CommandAPDU(0x00, 0xA4, 0x04, 0x00, aid, 256);
        System.out.println(HexFormat.ofDelimiter(" ").withUpperCase().formatHex(select.getBytes()));
        System.out.println("Nc=" + select.getNc() + " Ne=" + select.getNe());
    }
}

It prints 00 A4 04 00 0B A0 00 00 03 08 00 00 10 00 01 00 00 and Nc=11 Ne=256, matching the JavaScript output above.

On the card side: Java Card

If you write the applet that answers these commands, Java Card’s javacard.framework API gives you the same structure from the other end:

API Use
ISO7816.OFFSET_CLA, OFFSET_INS, OFFSET_P1, OFFSET_P2, OFFSET_LC, OFFSET_CDATA Byte positions 0 to 5 in the APDU buffer (OFFSET_EXT_CDATA = 7 for extended length)
APDU.setIncomingAndReceive() Receives the command data of a case 3 or 4 command into the buffer
APDU.setOutgoing(), setOutgoingLength(), sendBytes() or setOutgoingAndSend() Switches to outbound, learns Ne, sets the actual response length and sends the data
ISOException.throwIt(sw) Ends the command with a status word, for example ISO7816.SW_WRONG_LENGTH (0x6700)
javacardx.apdu.ExtendedLength Tagging interface an applet implements to receive and send extended-length APDUs (up to 32767 data bytes)

The APDU buffer is at least 133 bytes long (5 header bytes plus 128 data bytes), so an applet that expects long commands must receive them in pieces or declare extended-length support.

Common mistakes

  • Wrong Lc or Le. Lc must equal the number of data bytes exactly. An Le that is too small or too large can produce 67 00 or 6C XX.
  • Counting hex characters instead of bytes. "A0000000031010" is fourteen characters but seven bytes. Lc is the byte count.
  • Treating SW1 SW2 as data. The status word belongs to the transport, not to the payload. Remove it before parsing TLV data.
  • Byte order in multi-byte fields. Extended Lc and Le are big-endian, and so are most multi-byte values inside smart-card data. See our explanation of big- and little-endian byte order.
  • Assuming 90 00 is the only answer worth handling. 61 XX, 6C XX and 63 CX need specific reactions, and 6A 82 after a SELECT is a normal result when you probe several AIDs.
  • Assuming extended length or chaining is available. Check the reader, the card and the application before relying on either.
  • Reusing an INS value across applications. Instruction meanings depend on the CLA and on the selected application.

How the code was tested

The test suite compares the module against Java’s own javax.smartcardio.CommandAPDU: a small Java program (included with our engineering notes) generated 2,000 commands covering every case and the boundary lengths 0, 1, 255, 256, 257, 65535 and 65536, and the JavaScript output must equal the JDK’s bytes, while the decoder must recover the same Nc and Ne the JDK reports. The suite also checks the four cases byte for byte, the switch to extended form only when needed, response splitting, and rejection of invalid input:

// apdu.test.mjs — node --test apdu.test.mjs   (APDU_IMPL=<path> tests another copy, e.g. code extracted from the live page)
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
const impl = process.env.APDU_IMPL ? new URL(process.env.APDU_IMPL, `file://${process.cwd()}/`).href : './apdu.mjs';
const { encodeCommand, decodeCommand, parseResponse, fromHex, toHex, ApduError } = await import(impl);

const data = (nc, seed) => Uint8Array.from({ length: nc }, (_, i) => (i * 31 + seed) & 0xff);
const vectors = readFileSync(new URL('./conformance/vectors.jsonl', import.meta.url), 'utf8').trim().split('\n').map((l) => JSON.parse(l));

test('encoding matches the JDK javax.smartcardio.CommandAPDU on 2,000 vectors', () => {
  for (const v of vectors) {
    const b = encodeCommand({ cla: v.cla, ins: v.ins, p1: v.p1, p2: v.p2, data: data(v.nc, v.seed), ne: v.ne });
    if (v.hex) assert.equal(toHex(b, ''), v.hex, JSON.stringify(v));
    else {
      assert.equal(b.length, v.len, JSON.stringify(v));
      assert.equal(createHash('sha256').update(b).digest('hex').toUpperCase(), v.sha256, JSON.stringify(v));
    }
    const d = decodeCommand(b);
    assert.deepEqual([d.cla, d.ins, d.p1, d.p2, d.data.length, d.ne], [v.cla, v.ins, v.p1, v.p2, v.jdk_nc, v.jdk_ne]);
    assert.deepEqual(Array.from(d.data), Array.from(data(v.nc, v.seed)));
  }
});

test('the four cases, short form', () => {
  assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xa4, p1: 4, p2: 0 })), '00 A4 04 00');                       // case 1
  assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xb0, p1: 0, p2: 0, ne: 256 })), '00 B0 00 00 00');           // case 2, Le 00 = 256
  assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xd6, p1: 0, p2: 0, data: '0102' })), '00 D6 00 00 02 01 02'); // case 3
  assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xa4, p1: 4, p2: 0, data: 'A000000308000010000100', ne: 256 })),
    '00 A4 04 00 0B A0 00 00 03 08 00 00 10 00 01 00 00');                                                      // case 4
  assert.deepEqual([1, 2, 3, 4].map((c) => decodeCommand(['00A40400', '00B0000000', '00D60000020102', '00A404000BA00000030800001000010000'][c - 1]).case), [1, 2, 3, 4]);
});

test('extended form is used only when needed', () => {
  assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xb0, p1: 0, p2: 0, ne: 257 })), '00 B0 00 00 00 01 01');
  assert.equal(toHex(encodeCommand({ cla: 0, ins: 0xb0, p1: 0, p2: 0, ne: 65536 })), '00 B0 00 00 00 00 00');
  const big = encodeCommand({ cla: 0, ins: 0xd6, p1: 0, p2: 0, data: new Uint8Array(256) });
  assert.equal(toHex(big.subarray(0, 7)), '00 D6 00 00 00 01 00');
  const c4 = encodeCommand({ cla: 0, ins: 0x2a, p1: 0x9e, p2: 0x9a, data: new Uint8Array(10), ne: 300 });
  assert.equal(toHex(c4), '00 2A 9E 9A 00 00 0A ' + '00 '.repeat(10) + '01 2C');
  assert.equal(decodeCommand(c4).extended, true);
});

test('responses: status bytes are always the last two', () => {
  const r = parseResponse('6F 07 84 05 A0 00 00 00 01 90 00');
  assert.equal(r.sw, 0x9000); assert.equal(toHex(r.data), '6F 07 84 05 A0 00 00 00 01');
  assert.deepEqual([parseResponse('6A82').sw1, parseResponse('6A82').sw2, parseResponse('6A82').data.length], [0x6a, 0x82, 0]);
  assert.throws(() => parseResponse('90'), ApduError);
});

test('invalid input is rejected', () => {
  assert.throws(() => encodeCommand({ cla: 256, ins: 0, p1: 0, p2: 0 }), ApduError);
  assert.throws(() => encodeCommand({ cla: 0, ins: 0, p1: 0, p2: 0, ne: 65537 }), ApduError);
  assert.throws(() => encodeCommand({ cla: 0, ins: 0, p1: 0, p2: 0, data: new Uint8Array(65536) }), ApduError);
  assert.throws(() => fromHex('0G'), ApduError);
  assert.throws(() => fromHex('ABC'), ApduError);
  assert.throws(() => decodeCommand('00A404'), ApduError);
  assert.throws(() => decodeCommand('00A40400050102'), ApduError);       // Lc says 5, only 2 follow
  assert.throws(() => decodeCommand('00A4040000000001'), ApduError);     // extended Lc 0000 followed by data
  assert.equal(decodeCommand('00A40400000000').ne, 65536);               // but 00 0000 alone is case 2 extended, Le = 65536
});

Frequently asked questions

What does APDU stand for?

Application protocol data unit: the unit of data exchanged between a card and the device talking to it, at the level of commands and responses rather than of the transmission protocol underneath.

What is the difference between Le and Ne?

Ne is the number of response bytes you expect; Le is the field that encodes it. In short form an Le of 00 encodes Ne = 256; in extended form 00 00 encodes 65536. An absent Le means no response data is expected.

Why does my card answer 6A 82 to SELECT?

The card has no application matching that AID. When you probe a list of AIDs this is the expected answer for every AID the card does not carry.

Is 90 00 the only success code?

It is the normal one. Some applications define additional success or warning codes of their own; check the application’s specification, and handle 61 XX if your reader stack passes it through.

Can I send extended-length APDUs over NFC on Android?

Only if the card and the phone’s NFC stack support them; IsoDep.isExtendedLengthApduSupported() tells you, and IsoDep.getMaxTransceiveLength() gives the largest APDU you can send.

Related reading on Ambimat

Sources

  • ETSI TS 102 221 V18.4.0, Smart Cards; UICC-Terminal interface; Physical and logical characteristics – §10.1–10.2 APDU structure, command cases, coding of CLA, INS and status words; §11.1 SELECT, READ BINARY, UPDATE BINARY, READ RECORD; §7.3.1.1.5 procedure bytes: etsi.org
  • NIST SP 800-73pt2-5, Interfaces for Personal Identity Verification: Part 2 – PIV Card Application Card Command Interface (2024) – SELECT, GET DATA, VERIFY, command chaining, status words: nvlpubs.nist.gov; Part 1 (PIV AID): nvlpubs.nist.gov
  • Oracle, Java SE 21 API, javax.smartcardio: CommandAPDU, ResponseAPDU, CardChannel; and the OpenJDK source of CommandAPDU (extended-length byte layouts)
  • Oracle, Java Card API 3.2: APDU, ISO7816, ISOException, ExtendedLength
  • Android Developers, android.nfc.tech.IsoDep (extended length support, maximum transceive length): developer.android.com
  • S. J. Murdoch, S. Drimer, R. Anderson, M. Bond, Chip and PIN is Broken, IEEE Symposium on Security and Privacy 2010 (published payment-card APDU trace): cl.cam.ac.uk

Written and tested by the Ambimat engineering team. The JavaScript module and examples were run on Node.js 25.8.2; the Java cross-check on OpenJDK 21.