Skip to main content
digital payment systems cryptography banking protocols and blockchain internals

Payment Card Transaction Internals

9 min read Chapter 4 of 21

Payment Card Transaction Internals

When you tap your card at a coffee shop, you see a single event: the terminal beeps, a receipt prints, you leave with coffee. Behind that 2-second interaction, a message traverses at least four organizations, crosses multiple encryption boundaries, undergoes fraud scoring, triggers a real-time balance check, and initiates a multi-day settlement process that eventually moves actual money between banks.

This chapter dissects each layer.

The Four-Party Model

Every card payment involves four parties (five if you count the network separately):

  1. Cardholder: The person paying. Their card carries a PAN, cryptographic keys, and (if EMV) a chip that generates per-transaction cryptograms.

  2. Merchant: Accepts payment. Their POS terminal or e-commerce platform constructs an authorization request, encrypts sensitive fields, and sends it to their acquirer.

  3. Acquirer (Merchant’s Bank): Routes the authorization request to the card network. Takes on the risk that the merchant is legitimate. Deposits settlement funds into the merchant’s account.

  4. Issuer (Cardholder’s Bank): Receives the authorization request, checks the cardholder’s account (balance, credit limit, fraud signals), and approves or declines. Bears the fraud liability in most cases.

  5. Card Network (Visa, Mastercard, etc.): Routes messages between acquirer and issuer. Sets interchange rates. Operates the clearing and settlement system.

ISO 8583: The Payment Message Protocol

ISO 8583 is the binary message format used for financial transaction messages between acquirers and card networks. It predates REST, gRPC, and even HTTP. Every card authorization, reversal, chargeback, and settlement message uses this format.

Message Structure

An ISO 8583 message has three parts:

  1. Message Type Indicator (MTI): 4-digit code identifying the message
  2. Bitmap: 64 or 128 bits indicating which data elements are present
  3. Data Elements (DE): The actual fields, variable-length, packed in binary
from dataclasses import dataclass
from enum import Enum
import struct

class MessageType(Enum):
    AUTH_REQUEST = "0100"        # Authorization request
    AUTH_RESPONSE = "0110"       # Authorization response
    AUTH_REVERSAL = "0400"       # Reversal (void)
    AUTH_REVERSAL_RESP = "0410"  # Reversal response
    FINANCIAL_REQUEST = "0200"   # Purchase / cash advance
    FINANCIAL_RESPONSE = "0210"  # Purchase response
    NETWORK_MGMT = "0800"       # Network management (sign-on, key exchange)

@dataclass
class ISO8583Message:
    """
    Represents a parsed ISO 8583 message.
    
    In production, use a library like py8583 or jPOS.
    This implementation shows the internal structure.
    """
    mti: str
    bitmap: int  # 64-bit primary bitmap (128-bit if DE1 is set)
    data_elements: dict[int, bytes]
    
    def has_field(self, field_number: int) -> bool:
        """Check if a data element is present using the bitmap."""
        if field_number <= 64:
            return bool(self.bitmap & (1 << (64 - field_number)))
        return bool(self.bitmap & (1 << (128 - field_number)))

# Key data elements in a typical authorization request
AUTHORIZATION_FIELDS = {
    2:  "Primary Account Number (PAN)",       # Up to 19 digits
    3:  "Processing Code",                     # 6 digits: txn type + accounts
    4:  "Transaction Amount",                  # 12 digits, minor units
    7:  "Transmission Date and Time",          # MMDDhhmmss
    11: "Systems Trace Audit Number (STAN)",   # 6 digits, unique per terminal
    12: "Local Transaction Time",              # hhmmss
    13: "Local Transaction Date",              # MMDD
    14: "Expiration Date",                     # YYMM
    22: "POS Entry Mode",                      # How card data was captured
    23: "Card Sequence Number",                # For multi-card accounts
    25: "POS Condition Code",                  # e-commerce, MOTO, etc.
    26: "PIN Capture Code",                    # Length of PIN captured
    32: "Acquiring Institution ID",            # Acquirer BIN
    35: "Track 2 Data",                        # Magnetic stripe equivalent
    37: "Retrieval Reference Number",          # Unique transaction reference
    38: "Authorization ID Response",           # 6-char approval code
    39: "Response Code",                       # "00" = approved, "05" = declined
    41: "Card Acceptor Terminal ID",           # Terminal identifier
    42: "Card Acceptor ID Code",               # Merchant identifier
    43: "Card Acceptor Name/Location",         # Merchant name + city + country
    48: "Additional Data",                     # EMV data, 3DS data, tokens
    49: "Transaction Currency Code",           # ISO 4217 (840 = USD, 978 = EUR)
    52: "PIN Data",                            # Encrypted PIN block (8 bytes)
    55: "ICC System Related Data",             # EMV chip data (TLV-encoded)
}

