I moved into fintech in March 2025. For the first few months I was looking at a pile of acronyms, private specs, and message dumps that I did not understand well. After about a year, the picture started to make sense.
It covers three things:
- What ISO 8583 is and where it shows up in card payments.
- Why it is harder in practice than the public standard makes it look.
- Two Go projects I built around that work:
iso8583toolandtornago.
The cast of characters in a card payment
Before talking about the wire format, it helps to name the people and systems involved.
| Actor | Role |
|---|---|
| Cardholder | The person using a credit card |
| Merchant | The store or ecommerce site |
| Acquirer | The company that handles payments for the merchant side |
| Brand | Visa, Mastercard, JCB, and so on |
| Issuer | The company that issued the card |
When a cardholder pays a merchant, the merchant does not talk to the issuer directly. The request usually goes through the acquirer and the card brand before it reaches the issuer. The issuer then decides whether to approve or decline the transaction, and that answer flows back the same way.
That alone explains why payment systems feel different from ordinary CRUD apps. Even a simple purchase depends on multiple companies, multiple protocols, and a lot of assumptions that are not visible in the UI.
Authorization first, clearing later
Card payments are not a single event.
In day-to-day card processing, it is common to separate:
- Authorization: "Can this card spend this amount right now?"
- Clearing: "The transaction is final. Please settle it."
In Visa terms, people often call these BASE I and BASE II.
When you click "Buy" on an ecommerce site, the real-time part is authorization. That is where the issuer checks things like:
- Is the card still valid?
- Is the amount within the limit?
- Does this transaction look suspicious?
The message format that shows up over and over in that world is ISO 8583.
What ISO 8583 actually is
At a high level, ISO 8583 is an international standard for card-originated financial messages. In practice, it is everywhere around authorization traffic, network management, reversals, and similar payment flows.
The basic structure is simple enough:
-
MTItells you what kind of message this is. -
Bitmaptells you which fields are present. -
Data elementshold the actual payload.
That sounds manageable until you look at a real message.
ISO 8583 mixes several encoding styles in the same message:
- ASCII
- BCD
- binary
It also uses fixed-length and variable-length fields, and variable-length fields have their own length prefixes. Then each brand and each network adds private extensions on top.
So the public story is "MTI + bitmap + fields." The real story is "MTI + bitmap + fields + encoding rules + layout rules + private agreements."
A quick example
The first field to learn is the MTI, the Message Type Indicator. It is a 4-digit code at the front of the message.
For example:
-
0100: authorization request -
0110: authorization response -
0200: financial request -
0210: financial response -
0800: network management request
That part is neat. The bitmap is also conceptually neat: it is just a bitset that says which fields exist.
Then the trouble starts.
A field may be:
- fixed-length or variable-length
- plain ASCII or packed BCD
- a simple value or a nested TLV structure
Field 55 is the classic example. In many card payment flows it carries EMV data, which usually means BER-TLV inside an ISO 8583 field inside a bigger payment flow. If you guessed the wrong encoding or the wrong field model, nothing lines up anymore.
That is why raw ISO 8583 captures are hard to read at first. The bytes are not random, but they are not friendly either.
Why ISO 8583 is harder than the standard suggests
The hardest part is not memorizing field numbers. The hard part is that the public standard is only the floor.
In real systems, you also run into:
- brand-specific extensions that are not public
- different interpretations of the same field depending on partner setup
- messages that are technically parseable but operationally wrong
- systems that send malformed messages and still expect interoperability
You can read the spec, ask questions, and write unit tests, and still get surprised in integration.
That gap between "the standard says X" and "the network actually does Y" is what pushed me to build tooling instead of trying to inspect everything by eye.
I am not one of those engineers who can stare at a hex dump and casually spot field boundaries. I wrote software because I wanted the computer to do that part for me.
iso8583tool: the tool I wanted on day one
iso8583tool is a CLI for debugging and inspecting ISO 8583 messages. Under the hood it uses moov-io/iso8583, which was the right call. I did not want to spend time writing my own parser from scratch.
It started as a parser CLI, but the use case that mattered to me was more operational than academic: unpack a capture, compare a request and response, guess which spec a partner message is using, check what would go on the wire, and share the result without leaking cardholder data.
As of v0.7.0, the command set is:
-
view: unpack a message and read it as a human -
diff: compare two messages field by field -
redact: mask cardholder data before sharing a capture -
convert: move between packed messages and JSON documents -
send: send one message to a TCP endpoint and decode the response -
validate: check whether a message unpacks cleanly and whether obvious issues exist -
doctor: guess which built-in spec preset fits a capture -
specs: list the built-in spec presets -
sample: print or export built-in sample messages -
version: print the tool version
Where it helps in real work
The core job is still simple: take an ugly payment message and turn it into something a human can read.
In practice, I use it for troubleshooting. It is usually somewhere in the middle of a fault investigation: checking a production-like capture, diffing two messages, confirming transport framing, or narrowing down whether the problem is the message itself or my spec.
For example, view gives you a summary plus decoded fields, while masking cardholder data by default:
Summary: 0110 | Approved | JPY 5000 | STAN 123456 | TERMID01
F2 Primary Account Number........: 411111******1111
F39 Response Code.................: 00 -> Approved
F49 Transaction Currency Code.....: 392 -> JPY (Japanese yen)
55.8A Authorisation Response Code..: 3030 -> Approved
That safe-by-default behavior matters. In payment systems, a debugging tool that leaks PANs into logs is not very useful.
iso8583tool now masks PAN, track data, PIN-related data, and sensitive EMV tags across view, diff, redact, and send. The exception is convert, which intentionally emits unmasked output so the document can round-trip back into a valid packed message. The README and changelog now call that out explicitly, which was the right fix.
The newer features that changed the tool for me
The biggest upgrades were not cosmetic. They made the tool useful in actual troubleshooting.
doctor
ISO 8583 does not have one universal wire encoding. A capture might be ASCII, packed BCD, or some mixed layout. doctor tries the built-in presets and tells you which one is the best fit. That helps when you are looking at a partner capture and are not even sure which ruleset you should start with.
specs
The built-in presets are now discoverable instead of tribal knowledge. Right now the defaults include:
basei-starterspec87asciispec87bcd-starter
send
This was a big addition. It lets me send one message to a TCP endpoint, decode the response, and inspect both sides. It is intentionally narrow: not a switch, not a simulator, just a focused probe for transport checks, E2E testing, and fault isolation.
Recent releases added:
-
--dry-runto confirm how a message will be packed and framed before opening a connection -
--expect-mtiand--expect-fieldso shell-based CI can assert on the decoded response without piping everything throughjq - clearer behavior around masked output and framed raw bytes
validate --strict
This is not network certification, but it catches plenty of bad assumptions early. It knows about message-class-aware checks and points you toward doctor when a message simply does not unpack under the current spec.
Taken together, that makes the tool more useful as operational tooling than as a standalone parser. I built it because I wanted something safe enough for routine investigation and narrow enough that I could trust what it was doing.
Where it still cannot save you
iso8583tool is not enough on its own. The biggest limitation is still private field definitions.
If a brand, switch, or partner uses private extensions, someone still has to model those fields. In iso8583tool, that means providing the right spec or overlay so the tool knows whether a private field should be treated as:
- opaque text
- TLV
- positional subfields
- nested bitmap
That is not a tooling flaw so much as a fact of the domain. If the real field layout is covered by NDA, an open source tool cannot ship it for you.
What the tool can do is make that last mile much smaller. You can start from a working base preset, add overlays, and iterate instead of beginning with an empty parser.
PCI DSS is why masking is non-negotiable
One thing that becomes obvious in payment work: card data is operationally expensive.
Under PCI DSS, data like the PAN is not something you casually dump into a database or paste into a ticket. Sensitive authentication data is even stricter. Once you spend time around issuer or payment environments, you stop seeing card numbers as "just strings."
That mindset shaped iso8583tool.
I wanted the defaults to be safe enough that I could use the tool in routine debugging without immediately worrying about whether I had just copied raw cardholder data into the wrong place.
Why this led me to Tor and dark-web tooling
The other side of card work is fraud.
Leaked card data sometimes shows up in dark-web marketplaces or forums. From an issuer or anti-fraud perspective, that creates a practical question: can we monitor places where leaked PANs may appear?
That was the motivation behind tornago.
tornago: a thin Go wrapper around Tor
tornago is a lightweight wrapper around the Tor command-line tool. It has three core jobs:
- start and manage Tor daemons
- route HTTP and TCP traffic through Tor
- create and manage onion services through the ControlPort
The project has grown since the first version. Today it covers the basics I wanted: zero external Go dependencies, cross-platform support, client and hidden-service work, and a few operational niceties like retries, metrics, rate limiting, and even slow relay avoidance.
I still kept tornago intentionally thin. In anti-fraud work there is a real reason to reach .onion services, but Tor also has obvious abuse potential. I did not want to build a library whose main selling point was making abuse more convenient. So tornago stays close to the Tor CLI instead of turning into a full crawling framework.
A note about .onion sites
There is nothing inherently illegal about the dark web in the narrow technical sense. A .onion service is just a service reachable inside the Tor network. But the same properties that make Tor useful for privacy also make it attractive for criminal use, and that matters if you are thinking about fraud monitoring.
I considered building a more complete monitoring tool on top of tornago, but stopped there. Once you start talking about persistent crawling, indexing, and diffing content that may contain illegal or highly sensitive data, the compliance and operational risks go up fast.
So I kept the reusable part: the Tor library.
Final thoughts
That is a small part of what I learned in a little over a year.
When I first moved into fintech, I felt there was not much I could realistically turn into open source. Once I understood the domain better, ideas started to appear.
Links:





Top comments (0)