npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tikpay/iso8583-ts

v0.4.0

Published

ISO 8583 parser written in TypeScript with LLVAR/LLLVAR enforcement, bitmap handling and built-in field level BCD/ASCII encoding.

Readme


A fast, type-safe ISO 8583 encoder/decoder for TypeScript. You declare a per-field spec (encoding, length rules, masking); the library packs a { mti, fields } object into a Buffer and unpacks it back, with field-level BCD/ASCII/LLVAR behavior configured exactly.

  • Type-safe spec — one MessageSpec describes every data element
  • Field-level encoding — BCD or ASCII numerics, LLVAR/LLLVAR with configurable length headers and count modes
  • Bitmap handling — primary + automatic secondary (DE 65–128)
  • PCI-friendlyexplain() masks cardholder data by spec
  • Dual build — ESM + CJS, ships .d.ts

Scope: this library encodes/decodes the ISO 8583 message body only. It does not add the 2-byte TCP length prefix that many acquirer links require — frame that yourself around pack() / before unpack(). See Not handled.


🚀 Install

npm i @tikpay/iso8583-ts

Requires Node ≥ 18.18.


⚡ Quick start

A complete, runnable example — define a spec once, then pack and unpack:

import {
  Iso8583,
  type MessageSpec,
  N,
  AN,
  LLVARn,
  LLVARans,
  bitmap,
  NumericEncoding,
  VarLenHeaderEncoding,
  VarPayloadEncoding,
  VarLenCountMode,
} from '@tikpay/iso8583-ts'

const spec: MessageSpec = {
  0: { name: 'MTI', format: N(4, { encoding: NumericEncoding.BCD }) }, // optional, see "MTI & bitmap"
  1: { name: 'Bitmap', format: bitmap(8) }, // optional, 8 bytes = DE 1–64
  2: {
    name: 'PAN',
    format: LLVARn({
      length: 19,
      lenHeader: VarLenHeaderEncoding.BCD,
      payload: VarPayloadEncoding.BCD_DIGITS,
      lenCountMode: VarLenCountMode.DIGITS,
    }),
    mask: 'pan', // explain() shows first 6 / last 4 only
  },
  3: { name: 'Processing Code', format: N(6) },
  4: { name: 'Amount', format: N(12) },
  11: { name: 'STAN', format: N(6) },
  22: { name: 'POS Data', format: AN(12) },
  35: { name: 'Track 2', format: LLVARans({ length: 37 }), mask: 'redact' },
  49: { name: 'Currency', format: N(3) },
}

const iso = new Iso8583(spec)

// ---- Pack: object -> Buffer ----
const { bytes } = iso.pack('0200', {
  3: '000000',
  4: '000000001000',
  11: '123456',
  22: '9010A0B00000',
  49: '978',
})

// ---- Unpack: Buffer -> object ----
const decoded = iso.unpack(bytes)
decoded.mti // '0200'
decoded.fields // { 3: '000000', 4: '000000001000', 11: '123456', 22: '9010A0B00000', 49: '978' }
decoded.bytesRead // number of bytes consumed

🧠 Core concepts

  • MessageSpecRecord<number, FieldSpec> keyed by data-element (DE) number. FieldSpec = { name: string; format: FormatObject; mask?: 'pan' | 'redact' }.
  • DE 0 and DE 1 are optional config slots. A Numeric format at DE 0 sets the MTI encoding (BCD = 2 bytes, ASCII = 4 bytes); omit it and the MTI defaults to BCD. A bitmap(8 | 16) at DE 1 sets the bitmap size: 8 = 64-bit (DE 1–64 only), 16 = 128-bit (allows DE 65–128); omit it and the message is 64-bit. They are not emitted as normal fields.
  • Format helpers (N, AN, LLVARn, …) build the format value. Never construct format objects by hand — use the helpers; they validate lengths.
  • pack(mti, fields) encodes only DE 2–128 present in fields. The bitmap is computed automatically; the secondary bitmap is added automatically when any DE > 64 is present.
  • unpack(buf) returns { mti, fields, bytesRead }. Field values come back as string (numeric/alpha), Buffer (binary), per the field's format.

🧩 Format helpers

Pass the return value as a field's format. Each helper validates its length and throws on bad input.