A Real Authorization Flow

Here’s what happens at the protocol level when you tap your card:

def build_authorization_request(
    pan: str,
    amount_cents: int,
    currency_code: str,
    terminal_id: str,
    merchant_id: str,
    emv_data: bytes,
    pin_block: bytes | None = None
) -> ISO8583Message:
    """
    Build an ISO 8583 0100 (authorization request) message.
    
    This is the message the acquirer sends to the card network
    after receiving the transaction from the merchant's terminal.
    """
    from datetime import datetime
    now = datetime.utcnow()
    
    elements = {
        2:  pan.encode('ascii'),
        3:  b'000000',  # Purchase, default accounts
        4:  str(amount_cents).zfill(12).encode('ascii'),
        7:  now.strftime('%m%d%H%M%S').encode('ascii'),
        11: b'000001',  # STAN — would be sequential in production
        12: now.strftime('%H%M%S').encode('ascii'),
        13: now.strftime('%m%d').encode('ascii'),
        22: b'071',     # ICC + contactless
        25: b'00',      # Normal presentment
        32: b'123456',  # Acquirer BIN
        37: b'000000000001',  # Retrieval reference
        41: terminal_id.encode('ascii'),
        42: merchant_id.ljust(15).encode('ascii'),
        49: currency_code.encode('ascii'),
        55: emv_data,   # The chip's ARQC and related data
    }
    
    if pin_block:
        elements[52] = pin_block  # Encrypted PIN block
        elements[26] = b'04'     # 4-digit PIN captured
    
    # Build the bitmap
    bitmap = 0
    for field_num in elements:
        bitmap |= (1 << (64 - field_num))
    
    return ISO8583Message(
        mti="0100",
        bitmap=bitmap,
        data_elements=elements
    )

Data Element 55: The EMV Payload

DE55 is where chip card data lives. It’s a TLV (Tag-Length-Value) encoded structure containing:

EMV_TAGS_IN_DE55 = {
    "9F26": "Application Cryptogram (ARQC)",      # 8 bytes — the chip's signature
    "9F27": "Cryptogram Information Data (CID)",    # 1 byte — type of cryptogram
    "9F10": "Issuer Application Data (IAD)",        # Variable — issuer-specific
    "9F37": "Unpredictable Number",                 # 4 bytes — terminal random
    "9F36": "Application Transaction Counter (ATC)", # 2 bytes — monotonic counter
    "95":   "Terminal Verification Results (TVR)",   # 5 bytes — terminal checks
    "9A":   "Transaction Date",                      # 3 bytes (YYMMDD)
    "9C":   "Transaction Type",                      # 1 byte
    "9F02": "Amount Authorized",                     # 6 bytes (BCD)
    "9F03": "Amount Other",                          # 6 bytes (BCD)
    "9F1A": "Terminal Country Code",                 # 2 bytes
    "5F2A": "Transaction Currency Code",             # 2 bytes
    "9F33": "Terminal Capabilities",                 # 3 bytes
    "9F34": "Cardholder Verification Method Results", # 3 bytes
    "9F35": "Terminal Type",                         # 1 byte
    "9F09": "Application Version Number",            # 2 bytes
}

