Open-source project
moov-io/iso8583 avatar
moov-io/iso8583

moov-io/iso8583: marshalling ISO 8583 card messages in Go

A golang implementation to marshal and unmarshal iso8583 message.

535 stars165 forksGoApache-2.0

At a glance

What is it?
moov-io/iso8583 is a Go implementation of the ISO 8583 standard for card-originated financial transaction messages, where a message specification is a set of Go types with struct tags that map fields to bit numbers. It supports the full 0.x release line with a stable and oldstable Go policy.
Who is it for?
Adopt moov-io/iso8583 if you are integrating a card network or an acquirer in Go and want message definitions expressed as ordinary Go types rather than a hand-written codec, since the struct tag approach makes a specification readable and reviewable. Do not adopt it if you need a field your acquirer sends that the library does not know about, without checking the unknown tag handling first.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What ISO 8583 is and where this library sits

ISO8583 implements an ISO 8583 message reader and writer in Go. ISO 8583 is an international standard for card-originated financial transaction messages that defines both message format and communication flow.

The standard is used by major card networks around the globe including Visa, Mastercard and Verve, and it supports card purchases, withdrawals, deposits, refunds, reversals, balance inquiries, inter-account transfers, administrative messages, secure key exchanges and more.

Two properties of the standard shape everything about the library. It defines both message format and communication flow, so a library for it is not merely a codec: there is a message layer and a network layer. And because the format is a fixed field numbering, a message is fundamentally a bag of numbered fields, some of which are themselves bags of numbered subfields. That structure is why the library is built around a specification object and a path-based accessor rather than around a fixed struct.

The library belongs to Moov, whose stated mission is to give developers an easy way to create and integrate bank processing into their own software products, with each open source project focused on solving a single responsibility in financial services and designed around performance, scalability and ease of use. That framing explains the shape of the package: a set of composable pieces rather than an all-in-one acquirer stack.

The primary language is Go, the licence is Apache-2.0, the default branch is master, and the recent releases are v0.26.2 from 2026-10-08, v0.26.1 from 2026-08-13 and v0.26.0 from 2026-04-11. The last push was on 2026-10-20, so the work is current.

The repository layout shows the decomposition. There are separate packages for field/, encoding/, prefix/, padding/, network/, sort/, exp/ for experimental features, errors/ and utils/, alongside the core files message.go, message_spec.go, spec87.go, describe.go, scanner.go, field_filter.go and unknown_tags.go. There is also a cmd/ directory for the CLI, an examples/ directory with field definitions and specs, and a docs/ directory for the project documentation.

The presence of spec87.go and spec87ascii.json tells you that a built-in specification for ISO 8583 version 87 exists in the package, so you are not required to write a specification from nothing to get a working message.

Message specifications as Go types with struct tags

The design decision that makes this library pleasant to use is that a message specification is defined with Go types and struct tags rather than with a table of metadata in some other format.

The README's example defines the fields of an authorization message this way.

go
type Authorization struct {
	MTI                  string               `iso8583:"0"`  // Message Type Indicator
	PrimaryAccountNumber string               `iso8583:"2"`  // PAN
	ProcessingCode       string               `iso8583:"3"`  // Processing code
	Amount               int64                `iso8583:"4"`  // Transaction amount
	STAN                 string               `iso8583:"11"` // System Trace Audit Number
	ExpirationDate       string               `iso8583:"14"` // YYMM
	AcceptorInformation  *AcceptorInformation `iso8583:"43"` // Merchant details
}

The tag carries the bit number, so the mapping between a Go field and a wire position is visible in the type definition itself. A reviewer can read the whole message layout without leaving the file.

Note which field is a pointer. AcceptorInformation is a composite field at bit 43, and the pointer is what marks it as a bag of subfields rather than a leaf value. The subfield struct follows the same convention.

go
type AcceptorInformation struct {
	Name    string `iso8583:"1"`
	City    string `iso8583:"2"`
	Country string `iso8583:"3"`
}

The field numbers are the standard's, so bit 2 is the primary account number and bit 4 is the transaction amount, and the type choice follows the field's meaning. Amount is an int64 rather than a string, and the library handles the encoding of a numeric field into its fixed-width wire representation.

The consequence is that a specification is just Go code. It can be version-controlled, reviewed by people who do not know ISO 8583, and reused directly as the type you unmarshal into. That last point is the practical win: the same struct that describes the message is the struct you want the values in.

The examples/fields/ directory holds the reusable field definitions, and examples/spec.go holds a specification built from them, which is what the Quick Start imports as examples.Spec.

Pack, unpack and get values back out by path

The Quick Start describes three things the library does: define message structure using Go types, pack a message for transmission, and unpack and parse a received message.

Packing starts from a message built on a specification, then the field values are set and the message is marshalled.

