APDU from First Principles: CLA, INS, P1/P2, Le, Lc, and SW1/SW2
If you have ever poked at a smart card with PC/SC, gpshell, or OpenSC, you have seen a stream of hex bytes labelled APDUs. This is what those bytes actually mean, when each form is used, and which mistakes keep biting people in production.
This post is the cornerstone of our APDU cluster. The companion utility tools are linked throughout: paste your hex into the APDU parser as you read.
What an APDU actually is
APDU stands for Application Protocol Data Unit. It is the wire format for messages between a smart-card terminal (the reader) and the application running on the chip (the card). It is defined in ISO/IEC 7816-4, and used by every contact and contactless smart-card application in production: EMV banking cards, FIDO authenticators, enano-card applets, transit cards, SIMs, government eIDs.
Two kinds of APDU exist:
- Command APDU — what the reader sends to the card.
- Response APDU — what the card sends back, ending always with two status bytes (SW1 and SW2).
Command APDU shape
A command APDU is at minimum a 4-byte header:
// Header: 4 bytes
CLA INS P1 P2
Followed by zero, one, or both of:
- Lc + Data — outbound data the reader is sending to the card.
Lcis the length. - Le — the maximum length of response data the reader expects.
Combinations give the four cases:
| Case | Layout | What it means |
|---|---|---|
| Case 1 | CLA INS P1 P2 | No data going either direction. Almost a "ping". |
| Case 2 | CLA INS P1 P2 Le | No data sent. Reader is asking for up to Le bytes back. |
| Case 3 | CLA INS P1 P2 Lc Data | Reader sends Lc bytes; expects only SW1/SW2 back. |
| Case 4 | CLA INS P1 P2 Lc Data Le | Both directions. |
CLA — the class byte
CLA tells the card how to interpret the command and what context it is in (channel, secure messaging, proprietary or inter-industry).
// CLA bit layout for inter-industry commands (b8..b1)
b8 b7 b6 b5 command class
0000 = inter-industry, ISO/IEC 7816-4
10xx = proprietary (GP often uses 0x80)
b4 b3 secure messaging
00 = none
10 = header authenticated
11 = full secure messaging (cmd + body)
b2 b1 logical channel (0..3)
So 0x00 = inter-industry, no secure messaging, channel 0. 0x80 = proprietary (GlobalPlatform style), channel 0. 0x84 = proprietary, header-authenticated SM, channel 0. 0x0C = inter-industry with full SM, channel 0.
INS — the instruction byte
INS is the actual operation code. ISO 7816-4 defines a "global" set; individual specs (PIV, OpenPGP, EMV, FIDO/CTAP1) define their own. Common ones:
| INS | Operation |
|---|---|
0x20 | VERIFY (PIN / CHV) |
0x22 | MANAGE SECURITY ENVIRONMENT |
0x84 | GET CHALLENGE |
0x88 | INTERNAL AUTHENTICATE |
0xA4 | SELECT (file / AID) |
0xB0 | READ BINARY |
0xB2 | READ RECORD |
0xC0 | GET RESPONSE |
0xCA | GET DATA |
0xD6 | UPDATE BINARY |
Convention: even INS values are most common; odd INS often indicates a variant of the same operation (extended addressing, alternative encoding).
P1 and P2 — instruction parameters
P1 and P2 are two opaque bytes whose meaning is defined per-INS. For SELECT (INS=0xA4), P1 selects the addressing mode and P2 controls the response format:
// SELECT P1 = 04 (Select by DF name / AID)
// SELECT P2 = 00 (return FCI; first or only occurrence)
00 A4 04 00 07 A0 00 00 00 03 10 10 00
^^ ^^ ^^ ^^ ^^ ^^^^^^^^^^^^^^^^^^^^^ ^^
CLA INS P1 P2 Lc AID Le
This is a real EMV Visa "select PSE" command. Lc=7 because the AID is 7 bytes; Le=00 means "give me up to 256 bytes back". The card will respond with an FCI template (BER-TLV with tag 6F) — paste a real one into our TLV parser if you want to see it decoded.
Lengths: Lc, Le, and the difference between 0 and 256
Three things to internalise about lengths:
1. Lc is mandatory iff data is present
If you are sending data, you need exactly one Lc byte (short form) or three Lc bytes (extended form, see below). If not, you don’t.
2. Le has a special "all of it" encoding
In short form, Le = 0x00 does not mean "zero bytes". It means "as many as the card will return, up to 256". This trips people up. Le = 0x01 means exactly one byte. There is no way to ask for "exactly zero" with Le present — drop Le entirely (Case 1 or Case 3) for that.
// Le = 0x00 -> ask for up to 256 bytes.
00 A4 04 00 07 A0 00 00 00 03 10 10 00
3. 61XX means "I have data for you, come get it"
If the card has more data than fits, it returns 61XX where XX is the number of bytes still available. The reader then issues GET RESPONSE (00 C0 00 00 XX) to retrieve them. Many older drivers wrap this transparently; many newer ones do not. If you see 6100 ignored in your logs, that is a bug.
Extended-length APDUs: when 256 bytes is not enough
Short-form Lc and Le max out at 255 and 256 respectively. Extended-length APDUs prefix Lc and Le with a 0x00 byte and use 2-byte big-endian values.
| Form | Lc encoding | Le encoding | Max data per direction |
|---|---|---|---|
| Short | 1 byte (1..255) | 1 byte (1..256, 0=256) | 255 / 256 |
| Extended Case 2 | — | 00 LeHi LeLo (1..65536) | 65536 |
| Extended Case 3 | 00 LcHi LcLo (1..65535) | — | 65535 |
| Extended Case 4 | 00 LcHi LcLo | LeHi LeLo | 65535 / 65536 |
Extended-length is negotiated per-application. Many cards advertise support in their ATR historical bytes (the SELECT response FCI sometimes includes extended-length info too). Send extended-length to a card that only supports short-form and you will get back 6700 ("wrong length") at best, or a confused timeout at worst.
Response APDU shape
A response APDU is at minimum two bytes — SW1 SW2. Optional response data precedes them.
// Successful response with data:
[Data ... ] SW1 SW2
// Just status:
SW1 SW2
SW1 SW2 — status words
The two status bytes tell you what happened. The full table is in our APDU Status Dictionary. The 12 you must memorise:
| SW1 SW2 | Meaning |
|---|---|
9000 | OK. Universal success. |
61XX | OK; XX bytes are still available — issue GET RESPONSE. |
6CXX | Wrong Le. The card tells you the right length is XX. Re-send with corrected Le. |
6700 | Wrong length. (Generic; not as helpful as 6CXX.) |
63CX | Verify failed; X retries remaining (PIN). |
6982 | Security status not satisfied. (Translation: log in first.) |
6985 | Conditions of use not satisfied. (FIDO often: user-presence absent.) |
6A82 | File not found. (Wrong AID, or app uninstalled.) |
6A86 | Wrong P1/P2. |
6A87 | Lc inconsistent with P1/P2. |
6D00 | INS not supported. (Wrong applet for this command.) |
6E00 | CLA not supported. (Wrong applet, or wrong channel mode.) |
A real APDU flow
Here is the start of a Visa cardholder verification flow. Every byte matters.
// 1. Select the Payment System Environment (PSE):
>> 00 A4 04 00 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 00
<< 6F 1E 84 0E 32 50 41 59 2E 53 59 53 2E 44 44 46 30 31 A5 0C 88 01 01 5F 2D 02 65 6E 90 00
// FCI in the response (tag 6F) lists EMV applications.
// Decode that with the TLV parser.
// 2. Select the AID we found inside the FCI:
>> 00 A4 04 00 07 A0 00 00 00 03 10 10 00
<< 6F 1E 84 07 A0 00 00 00 03 10 10 A5 13 50 0A 56 49 53 41 20 44 45 42 49 54 87 01 02 9F 38 03 9F 1A 02 90 00
// 3. GET PROCESSING OPTIONS — tell the card the terminal capabilities:
>> 80 A8 00 00 02 83 00 00
<< ...
// 4. READ RECORD ...
A real EMV transaction is hundreds of APDUs. The pattern is the same: SELECT to land on the right applet, then a sequence of operation-specific commands gated by status words.
Six pitfalls we have walked into
- Mixing CLA conventions. A GlobalPlatform tool sends
0x80CLA; an EMV terminal sends0x00. Send0x80to an inter-industry applet and you get6E00. - Forgetting
61XXchaining. Modern PC/SC stacks usually handle this. Embedded Linux drivers and Android NFC stacks often do not. If you have a 600-byte response and you only see 256 bytes, look for the missingGET RESPONSE. - Treating
Le=0x00as zero. It means "give me up to 256 bytes". Many libraries get this wrong; the more careful ones expose a sentinel. - Mixing short and extended length. Within a single APDU it is one or the other. Across APDUs in the same session you can mix only if the card declared support for both.
- Sending extended-length to a card that doesn’t support it. You don’t get a clean reject — sometimes you get
6700, sometimes a timeout, sometimes a confused state. Always check the ATR / SELECT FCI for extended-length capability. - Ignoring secure messaging on a personalised card. Many issuer-personalised cards reject plain APDUs after personalisation. The applet is fine; you forgot to wrap the APDU in SCP02/SCP03 secure messaging.
Companion tools
- APDU Parser — paste any command or response APDU, see it decoded.
- SW1/SW2 Lookup — search status words by hex or by phrase.
- APDU Status Dictionary — the full filterable reference table.
- ISO/IEC 7816 Quick Reference — APDU shape, CLA bits, common INS bytes on one page.
- ATR Parser — for the answer-to-reset bytes that come before any APDU.
- TLV Parser — for the BER-TLV inside SELECT responses.
Related reading
- Technology: JavaCard — what runs on the chip on the other side of the APDU.
- Implementing FIDO2: A Complete Developer Guide — a different protocol, the same APDU plumbing underneath.
- JavaCard Development service — if you are building the applet that responds to these.
What this means for your deployment
If you are integrating with a smart card or a JavaCard applet, treat APDUs as the truth and your higher-level library wrappers as conveniences. When something fails, the diagnostic is always: capture the wire APDU, decode it, decode the response, and look up the status word. The wrapper’s exception class is downstream of all of that.
The good news: APDU is a 30-year-old format, the spec is short, and the rules are stable. Once you have internalised CLA / INS / P1 / P2 / Lc / Le / SW1 / SW2 you can read any smart-card protocol on the wire — EMV, FIDO over CTAP1, PIV, OpenPGP, GP, eSIM, transit. They all use the same envelope.