| Helper | ISO type | Signature | Notes | | ----------------------------------------------------- | -------- | --------------------------------------------------------- | ----------------------------------------------------------------------- | | N(len, opts?) | n | (length: number, opts?: { encoding?: NumericEncoding }) | Fixed numeric. BCD (default) packs 2 digits/byte; ASCII = 1 byte/digit. | | A(len) | a | (length: number) | Fixed alphabetic, space-padded. | | AN(len) | an | (length: number) | Fixed alphanumeric, space-padded. | | ANS(len) | ans | (length: number) | Fixed alphanumeric + printable specials. | | B(lenBytes) | b | (lengthBytes: number) | Fixed binary. Value is a Buffer or hex string. | | bitmap(8 \| 16) | — | (lengthBytes: 8 \| 16) | DE 1 only. 8 ⇒ 64-bit, 16 ⇒ 128-bit. | | LLVAR(opts) | — | variable, 2-digit length header | Generic; choose payload explicitly. | | LLVARn(opts) | n.. | variable, max length 99 | Numeric var. | | LLVARan(opts) | an.. | variable, max length 99 | Alphanumeric var. | | LLVARans(opts) | ans.. | variable, max length 99 | Alphanumeric+special var. | | LLVARb(opts) | b.. | variable, max length 99 | Binary var. Value is a Buffer or even-length hex string. | | LLLVAR(opts) / LLLVARn / LLLVARan / LLLVARans / LLLVARb | — | variable, max length 999 | 3-digit length header. Same option semantics as the LL variants. |

opts for every LL*/LLL* helper is a VarBase (omitted options fall back to the defaults in Encoding options):

type VarBase = {
  length: number // max LOGICAL length (digits for *n, bytes otherwise)
  payload?: VarPayloadEncoding // how the value bytes are encoded
  lenHeader?: VarLenHeaderEncoding // how the length prefix is encoded (default BCD)
  lenCountMode?: VarLenCountMode // does the length count digits or bytes?
}

🔤 Encoding options

These enums control the wire format. Defaults are chosen to match common acquirer specs, but set them explicitly when integrating — the right values come from your acquirer's manual.

| Enum | Values | Meaning | | ---------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | NumericEncoding | BCD ('bcd'), ASCII ('ascii') | Numeric packing. BCD = packed 2 digits/byte; ASCII = one char/byte. | | VarLenHeaderEncoding | BCD, ASCII | How the LL/LLL length prefix is encoded. BCD LL = 1 byte, LLL = 2 bytes; ASCII LL = 2 bytes, LLL = 3 bytes. | | VarPayloadEncoding | BCD_DIGITS ('bcd-digits'), ASCII, BINARY ('binary') | How the field value is encoded. | | VarLenCountMode | DIGITS ('digits'), BYTES ('bytes') | Whether the length header counts digits or bytes. |

Variable-field defaults (applied when you omit an option):

  • lenHeaderBCD always.
  • For LLVARn / LLLVARn (numeric): payloadBCD_DIGITS, lenCountModeDIGITS.
  • For LLVARb / LLLVARb (binary): payloadBINARY, lenCountModeBYTES.
  • For all other var helpers: payloadASCII, lenCountModeBYTES.
  • lenCountMode: DIGITS is only valid on numeric (*n) helpers — it throws otherwise.

📖 Recipes

Copy-paste field definitions for common data elements:

// PAN (DE 2): BCD digits, BCD length header counting digits, max 19. Mask in explain().
2: {
  name: 'PAN',
  format: LLVARn({ length: 19, lenHeader: VarLenHeaderEncoding.BCD,
    payload: VarPayloadEncoding.BCD_DIGITS, lenCountMode: VarLenCountMode.DIGITS }),
  mask: 'pan',
},

// Amount (DE 4): fixed 12-digit BCD numeric.
4: { name: 'Amount', format: N(12) },

// MTI as ASCII instead of BCD (DE 0 config slot):
0: { name: 'MTI', format: N(4, { encoding: NumericEncoding.ASCII }) },

// Track 2 (DE 35): ANS variable, max 37, redacted in explain().
35: { name: 'Track 2', format: LLVARans({ length: 37 }), mask: 'redact' },

// Fixed binary field (DE 52 PIN block): exactly 8 raw bytes. Pass a Buffer or hex string when packing.
52: { name: 'PIN', format: B(8), mask: 'redact' },