go
	// Pack the message
	msg := iso8583.NewMessage(examples.Spec)

	// Set the field values
	err := msg.Marshal(authData)
	if err != nil {
		panic(err)
	}

	// Pack the message
	packed, err := msg.Pack()
	if err != nil {
		panic(err)
	}

Two steps, and they are not the same thing. Marshal writes your struct's values into the message's fields, and Pack serialises the message into bytes. Keeping them separate is what makes it possible to read a field out of a message you did not write the struct for, which is the common case in an acquirer integration where one counterparty sends fields you do not model.

Unpacking is the reverse, and the message is created against the same specification.

go
	// Unpack the message
	msg = iso8583.NewMessage(examples.Spec)
	err = msg.Unpack(packed)
	if err != nil {
		panic(err)
	}

Reading values back is done either into a path or into a struct. The path form addresses a field or subfield by number.

go
	// get individual field values
	var amount int64
	err = msg.UnmarshalPath("4", &amount)
	if err != nil {
		panic(err)
	}

And the dotted form reaches into a composite.

go
	// get value of composite subfield
	var acceptorName string
	err = msg.UnmarshalPath("43.1", &acceptorName)
	if err != nil {
		panic(err)
	}

That dotted path is the API detail worth remembering, because ISO 8583 messages are nested by design and a flat field number is not enough to reach a merchant name inside field 43. UnmarshalPath takes the path as a string, so the same call covers both a top-level field and a subfield.

Unmarshalling the whole message back into the struct works the same way as Marshal did.

go
	// Get the field values into data structure
	authData = &Authorization{}
	err = msg.Unmarshal(authData)
	if err != nil {
		panic(err)
	}

And for inspection rather than processing, iso8583.Describe writes a readable rendering of the message to a writer, which the example sends to os.Stdout. The describe.go file at the repository root is where that lives.

The whole package is organised around this flow, and the network/ directory sits alongside it for the communication-flow half of the standard.

Installing it and the Go version policy

Installation is the standard go get.

bash
go get github.com/moov-io/iso8583

The module path matches the repository, and the package is imported as github.com/moov-io/iso8583 with the examples package at github.com/moov-io/iso8583/examples.

The dependency list is short, and that matters for a library that ends up inside a payment path. The go.mod requires testify for the tests, yerden/go-util, golang.org/x/text, which is what handles the character encoding that ISO 8583 messages need, and gopkg.in/yaml.v3, which is what allows specifications to be written in YAML. There is no cgo, no database driver and no network stack in the dependency tree.

The Go version policy is spelled out under its own heading, with the title Always up-to-date, never left behind. The project supports stable, which points to the current Go version, and oldstable, which points to the previous one, and the setup-go action automatically manages versioning so the project stays aligned with the latest and preceding Go releases. The reasoning given is that they strive to embrace the latest language enhancements while appreciating the need for a certain degree of backward compatibility, since not everyone can update immediately.

The current go.mod declares go 1.26.0 with a toolchain directive of go1.27.2, so the code is written against a recent language version. That is worth checking against your own build environment before you commit to it, because a project pinned to the current Go release is a project that moves with it.

There is a makefile at the root for the development workflow, and the repository also carries a Dockerfile-fuzz directory and a fuzz_test.go file, so the library is fuzzed. There is also a .goreleaser.yml for release automation, a renovate.json for dependency updates, an AGENTS.md and a CHANGELOG.md.

The wider feature set: scanning, JSON, unknown tags and the CLI

The README's table of contents points at a set of capabilities beyond the basic pack and unpack cycle, and the repository layout confirms each one has its own file.

Partial Message Parsing is handled by the MessageScanner, in scanner.go. The value of a scanner in a card integration is straightforward: you receive a stream of bytes that may contain one message, several messages, or a partial message, and you need to pull out complete ones without buffering the entire stream or failing on the fragment.

Inspecting message fields is the Describe function, which writes a human-readable rendering of a message to a writer.

JSON Encoding and Decoding means a message can be serialised to JSON and back, which is what you want when a message has to be logged, put on a queue, or handed to a service that does not speak ISO 8583. The yaml dependency points at the same idea from the other direction, since a specification can be authored in YAML.

Working with Unknown TLV Tags is the feature that shows the most care about real-world traffic, and it has its own file, unknown_tags.go. Card networks extend the standard. A counterparty sends a tag you have never heard of, and a library that errors out on an unrecognised tag will fail on legitimate traffic. Preserving unknown tags so they survive a decode and re-encode cycle is what lets a proxy or a router pass a message through without corrupting it.

There is also field_filter.go, which suggests you can select or exclude sets of fields when reading or writing, useful for logging a subset rather than everything.

