Every card tap you have made this year was almost certainly carried by a message format designed in 1987. When a Stripe or Adyen API returns authorised, somewhere behind it an acquirer has sent an ISO 8583 0100 to a card network, and the issuer has answered with a 0110 carrying a two-character response code in field 39.
Most developers never see these bytes. You start seeing them when you join an acquirer, build an issuer processor, connect a switch, or debug why a reversal never matched its original. Then you need to read hex dumps fluently, and most of the explainers that rank for this topic stop at a table of field names. This guide goes further: the MTI digit by digit, a bitmap decoded by hand, the field notation, the encoding traps, and where ISO 20022 actually stands for cards.
What Is ISO 8583 and Why Do Card Networks Still Use It?
ISO 8583 is the interchange message standard for card-originated transactions: authorisations, financial presentments, reversals, reconciliation and network management. Three editions exist: 1987, 1993 and 2003. The 1987 edition is still the one you meet most often in production. The 2003 edition never gained wide acceptance, and the network specifications (Visa's and Mastercard's own interface documents) are built on the older base with heavy private extensions.
It survives for a boring reason. The format sits in terminals, ATMs, acquirer hosts, network switches and issuer processors, and every one of those would need to change in lockstep to replace it. A message format that is compact, fast to parse and understood by every switch on the planet is hard to dislodge, whatever its ergonomics.
One structural point catches newcomers: an ISO 8583 message carries no routing information of its own. Transport framing (a length prefix, often a TPDU header on terminal links) is added around it, and that framing differs by network and by link.
How Does the ISO 8583 MTI Work?
Every message starts with a four-digit Message Type Indicator. Each digit has a fixed meaning:
| Position | Meaning | Values |
|---|---|---|
| 1 | Version | 0 = 1987, 1 = 1993, 2 = 2003, 8 = national use, 9 = private use |
| 2 | Class | 1 authorisation, 2 financial, 3 file actions, 4 reversal/chargeback, 5 reconciliation, 6 administrative, 7 fee collection, 8 network management |
| 3 | Function | 0 request, 1 request response, 2 advice, 3 advice response, 4 notification, 5 notification acknowledgement |
| 4 | Origin | 0 acquirer, 1 acquirer repeat, 2 issuer, 3 issuer repeat, 4 other |
So 0100 reads as: 1987 edition, authorisation class, request, from the acquirer. 0110 is the response to it. 0101 is the acquirer repeating a request it got no answer to. Once you read MTIs this way you stop memorising them.
The version digit matters more than people expect. A 1993-edition host sends 1100 and 1110 for the same authorisation flow, and its response codes are three digits rather than two. If you are parsing traffic from a new partner, check the first digit before you assume anything else about the field layout.
Common ISO 8583 Message Types: 0100, 0200, 0400 and 0800
| MTI | Name | Where you see it |
|---|---|---|
| 0100 / 0110 | Authorisation request / response | Card-not-present and dual-message POS: approve now, settle later |
| 0120 / 0130 | Authorisation advice / response | Stand-in processing told the issuer after the fact |
| 0200 / 0210 | Financial request / response | Single-message systems: ATM withdrawals, PIN debit |
| 0220 / 0230 | Financial advice / response | Completion after an earlier authorisation, such as a hotel checkout |
| 0400 / 0410 | Reversal request / response | Cancelling an earlier transaction |
| 0420 / 0430 | Reversal advice / response | The usual timeout reversal: "I never got your 0110, unwind it" |
| 0800 / 0810 | Network management request / response | Sign-on, sign-off, echo tests |
| 0820 | Network management advice | Key change notifications |
The split between 01xx and 02xx is the dual-message versus single-message distinction. In a dual-message flow the 0100 reserves funds and clearing happens later in a batch file. In a single-message flow, the 0200 is authorisation and financial posting in one go. If you have ever wondered why a void, a refund and a reversal behave so differently, this is the layer where that difference is born.
The 0420 deserves special attention. An acquirer that sends a 0100 and hears nothing back within its timeout cannot know whether the issuer approved. The safe move is a reversal advice, which tells the issuer to release any hold it may have placed. Advices are store-and-forward: the sender keeps retrying (with the repeat MTI, 0421) until it gets the 0430. Get this wrong and cardholders see phantom holds for days.
How Do You Read an ISO 8583 Bitmap?
After the MTI comes the primary bitmap: 64 bits, one per data element from 1 to 64. A set bit means that field is present in the message. Bit 1 is special: if set, a secondary bitmap follows, covering fields 65 to 128. (A tertiary bitmap for 129 to 192 exists in the standard and is rarely used.)
On the wire the bitmap is either 8 raw binary bytes or 16 hexadecimal characters in ASCII or EBCDIC. Here is a typical card-present 0100 carrying fields 2, 3, 4, 7, 11, 12, 13, 14, 22, 37, 41, 42 and 49. Its primary bitmap is 723C040008C08000. Byte by byte:
| Hex | Binary | Fields present |
|---|---|---|
| 72 | 0111 0010 | 2, 3, 4, 7 |
| 3C | 0011 1100 | 11, 12, 13, 14 |
| 04 | 0000 0100 | 22 |
| 00 | 0000 0000 | none |
| 08 | 0000 1000 | 37 |
| C0 | 1100 0000 | 41, 42 |
| 80 | 1000 0000 | 49 |
| 00 | 0000 0000 | none |
Add any field above 64 (field 90 for a reversal, say) and bit 1 flips on, so the first byte becomes F2 and the bitmap reads F23C040008C08000 followed by the secondary bitmap.
The decode is five lines of Python, and I would write it yourself once before trusting a library:
def fields(bitmap_hex: str) -> list[int]:
bits = bin(int(bitmap_hex, 16))[2:].zfill(len(bitmap_hex) * 4)
return [i + 1 for i, b in enumerate(bits) if b == "1"]
fields("723C040008C08000")
[2, 3, 4, 7, 11, 12, 13, 14, 22, 37, 41, 42, 49]
The bitmap is also why ISO 8583 parsing is strictly sequential. Fields have no tags. The parser walks the bitmap in order and reads each present field using its known format and length. One wrong length definition and every subsequent field is garbage, which is the most common failure I have seen when onboarding a new acquirer spec.
Which ISO 8583 Data Elements Matter Most?
There are 128 data elements in the base standard, and a production integration touches perhaps 25 of them. These are the ones worth knowing by number:
| DE | Format | Content | Practical note |
|---|---|---|---|
| 2 | n..19 | Primary account number | LLVAR; mask it in every log line |
| 3 | n6 | Processing code | First two digits are the transaction type (00 purchase, 01 cash, 20 refund) |
| 4 | n12 | Transaction amount | Minor units, zero-padded, no decimal point |
| 7 | n10 | Transmission date and time | MMDDhhmmss, GMT |
| 11 | n6 | System trace audit number (STAN) | Matching key, together with DE 7 and DE 37 |
| 12 / 13 | n6 / n4 | Local time / local date | hhmmss and MMDD at the terminal |
| 14 | n4 | Expiry date | YYMM, not MMYY as printed on the card |
| 22 | n3 | POS entry mode | 05 chip, 07 contactless EMV, 10 credential on file, 80 fallback, 90 magstripe |
| 37 | an12 | Retrieval reference number | Survives into clearing and disputes |
| 38 | an6 | Authorisation code | Issuer-assigned approval code |
| 39 | an2 | Response code | 00 approved, 05 do not honour, 51 insufficient funds |
| 41 / 42 | ans8 / ans15 | Terminal ID / merchant ID | Fixed length, space-padded |
| 48 | ans...999 | Additional data, private use | Each network packs its own subelements here |
| 49 | n3 | Currency code | ISO 4217 numeric: 826 GBP, 978 EUR, 840 USD |
| 52 | b64 | PIN data | Encrypted PIN block |
| 55 | ans...999 | ICC data | EMV chip data as BER-TLV |
| 90 | n42 | Original data elements | Points a reversal at its original |
| 64 / 128 | b64 | Message authentication code | Last field of the primary or secondary bitmap |
Three of these trip people up repeatedly.
DE 4 has no decimal point.000000001250 is £12.50 for GBP, but 1,250 yen for JPY, because the exponent comes from the currency in DE 49. I covered the exponent table in the ISO 4217 minor units guide; the same rules apply here.
DE 39 changes shape between editions. It is two characters in 1987 and three digits in 1993, where 000 to 099 means approved and 100 to 199 means declined. The PSP-level decline codes in the card decline codes guide are ultimately mapped from this field.
DE 90 is 42 digits of matching data. In the common layout it concatenates the original MTI (4), the original STAN (6), the original transmission date and time (10), and the acquiring and forwarding institution IDs (11 each). If a reversal does not find its original, compare DE 90 against what you logged for the 0100. Nine times out of ten the STAN or DE 7 was regenerated on retry.
ISO 8583 Field Formats: LLVAR, BCD and EBCDIC
Field definitions use a compact notation. n is numeric, a alphabetic, an alphanumeric, ans alphanumeric plus special characters, b binary, and z track data. A number after the type is a fixed length (n6). Dots mean variable length, one dot per length digit: n..19 is an LLVAR with a two-digit length prefix and a maximum of 19, and ans...999 is an LLLVAR.
The standard defines the logical layout, not the byte encoding. That is left to each spec, and it is where most integration time goes:
- ASCII: each digit is one byte. Easy to read in a hex dump, twice the size of BCD.
- BCD (packed): two digits per byte.
n6processing code000000takes 3 bytes. Odd-length fields need a pad nibble, and specs disagree on whether it goes left or right. - EBCDIC: legacy mainframe character encoding, still found on some host links.
- Length prefixes: an LLVAR's length can be ASCII digits, BCD, or a single binary byte, independently of how the field body is encoded.
- Message framing: a two-byte binary length header is common, but not universal. The Go library moov-io/iso8583 ships separate header encoders for 2-byte binary, 4-byte ASCII, 2-byte BCD and Visa's own header format (VMLH), which tells you how much variety exists in the wild.
How Do Visa and Mastercard Customise ISO 8583?
Both networks publish their own interface specifications on top of the base standard, available to licensed members only. The base field numbers mostly hold, but the meaning lives in the private-use fields.
Mastercard calls them data elements ("DE 48") and splits DE 48 into numbered subelements: subelement 42 carries the electronic commerce security level indicator, and subelement 43 carries the UCAF data that proves a 3-D Secure authentication took place. That last one connects straight to the 3DS2 transStatus and ECI values you see at the API layer. Visa calls them "fields" and makes heavy use of fields 60 to 63 and 126 for its own data. Issuer-processor documentation (Galileo and SoFi publish theirs) shows the two numbering schemes side by side, and the mapping is mostly, not entirely, one to one.
The practical consequence: a "generic ISO 8583 parser" gets you the envelope. The value is in the network-specific packager that knows how DE 48 subelements or Visa's field 62 subfields are laid out, and that part is never open source because the specs are confidential.
ISO 8583 vs ISO 20022 for Card Payments in 2026
ISO 20022 does have a card business area. The acquirer-to-issuer messages, known as ATICA, live under the cain prefix, with pairs that mirror ISO 8583 classes almost one for one:
| ISO 8583 | ISO 20022 cain | Purpose |
|---|---|---|
| 0100 / 0110 | cain.001 / cain.002 | Authorisation initiation / response |
| 0200 / 0210 | cain.003 / cain.004 | Financial initiation / response |
| 0400 / 0410 | cain.005 / cain.006 | Reversal initiation / response |
| 05xx | cain.007 / cain.008 | Reconciliation |
| 08xx | cain.009 / cain.010 | Network management |
| 08xx (key change) | cain.011 / cain.012 | Key exchange |
On the terminal-to-acquirer side, the nexo Acquirer Protocol is the ISO 20022-based alternative, and the nexo standards body has submitted dozens of its messages into the ISO 20022 catalogue.
The trade-off is straightforward. ISO 20022 gives you named, typed, nested XML (or JSON) elements, room for rich data such as structured merchant and agent information, and validation from a schema. ISO 8583 gives you a payload that is a fraction of the size, a parser any switch already has, and four decades of operational knowledge. For bank transfers, where the pain.001 and pacs.008 migration was driven by Swift and the central bank systems with hard deadlines, ISO 20022 won. Cards have no equivalent forcing function. The European Cards Stakeholders Group's study for the ECB's payments board found no business case for the ecosystem as a whole to migrate the acquirer-to-issuer domain, and recommended leaving it to the market.
My position: ISO 8583 will still carry the large majority of card authorisations in 2030. The movement you will see is at the edges, where ISO 20022-shaped data gets tunnelled through private-use fields (DE 48, DE 55 and network equivalents) rather than replacing the envelope. Agentic payments are a good test case: the new agent and intent data from the card schemes is far more likely to arrive as extra subelements than as a switch tocain.001. If you are hiring for a card platform, ISO 8583 fluency is not a legacy skill.
ISO 8583 Libraries: jPOS vs moov-io vs pyiso8583
| Library | Language | Licence | Best for |
|---|---|---|---|
| jPOS | Java | AGPL (commercial licence available) | Full switch: packagers, channels, multiplexer, transaction manager. In production since 1998 |
| moov-io/iso8583 | Go | Apache 2.0 | Typed structs mapped by field tags; ASCII, BCD and EBCDIC encoders; TLV composites; a CLI that describes a message with the PAN masked |
| pyiso8583 | Python | MIT | Turning bytes into a dict and back; ideal for test tooling and log analysis |
| j8583 | Java | Open source | Lightweight message building without the jPOS runtime |
The choice is mostly about scope. jPOS is a framework, and the AGPL licence matters if you are building a commercial product you distribute or expose as a network service. moov-io is a codec, not a switch: you bring your own TCP handling and retry logic, which is what most modern Go payment services want anyway. pyiso8583 is the fastest way to answer "what is in this hex dump?" Decoding b'02004000000000000000101234567890' returns the MTI 0200, a primary bitmap with only bit 2 set, and a ten-digit PAN in field 2.
What This Means for Developers Building Card Integrations
1. Get the spec, then build a field table. Field number, format, body encoding, length-prefix encoding, padding. Every parsing bug I have chased traced back to one wrong row in that table.
2. Log the matching keys, not the PAN. Store MTI, STAN (DE 11), DE 7, RRN (DE 37) and the institution IDs for every outbound request. Reversals and advices match on these. Mask DE 2 and never log DE 35, DE 52 or DE 55 raw.
3. Treat timeouts as a reversal trigger. If no 0110 arrives within the network's window, send the 0420 and keep repeating as 0421 until you get a 0430. Do not retry the original 0100 blindly.
4. Write golden-file tests from real captures. Take sanitised hex dumps from the network's certification environment, decode them with an independent tool such as pyiso8583, and assert your codec produces identical bytes in both directions.
5. Check the version digit first. A 1100 partner needs three-digit response codes and a longer DE 22. Handle it explicitly rather than discovering it in certification.
6. Keep private-use fields isolated. Put DE 48 subelement parsing, Visa field 62 and similar behind a network-specific module. That is where change arrives, and you want it contained.
If you work above the PSP API, none of this changes how you call Stripe or Adyen. But it explains why their response codes, reversal timings and matching identifiers behave the way they do, and it is the layer you will meet the day you build closer to the network. More of my notes on card and payment infrastructure are on the Tom Wang homepage.