def parse_tlv(data: bytes) -> dict[str, bytes]:
    """
    Parse EMV TLV-encoded data from DE55.
    
    EMV uses BER-TLV encoding where tags can be 1-3 bytes
    and lengths follow ASN.1 BER rules.
    """
    result = {}
    pos = 0
    
    while pos < len(data):
        # Parse tag
        tag_start = pos
        tag_byte = data[pos]
        pos += 1
        
        # Multi-byte tag: if lower 5 bits are all 1s
        if (tag_byte & 0x1F) == 0x1F:
            while pos < len(data) and (data[pos] & 0x80):
                pos += 1
            pos += 1  # Last byte of tag
        
        tag = data[tag_start:pos].hex().upper()
        
        # Parse length
        length_byte = data[pos]
        pos += 1
        if length_byte <= 0x7F:
            length = length_byte
        elif length_byte == 0x81:
            length = data[pos]
            pos += 1
        elif length_byte == 0x82:
            length = (data[pos] << 8) | data[pos + 1]
            pos += 2
        else:
            break
        
        # Extract value
        value = data[pos:pos + length]
        pos += length
        result[tag] = value
    
    return result

Authorization, Clearing, and Settlement

A card payment isn’t a single event — it’s a three-phase process:

Phase 1: Authorization (Real-time, ~2 seconds)

The merchant’s terminal sends an auth request through the acquirer and network to the issuer. The issuer checks:

  • Does the account exist?
  • Is the card active (not frozen, expired, or reported stolen)?
  • Is there sufficient balance/credit?
  • Does the fraud scoring system flag this transaction?

If approved, the issuer returns response code 00 and an authorization code (6 alphanumeric characters). The issuer places a hold on the cardholder’s available balance but does not move any money yet.

Phase 2: Clearing (Batch, T+0 to T+1)

At end of day, the merchant (or their acquirer) submits a clearing file — a batch of all authorized transactions. The card network processes these, calculates interchange fees, and prepares settlement instructions.

Clearing Record for Transaction:
  Auth Code: A12B3C
  Amount: $42.50
  Merchant: Coffee Shop LLC
  MCC: 5812 (Eating Places/Restaurants)
  
  Interchange rate: 1.80% + $0.10 (Visa CPS Retail)
  Interchange fee: $0.87
  Network assessment: 0.14% = $0.06
  Acquirer keeps: $42.50 - $0.87 - $0.06 - acquirer_margin
  Merchant receives: ~$41.15 (varies by acquirer agreement)

Phase 3: Settlement (T+1 to T+2)

The card network instructs settlement through the banking system:

  • The issuer’s settlement bank transfers funds to the network
  • The network transfers (net of interchange) to the acquirer’s settlement bank
  • The acquirer deposits into the merchant’s bank account

No actual card network handles individual transactions in settlement. They calculate net positions: if Bank A issued cards that spent $10M at merchants acquiring through Bank B, and Bank B issued cards that spent $8M at Bank A’s merchants, the net settlement is Bank A paying Bank B $2M.

from dataclasses import dataclass, field
from decimal import Decimal
from collections import defaultdict

@dataclass
class SettlementPosition:
    """
    Calculate net settlement positions between institutions.
    
    In production, the card network (e.g., VisaNet, Banknet)
    runs this calculation across all member institutions,
    producing a single net amount per institution per currency.
    """
    positions: dict[str, Decimal] = field(
        default_factory=lambda: defaultdict(Decimal)
    )
    
    def add_transaction(
        self,
        issuer_id: str,
        acquirer_id: str,
        amount: Decimal,
        interchange_rate: Decimal
    ):
        interchange = amount * interchange_rate
        
        # Issuer receives interchange
        self.positions[issuer_id] += interchange
        # Issuer pays the transaction amount
        self.positions[issuer_id] -= amount
        
        # Acquirer receives transaction amount minus interchange
        self.positions[acquirer_id] += (amount - interchange)
    
    def get_net_positions(self) -> dict[str, Decimal]:
        """
        Returns net position per institution.
        Positive = institution receives funds.
        Negative = institution owes funds.
        """
        return dict(self.positions)

This net settlement approach is why card payments take 1-2 business days to settle. The clearinghouse batches millions of transactions, calculates the smallest possible number of actual fund transfers between banks, and instructs those transfers through the interbank payment rail (ACH in the US, SEPA in Europe, BACS in the UK).