The ISO8583 CLI is in cmd/, and the docs/ directory holds the project documentation. The documentation index in the README lists pages for defining message specifications, working with ISO 8583 messages, setting and getting message data, partial message parsing, inspecting fields, JSON encoding and decoding, and unknown TLV tags.

The network/ directory is the other half of the standard. ISO 8583 defines communication flow as well as message format, and a library that also ships a network layer is covering that part rather than leaving you to write the transport.

Where moov-io/iso8583 is the wrong tool

There are three limits, and the first is about the scope of the standard it implements.

The first is that this is a codec and a transport, not a card processing platform. Moov's framing is that each of their open source projects solves a single responsibility in financial services, and this one is the message layer. If you need a scheme to manage the whole payment lifecycle, transaction routing, dispute handling, reconciliation and settlement, those are separate projects and separate operational work, and this package does not reduce them.

The second is that ISO 8583 has variants and the library implements one standard. The built-in specification is for version 87, and a card network's own flavour of the standard differs in field layouts, encodings and proprietary extensions. The library's answer to that is extensibility, through user-defined fields, YAML specifications and the unknown tag handling, and that answer is good but it means the work of mapping your acquirer's dialect is still yours. The tag numbers and field definitions in examples/fields/ are a starting point, not your acquirer's specification.

The third is the Go version floor, which is a live constraint rather than a historical one. The policy of tracking stable and oldstable is generous, but a go.mod declaring go 1.26.0 with a go1.27.2 toolchain means a team on an older Go release has to upgrade before they can build this. In an organisation with a long-lived Go toolchain policy, that conversation happens before the integration does.

There is also a factual note about the project's own claim. The README says the package has been thoroughly tested and trusted in the real world, and that it has proven its reliability and robustness in real-world, high-stakes scenarios. That is a statement by the maintainers, and it is the kind of claim worth taking at face value in this category: a message codec for card networks has a long tail of acquirer-specific quirks, and the fact that the project is used in production is meaningful evidence in a way that a unit test count is not. The README also asks for issues to be raised on GitHub or on the community Slack channel.

The honest alternative depends on the language and the role. In Go itself, the closest comparison is writing the codec by hand for your acquirer's specification, which gives you exact control over every field but means owning the encoding rules, the error cases and the conformance testing for a format where a single byte wrong is a reconciliation problem. For a team that only needs to read and write a narrow set of messages for one counterparty, that is sometimes the smaller job, because the library's generality is what you are paying for.

For a Java shop, the established ISO 8583 libraries cover the same ground with the same trade-offs, and for a Python team the same. The reason to pick this one is the Go idioms, the struct-tag specifications that a Go reviewer can read, and the fact that it comes from an organisation whose other projects cover the rest of the processing stack, so a Go shop already running Moov for other responsibilities has a coherent set to build on.

Editorial conclusion

Adopt moov-io/iso8583 if you are integrating a card network or an acquirer in Go and want message definitions expressed as ordinary Go types rather than a hand-written codec, since the struct tag approach makes a specification readable and reviewable. Do not adopt it if you need a field your acquirer sends that the library does not know about, without checking the unknown tag handling first. Before you start, read the specification reference in docs/ and the field definitions in examples/fields/, and note the last push was on 2026-10-20 with release v0.26.2 from 2026-10-08.

Frequently asked questions

What is moov-io/iso8583 and what standard does it implement?

It is a Go implementation of an ISO 8583 message reader and writer. ISO 8583 is an international standard for card-originated financial transaction messages that defines both message format and communication flow, used by networks including Visa, Mastercard and Verve.

How do I install moov-io/iso8583?

Run go get github.com/moov-io/iso8583. The module requires Go 1.26.0 with a go1.27.2 toolchain directive, and the project supports the stable and oldstable Go releases as tracked by the setup-go action.

How are message specifications defined in moov-io/iso8583?

With ordinary Go types and struct tags that carry the bit number, such as a field tagged iso8583:"4" for the transaction amount. A composite field is represented by a pointer to a nested struct whose own fields carry subfield numbers, so the type definition is the specification.

How do I read a subfield from a message in moov-io/iso8583?

Use UnmarshalPath with a dotted path, so msg.UnmarshalPath("43.1", &acceptorName) reads subfield 1 of composite field 43. UnmarshalPath("4", &amount) reads a top-level field, and Unmarshal fills a whole struct defined by your specification.

What happens when a message contains a field moov-io/iso8583 does not know?

The library has dedicated handling for unknown TLV tags, in unknown_tags.go, so unrecognised tags can be preserved rather than causing a parse failure. This matters in practice because card networks extend the standard and a message may carry tags a local specification does not define.

Official sources

  1. License: Apache-2.0
  2. moov-io/iso8583 on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/moov-io-iso8583.svg)](https://hysenlabs.com/projects/moov-io-iso8583)

Was this useful?