Key Data Elements Deep Dive
Lesson, slides, and applied problem sets.
View SlidesLesson
Module 4: Key Data Elements Deep Dive
The Heart of the Message
You've parsed the MTI. You've decoded the bitmap. You've extracted each field's raw bytes. Now what?
Each field in an ISO8583 message carries specific meaning. Some are straightforward - DE11 (STAN) is just a 6-digit number. Others are surprisingly complex - DE35 (Track 2 Data) encodes PAN, expiry, service code, and discretionary data in a single string with its own parsing rules.
This module covers the fields you'll encounter in 90% of authorization messages. Master these, and you can debug most payment failures.
DE2: Primary Account Number (PAN)
The PAN is the card number. It's probably the most sensitive field in the message.
Format
Field: DE2
Type: LLVAR n..19
Example: "4532015112830366" (16 digits)
"5425233430109903" (16 digits, Mastercard)
"378282246310005" (15 digits, Amex)
The length varies: Visa/Mastercard use 16 digits, Amex uses 15, some older cards use 13.
The Structure
A PAN isn't random. It has structure:
PAN: 4532015112830366
├──────┤├──────────┤├┤
IIN/BIN Account Check
Number Digit
IIN (Issuer Identification Number):
- First 6-8 digits (historically 6, now migrating to 8)
- Identifies the card network and issuing bank
- 4... = Visa
- 51-55... = Mastercard
- 34, 37... = American Express
- 6011, 65... = Discover
The IIN (also called BIN - Bank Identification Number) tells you who issued the card without contacting anyone.
Luhn Validation
The last digit is a check digit, calculated using the Luhn algorithm. This catches typos and transmission errors.
The Algorithm:
- Starting from the rightmost digit (check digit), double every second digit
- If doubling results in a number > 9, subtract 9
- Sum all digits
- If sum is divisible by 10, the PAN is valid
Example: 4532015112830366
Position: 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
Digit: 4 5 3 2 0 1 5 1 1 2 8 3 0 3 6 6
Double: [8] 5 [6] 2 [0] 1 [10] 1 [2] 2 [16] 3 [0] 3 [12] 6
After -9: 8 5 6 2 0 1 1 1 2 2 7 3 0 3 3 6
↑ ↑ ↑
10→1 16→7 12→3
Sum: 8+5+6+2+0+1+1+1+2+2+7+3+0+3+3+6 = 50
50 % 10 = 0 ✓ Valid!
Go Implementation
// ValidateLuhn checks if a PAN passes the Luhn check
func ValidateLuhn(pan string) bool {
sum := 0
double := false
// Process right-to-left
for i := len(pan) - 1; i >= 0; i-- {
digit := int(pan[i] - '0')
if digit < 0 || digit > 9 {
return false // Invalid character
}
if double {
digit *= 2
if digit > 9 {
digit -= 9
}
}
sum += digit
double = !double
}
return sum%10 == 0
}
PAN Masking
PANs are highly sensitive. In logs, you'll see masked versions:
Full: 4532015112830366
Masked: 453201******0366 (first 6, last 4)
Masked: ************0366 (last 4 only)
Masked: 4532XXXXXXXXXXXX (first 4, rest hidden)
PCI DSS rules: You can display first 6 and last 4, or first 4 alone. Never log the full PAN.
// MaskPAN returns a PCI-compliant masked PAN (first 6 + last 4)
func MaskPAN(pan string) string {
if len(pan) < 13 {
return strings.Repeat("*", len(pan))
}
return pan[:6] + strings.Repeat("*", len(pan)-10) + pan[len(pan)-4:]
}
Common Bugs
Bug 1: Treating PAN as a number
// WRONG: Loses leading zeros, overflows
pan, _ := strconv.Atoi("0000123456789012")
// RIGHT: Keep as string
pan := "0000123456789012"
Some test PANs have leading zeros. Integer conversion destroys them.
Bug 2: Hardcoding 16-digit length
// WRONG: Amex has 15 digits
if len(pan) != 16 {
return false
}
// RIGHT: Accept valid range
if len(pan) < 13 || len(pan) > 19 {
return false
}
DE3: Processing Code
DE3 tells you what kind of transaction this is. It's 6 digits with a specific structure.
Format
Field: DE3
Type: Fixed n 6
Example: "000000" (Purchase with default accounts)
"010000" (Cash advance)
"200000" (Refund)
The Structure
DE3: 00 00 00
├┤ ├┤ ├┤
│ │ └─ To Account Type
│ └─ From Account Type
└─ Transaction Type
Transaction Types (first 2 digits):
00 = Purchase
01 = Cash Advance / Cash Withdrawal
02 = Adjustment
09 = Purchase with Cash Back
20 = Refund / Return
30 = Balance Inquiry
Account Types (digits 3-4 and 5-6):
00 = Default / Unspecified
10 = Savings
20 = Checking
30 = Credit
40 = Universal / Unspecified
Real-World Examples
"000000" = Purchase, default accounts
Most common auth request
"010020" = Cash Advance from Credit account to Checking
ATM withdrawal billed to credit card
"200000" = Refund, default accounts
Return/reversal credit
"003000" = Purchase from Credit account
Explicit credit card purchase
"310000" = Balance Inquiry
Card check without purchase
Why It Matters
Processing code drives:
- Fee calculation: Cash advances have different interchange
- Account selection: Debit cards can have multiple accounts
- Authorization rules: Some merchants can't do cash back
- Fraud detection: Unusual processing codes trigger alerts
type ProcessingCode struct {
TransactionType string // First 2 digits
FromAccount string // Middle 2 digits
ToAccount string // Last 2 digits
}
func ParseProcessingCode(de3 string) ProcessingCode {
if len(de3) != 6 {
return ProcessingCode{}
}
return ProcessingCode{
TransactionType: de3[0:2],
FromAccount: de3[2:4],
ToAccount: de3[4:6],
}
}
func (pc ProcessingCode) IsPurchase() bool {
return pc.TransactionType == "00" || pc.TransactionType == "09"
}
func (pc ProcessingCode) IsRefund() bool {
return pc.TransactionType == "20"
}
func (pc ProcessingCode) IsCashAdvance() bool {
return pc.TransactionType == "01"
}
DE4, DE5, DE6: Amount Fields
Money is surprisingly hard to represent correctly. ISO8583 uses fixed-length numeric fields with implicit decimal points.
Format
Field: DE4 (Transaction Amount)
Type: Fixed n 12
Example: "000000010000" = 100.00 (with 2 decimal places)
"000000000099" = 0.99
Field: DE5 (Settlement Amount)
Type: Fixed n 12
Example: Same format, different currency context
Field: DE6 (Cardholder Billing Amount)
Type: Fixed n 12
Example: Same format, in cardholder's currency
The Decimal Point Problem
Where's the decimal point in 000000010000?
It depends on the currency. Different currencies have different exponents:
Currency Code Exponent 10000 means
───────────────────────────────────────────
USD 840 2 $100.00
EUR 978 2 €100.00
JPY 392 0 ¥10000 (no decimals)
KWD 414 3 0.100 KWD (3 decimals)
BHD 048 3 0.100 BHD
The currency exponent from DE49 (Currency Code) determines interpretation:
- Exponent 2: Divide by 100 (USD, EUR, GBP)
- Exponent 0: No division (JPY, KRW)
- Exponent 3: Divide by 1000 (KWD, BHD)
ISO 4217 Currency Codes
DE49 contains a 3-digit ISO 4217 numeric currency code:
var currencyExponents = map[string]int{
"840": 2, // USD
"978": 2, // EUR
"826": 2, // GBP
"392": 0, // JPY
"410": 0, // KRW
"414": 3, // KWD
"048": 3, // BHD
// ... many more
}
func ParseAmount(de4 string, de49 string) float64 {
amount, _ := strconv.ParseInt(de4, 10, 64)
exponent := currencyExponents[de49]
switch exponent {
case 0:
return float64(amount)
case 2:
return float64(amount) / 100.0
case 3:
return float64(amount) / 1000.0
default:
return float64(amount) / 100.0 // Default to 2
}
}
Why Three Amount Fields?
Consider a US cardholder buying something in Japan:
DE4 = "000000005000" (¥5000, transaction amount in JPY)
DE49 = "392" (JPY)
DE5 = "000000003300" ($33.00, settlement in USD)
DE50 = "840" (USD)
DE6 = "000000003300" ($33.00, billed to cardholder)
DE51 = "840" (USD)
- DE4: Amount in transaction currency (what the merchant sees)
- DE5: Amount in settlement currency (what the acquirer settles)
- DE6: Amount in cardholder currency (what appears on the statement)
The Floating Point Trap
// WRONG: Floating point comparison
if amount == 100.00 {
// This might not match due to floating point precision!
}
// RIGHT: Work with integers (cents/pips)
amountCents := 10000 // $100.00 = 10000 cents
if amountCents == 10000 {
// Exact match
}
Always store and compare amounts as integers in the smallest currency unit.
DE7, DE12, DE13: Date and Time Fields
Time in ISO8583 is fragmented across multiple fields, each with its own format.
Field Formats
DE7: Transmission Date/Time - n 10 (MMDDhhmmss)
DE12: Local Time - n 6 (hhmmss)
DE13: Local Date - n 4 (MMDD)
The Timezone Problem
Transaction at a US terminal:
Local time: 2024-03-15 14:30:45 EST
DE7 = "0315193045" (UTC: March 15, 19:30:45)
DE12 = "143045" (Local: 14:30:45)
DE13 = "0315" (Local: March 15)
- DE7 is typically UTC (Coordinated Universal Time)
- DE12 + DE13 are terminal local time
This is a common source of bugs when matching requests to responses.
Where's the Year?
Notice something missing? There's no year in DE7, DE12, or DE13.
ISO8583 assumes messages are processed quickly. Year is implicit - usually the current year. At year boundaries (December 31 → January 1), this gets tricky:
func ParseDE7(de7 string, referenceTime time.Time) time.Time {
// DE7 format: MMDDhhmmss
month, _ := strconv.Atoi(de7[0:2])
day, _ := strconv.Atoi(de7[2:4])
hour, _ := strconv.Atoi(de7[4:6])
minute, _ := strconv.Atoi(de7[6:8])
second, _ := strconv.Atoi(de7[8:10])
// Guess the year - use reference time
year := referenceTime.Year()
// Handle year boundary: if DE7 says December and it's January,
// the message is probably from last year
if month == 12 && referenceTime.Month() == 1 {
year--
}
// If DE7 says January and it's December, it's probably next year
if month == 1 && referenceTime.Month() == 12 {
year++
}
return time.Date(year, time.Month(month), day, hour, minute, second, 0, time.UTC)
}
Time Field Mismatches
When request DE7 doesn't match response DE7, check:
- Timezone conversion error: Terminal vs host timezone
- Network delay: 30+ second delays cause mismatches
- Replay attacks: Suspicious if time is hours old
- Year boundary: December 31 transactions appearing in January
DE11: Systems Trace Audit Number (STAN)
STAN is a 6-digit number that uniquely identifies a transaction from a terminal's perspective.
Format
Field: DE11
Type: Fixed n 6
Example: "000001", "123456", "999999"
Uniqueness Scope
STAN must be unique within:
- Same terminal
- Same business day (or same settlement period)
After 999999, it wraps to 000001 (not 000000).
type STANGenerator struct {
current uint32
mu sync.Mutex
}
func (g *STANGenerator) Next() string {
g.mu.Lock()
defer g.mu.Unlock()
g.current++
if g.current > 999999 {
g.current = 1
}
return fmt.Sprintf("%06d", g.current)
}
Matching Role
STAN is part of the "matching key" that links request to response:
Request matching key: DE11 + DE12 + DE32 (sometimes)
STAN + LocalTime + AcquirerID
Reversal matching key: DE11 + DE12 + DE32 + original DE37
When a host receives a reversal, it uses these fields to find the original transaction.
DE37: Retrieval Reference Number (RRN)
RRN is a 12-character identifier assigned by the acquirer or network.
Format
Field: DE37
Type: Fixed an 12
Example: "123456789012"
"240315123456" (date-based: YYMMDD + 6 digits)
Difference from STAN
| Field | Length | Assigned By | Scope |
|---|---|---|---|
| DE11 (STAN) | 6 digits | Terminal | Terminal + day |
| DE37 (RRN) | 12 chars | Acquirer/Network | Network-wide |
RRN is what customer service uses to look up transactions. It appears on receipts and statements.
Common Formats
Format 1: Sequential
"000000000001", "000000000002", ...
Format 2: Date + Sequence
"240315" + "123456" = "240315123456"
YYMMDD Sequence
Format 3: Terminal + Sequence
"TRM001" + "123456" = "TRM001123456"
6-char 6-digit
The format varies by network. Document what your network expects.
DE35: Track 2 Data
Track 2 is the magnetic stripe data. Even in the chip era, this field appears in fallback transactions and some network flows.
Wire Format
Field: DE35
Type: LLVAR z..37
Example: "4532015112830366D25121011234500000000"
The z data type means "Track 2 code set": digits (0-9), separator = or D, and field separator F.
Structure
Track 2: 4532015112830366D2512101DDDDDDDDDDDDD
├──────PAN──────┤├────────────────┤
│├──┤├─┤├──────────┤
Sep Exp SC Discretionary
YYMM
Components:
- PAN: Primary Account Number (variable, up to 19)
- Separator: "=" (ASCII) or "D" (some encodings)
- Expiry: YYMM format (4 digits)
- Service Code: 3 digits
- Discretionary Data: Issuer-specific, variable
Service Code Breakdown
The 3-digit service code encodes card capabilities:
Service Code: 1 0 1
│ │ └─ Digit 3: PIN requirements
│ └─ Digit 2: Authorization requirements
└─ Digit 1: Interchange rules
Digit 1 (Interchange):
1 = International, chip
2 = International, magnetic
5 = National, chip
6 = National, magnetic
Digit 2 (Authorization):
0 = Normal authorization
2 = Contact issuer
4 = Contact issuer except under floor limit
Digit 3 (PIN/Services):
0 = No restrictions
1 = No cash
2 = Goods/services + cash
3 = ATM only
5 = Goods/services + cash, PIN required
6 = No restrictions, PIN required
7 = Goods/services, PIN required
Parsing Track 2
type Track2Data struct {
PAN string
ExpiryYYMM string
ServiceCode string
Discretionary string
}
func ParseTrack2(track2 string) (Track2Data, error) {
// Find separator (= or D)
sepIdx := strings.IndexAny(track2, "=D")
if sepIdx == -1 {
return Track2Data{}, fmt.Errorf("no separator found in track 2")
}
pan := track2[:sepIdx]
rest := track2[sepIdx+1:]
// Expiry is always 4 digits after separator
if len(rest) < 4 {
return Track2Data{}, fmt.Errorf("track 2 too short for expiry")
}
expiry := rest[:4]
rest = rest[4:]
// Service code is next 3 digits
if len(rest) < 3 {
return Track2Data{}, fmt.Errorf("track 2 too short for service code")
}
serviceCode := rest[:3]
discretionary := rest[3:]
return Track2Data{
PAN: pan,
ExpiryYYMM: expiry,
ServiceCode: serviceCode,
Discretionary: discretionary,
}, nil
}
Track 2 vs Track 1
Track 1 (DE45) contains cardholder name. Track 2 doesn't.
Track 1: B4532015112830366^SMITH/JOHN^25121010000000000000
Track 2: 4532015112830366=25121010000000000000
Why Track 2 is more common:
- Shorter (less bandwidth)
- No cardholder name (simpler PCI scope)
- Sufficient for most authorizations
DE39: Response Code
This 2-character field tells you whether the transaction was approved.
Common Response Codes
Code Meaning Action
──── ─────── ──────
00 Approved Complete transaction
01 Refer to card issuer Voice authorization needed
03 Invalid merchant Check configuration
05 Do not honor Decline (generic)
12 Invalid transaction Check processing code
13 Invalid amount Check DE4
14 Invalid card number Check PAN/Luhn
30 Format error Check message structure
41 Lost card, pick up Decline, retain card
43 Stolen card, pick up Decline, retain card
51 Insufficient funds Decline
54 Expired card Decline
55 Incorrect PIN Decline, decrement counter
57 Transaction not permitted Card restrictions
61 Exceeds withdrawal limit Decline
91 Issuer unavailable Retry later
96 System malfunction Retry later
Response Code Categories
type ResponseCategory string
const (
CategoryApproved ResponseCategory = "approved"
CategoryDeclined ResponseCategory = "declined"
CategoryReferral ResponseCategory = "referral"
CategorySystemError ResponseCategory = "system_error"
CategoryPickUp ResponseCategory = "pick_up"
)
func CategorizeResponse(code string) ResponseCategory {
switch code {
case "00":
return CategoryApproved
case "01", "02":
return CategoryReferral
case "41", "43":
return CategoryPickUp
case "91", "96":
return CategorySystemError
default:
// Most other codes are declines
return CategoryDeclined
}
}
Response Code vs Action Code
Some networks use DE39 (Response Code) and DE38 (Authorization Code) together:
Approved transaction:
DE38 = "A12345" (Authorization code)
DE39 = "00" (Approved)
Declined transaction:
DE38 = "" (No auth code)
DE39 = "51" (Insufficient funds)
The authorization code is what goes on the receipt. The response code is for systems.
DE54: Additional Amounts
When DE4 isn't enough, DE54 provides structured additional amounts.
Format
Field: DE54
Type: LLLVAR an...120
Structure: Multiple 20-byte records, TLV-like
Record Structure
Each 20-byte record:
Positions Length Content
1-2 2 Account type
3-4 2 Amount type
5-7 3 Currency code
8 1 Credit/Debit indicator (C/D)
9-20 12 Amount (right-justified, zero-filled)
Example
DE54: "0000840C000000150000"
├┤├┤├──┤│├────────────┤
00 00 840 C 000000150000
Parsed:
- Account Type: 00 (default)
- Amount Type: 00 (default/actual)
- Currency: 840 (USD)
- Sign: C (credit/positive)
- Amount: 1500.00 (with 2 decimal exponent)
Common Amount Types
Type Meaning
00 Actual or ledger balance
01 Available balance
02 Credit limit
20 Remaining available
40 Cash-back amount
Multiple Amounts
DE54 can contain multiple records:
DE54: "0001840C000000150000" + "0001840D000000025000"
└── Available balance └── Amount owed
type AdditionalAmount struct {
AccountType string
AmountType string
Currency string
Sign string // "C" or "D"
Amount int64
}
func ParseDE54(de54 string) ([]AdditionalAmount, error) {
var amounts []AdditionalAmount
// Each record is exactly 20 characters
for i := 0; i+20 <= len(de54); i += 20 {
record := de54[i : i+20]
amount, _ := strconv.ParseInt(record[8:20], 10, 64)
amounts = append(amounts, AdditionalAmount{
AccountType: record[0:2],
AmountType: record[2:4],
Currency: record[4:7],
Sign: record[7:8],
Amount: amount,
})
}
return amounts, nil
}
Putting It Together: Authorization Request Fields
A typical 0100 authorization request contains:
Field Name Example
───── ──── ───────
MTI Message Type "0100"
DE2 PAN "4532015112830366"
DE3 Processing Code "000000"
DE4 Amount "000000010000"
DE7 Transmission Date/Time "0315143045"
DE11 STAN "123456"
DE12 Local Time "143045"
DE13 Local Date "0315"
DE14 Expiration Date "2512" (YYMM)
DE22 POS Entry Mode "051"
DE23 Card Sequence Number "001"
DE25 POS Condition Code "00"
DE26 PIN Capture Code "12"
DE32 Acquiring Institution "123456"
DE35 Track 2 "4532015112830366D2512..."
DE37 RRN "240315123456"
DE41 Terminal ID "TERM0001"
DE42 Merchant ID "MERCH00000001"
DE43 Merchant Name/Location "ACME STORE/ANYTOWN"
DE49 Currency Code "840"
DE52 PIN Data <8 bytes encrypted>
Field Dependencies
Some fields are interrelated:
DE4 + DE49 → Actual amount (need currency exponent)
DE12 + DE13 → Local transaction time (no year!)
DE35 → Contains PAN, expiry, service code (redundant with DE2, DE14)
DE22 → Determines which fields are mandatory
POS Entry Mode (DE22) is particularly important:
DE22 = "051": Chip read
- DE55 (EMV Data) should be present
- DE35 may be absent
DE22 = "021": Magnetic stripe
- DE35 should be present
- DE55 should be absent
DE22 = "010": Manual entry
- DE2, DE14 from keyboard
- No DE35, no DE55
Field Dependency and Integrity Rules
Once a field is present, validating semantic compatibility matters as much as parsing.
Authorization Dependency Matrix
For auth-like traffic (0100, 0120):
DE22 DE2/DE14 DE35 DE37 DE55
──── ─────── ──── ──── ────
051 optional (chip) optional usually usually required
(Chip) (PAN from card) (may be) (network) (EMV ICC)
021 usually optional required required usually absent
(Magstripe) (PAN2) (legacy magstripe)
010 required required optional absent
(Manual) (keyboard) (no track2) (no ICC)
Invariant checks you should add
- If
DE2andDE35are both present, their PAN values should start with the same core. - If
DE3says cash withdrawal/refund, verify merchant context and allowed account types. - If
DE14expiry is present, compare against current date in the transaction timezone. - If
DE4is non-zero butDE5is missing in cross-currency networks, flag it. - If
DE39 != 00, avoid exposing this raw code to cardholder UI.
Integrity Test Strategy
Treat these as deterministic checks:
- Reject early: malformed length prefix or wrong field type
- Reject hard: mismatched cross-field invariants
- Alert, don't hide: unexpected combinations may indicate channel translation issues
Progressive Field-Level Tests
Use this as a deepening checklist after parser implementation:
Stage 1: Base Identity
- DE2 valid length and Luhn
- DE3 6-digit structure
Stage 2: Correlation Integrity
- DE11 + DE12 + DE13 + DE32 uniqueness test
- DE37 format and optional idempotency consistency
Stage 3: Flow-Dependent Requirements
- For
DE22=051, require EMV fields and validate DE55 presence - For
DE22=021, require track 2 and evaluate DE55 absence
Stage 4: Temporal Consistency
- Parse DE7, DE12, DE13 and validate they describe a plausible transaction moment
- Reject impossible timestamp leaps around year boundaries unless explicitly wrapped
Stage 5: Amount Semantics
- Parse DE4 and DE49 together
- Parse DE54 records by exact 20-byte chunks
Any failed stage should return a typed error, not a best-effort value.
Common Pitfalls
1. PAN Length Assumptions
// WRONG
if len(pan) != 16 {
return errors.New("invalid PAN")
}
// RIGHT: Amex is 15, Diners can be 14, some regional cards are 13
if len(pan) < 13 || len(pan) > 19 {
return errors.New("PAN length out of range")
}
2. Amount Without Currency
// WRONG: What currency is this?
amount := parseAmount("000000100000") // $1000 or ¥100000?
// RIGHT: Always pair amount with currency
type Money struct {
Amount int64 // Minor units (cents, etc.)
Currency string // ISO 4217 code
}
func parseTransaction(de4, de49 string) Money {
amt, _ := strconv.ParseInt(de4, 10, 64)
return Money{Amount: amt, Currency: de49}
}
3. Track 2 Separator Ambiguity
// WRONG: Only looks for '='
sepIdx := strings.Index(track2, "=")
// RIGHT: Some encodings use 'D'
sepIdx := strings.IndexAny(track2, "=D")
4. Response Code String Comparison
// WRONG: Numeric comparison
code, _ := strconv.Atoi(de39)
if code == 0 { // "00" works, but "0" would too (wrong!)
// approved
}
// RIGHT: String comparison
if de39 == "00" {
// approved
}
5. Year Boundary in DE7
// WRONG: Always use current year
year := time.Now().Year()
// RIGHT: Handle December→January transition
func inferYear(month int, refTime time.Time) int {
year := refTime.Year()
if month == 12 && refTime.Month() == time.January {
year--
}
if month == 1 && refTime.Month() == time.December {
year++
}
return year
}
Real-World Case Study: The Phantom Transactions
A processor noticed that ~0.1% of transactions were duplicated - merchants were seeing double charges on some cards.
Investigation:
Transaction 1: DE11=123456, DE12=143045, DE37=240315123456
Transaction 2: DE11=123456, DE12=143045, DE37=240315123457
Wait - same STAN and time, different RRN?
Root Cause: The terminal's STAN generator wasn't thread-safe. Two transactions processed simultaneously got the same STAN. The host treated them as duplicates and replied with cached responses, but the RRN generator was different and created unique RRNs.
The Fix:
// Before: Race condition
func (g *STANGenerator) Next() string {
g.current++
return fmt.Sprintf("%06d", g.current)
}
// After: Thread-safe
func (g *STANGenerator) Next() string {
g.mu.Lock()
defer g.mu.Unlock()
g.current++
return fmt.Sprintf("%06d", g.current)
}
Lesson: STAN uniqueness is critical. Race conditions in STAN generation cause subtle, hard-to-debug duplicate transactions.
Summary
Key data elements are where ISO8583 moves from abstract byte manipulation to actual payment data:
| Field | Purpose | Key Points |
|---|---|---|
| DE2 | PAN | Luhn validation, 13-19 digits, mask for logs |
| DE3 | Processing Code | 6 digits: TxnType + FromAcct + ToAcct |
| DE4 | Amount | Pair with DE49 for currency exponent |
| DE7 | Transmission DateTime | UTC, no year (MMDDhhmmss) |
| DE11 | STAN | Unique per terminal per day |
| DE35 | Track 2 | PAN + Expiry + ServiceCode + Discretionary |
| DE37 | RRN | 12-char network-wide reference |
| DE39 | Response Code | "00" = approved, know the common declines |
The critical skill: When a transaction fails, these fields tell you why. Learn to read them like a story - PAN identifies the card, DE3 says what was attempted, DE39 says what went wrong.
What's Next
Now you're ready to apply this knowledge:
- pan-validator: Implement Luhn validation and IIN extraction
- track2-parser: Parse track 2 into its components
- auth-request-parser: Parse a complete 0100 message
These problems combine everything: bitmap parsing, field encoding, and semantic interpretation.
Module Items
PAN Validator
ISO8583 problem: PAN Validator
Track 2 Parser
Authorization Request Parser
Key Data Elements Deep Dive