// Binary variable field (e.g. DE 55 EMV data): up to 255 raw bytes, BCD length header.
55: { name: 'ICC Data', format: LLLVARb({ length: 255 }) },

// 128-bit message (needed when any DE > 64 is used):
1: { name: 'Bitmap', format: bitmap(16) },

// ASCII-numeric variable field with ASCII length header:
48: { name: 'Private', format: LLLVARans({ length: 999, lenHeader: VarLenHeaderEncoding.ASCII }) },

Packing values:

iso.pack('0200', {
  2: '4761739001010119', // string of digits for numeric fields
  4: '000000001000',
  52: Buffer.from('0123456789ABCDEF', 'hex'), // Buffer or hex string for binary fields
})

🕵️ explain() and masking

explain() renders a readable, line-per-field dump. Fields whose spec sets mask are redacted by default, so PANs, tracks and PIN blocks never land in logs (PCI-DSS). Masking follows your spec, not ISO 8583 DE numbers, so it works no matter how an acquirer lays out its fields.

iso.explain(bytes)
// MTI: 0200
// 002 PAN (LLVARn, len=19): 476173******0119
// 035 Track 2 (LLVARans, len=37): [redacted]
// 004 Amount (n, len=12): 000000001000

iso.explain(bytes, { unmask: true }) // raw values — local debugging only, never log this
  • mask: 'pan' → keeps first 6 / last 4 digits, stars the middle.
  • mask: 'redact' → replaces the whole value with [redacted].
  • no mask → value shown as-is.

🧾 API reference

class Iso8583 {
  constructor(spec: MessageSpec, opts?: { header?: HeaderSpec })

  pack(
    mti: string,
    fields: Record<number, Buffer | string | number>,
  ): {
    mti: string
    bytes: Buffer
  }

  unpack(buf: Buffer): {
    mti: string
    fields: Record<number, Buffer | string | number>
    bytesRead: number
  }

  explain(buf: Buffer, opts?: { unmask?: boolean }): string
}

type FieldSpec = { name: string; format: FormatObject; mask?: MaskMode }
type MessageSpec = Record<number, FieldSpec>
type MaskMode = 'pan' | 'redact'

// Optional message header (e.g. an acquirer prefix). encode wraps the body; decode reports
// how many leading bytes to skip on unpack.
type HeaderSpec = {
  encode: (messageBytes: Buffer) => Buffer
  decode: (buf: Buffer) => { headerLength: number }
}

⚠️ Errors

All errors are thrown Errors with stable messages. The most common:

| When | Message (pattern) | | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | Field present in pack/unpack with no spec entry | No spec for DE<n> | | Numeric value longer than the field | Numeric value has <x> digits > N<len> | | Alpha value longer than the field | DE<n> exceeds <kind><len> | | Variable field value over its max length | DE<n> length <x> > max <max> | | Binary field wrong size | DE<n> must be <expected> bytes | | Invalid hex for a fixed binary field | Invalid hex for DE<n> | | Invalid/odd hex for a binary var field | Invalid hex for binary var-length field | | Non-digit passed to a numeric field | Expected digits only, got "<s>" | | lenCountMode: 'digits' on a non-numeric var field | DE<n> invalid lenCountMode=digits for non-n field | | MTI not 4 digits | Invalid MTI "<m>" (expect 4 digits) | | DE > 64 present but bitmap is 8 bytes | Bitmap constrained to 64 bits but DE>64 present | | Secondary bitmap seen while constrained to 64 | Secondary bitmap present but constrained to 64 | | Buffer too short while decoding | Primary bitmap underrun / Secondary bitmap underrun / DE<n> underrun / Length header underrun |


🚫 Not handled

By design, so you stay in control of the transport:

  • TCP length framing. Acquirer links typically prefix each message with a 2-byte length header. This library does not add or strip it — wrap pack()'s bytes and slice it off before unpack().
  • Fixed-width trailing spaces. Alpha fields are space-padded and unpack trims trailing spaces, so values that legitimately end in spaces will not round-trip.
  • MAC / PIN cryptography. No key management, MACing, or PIN-block generation.

⭐ Inspiration

Inspired by excellent prior ISO 8583 work:

📄 License

MIT.