Free & open source — no account required

v1.2.5-PRICING-19
Financial Engineering • Engineering Documentation

ISO 20022 Engineering: Automating Global Financial Message Parsing

This technical guide provides an in-depth analysis of the iso 20022 to typescript engine, best practices for implementation, and data security standards.

Institutional Grade Utility

Need Advanced Financial Parsing?

Unlock the full power of the FinFlow Pro workbench. Specifically designed for FIX, SWIFT MT/MX, and ISO 20022 message inspection with industrial-grade accuracy.

Launch FinFlow Pro

ISO 20022 to TypeScript: XSD Codegen, pacs.008 Parsing, Zod IBAN/BIC Validation, and MT Coexistence

ISO 20022 is the XML-based global standard replacing SWIFT MT for cross-border payments, securities settlement, and reporting. The standard defines hundreds of message types (pacs.008 for customer credit transfers, camt.053 for bank statements, pain.001 for customer credit transfer initiation) each backed by an official XSD. Converting ISO 20022 to TypeScript requires three things: XSD-to-TypeScript code generation (using xsd2ts or cxsd to produce interfaces from the 200+ page schema), fast-xml-parser configuration for the ISO 20022 namespace conventions, and Zod validation for cross-field business rules (IBAN check digit, BIC format, amount precision) that XSD alone cannot express.

XSD Codegen: Generating TypeScript Types from ISO 20022 Schemas

// The ISO 20022 XSD schemas are available at iso20022.org/catalogue-messages
// For pacs.008.001.10 (Customer Credit Transfer):
// Download: pacs.008.001.10.xsd (typically 2-5MB, 400+ type definitions)

// Option 1: xsd2ts CLI (npm install -g xsd2ts)
// xsd2ts --input pacs.008.001.10.xsd --output src/types/pacs008.ts

// Option 2: cxsd (more complete, handles circular references)
// npm install -g cxsd
// cxsd pacs.008.001.10.xsd --out-dir src/types/

// Example of what gets generated for the core pacs.008 types:
export interface FIToFICustomerCreditTransferV10 {
  GrpHdr:  GroupHeader93;
  CdtTrfTxInf: CreditTransferTransaction50[];  // 1..unbounded transactions
  SplmtryData?: SupplementaryData1[];
}

export interface GroupHeader93 {
  MsgId:      string;   // Max35Text — unique per message
  CreDtTm:    string;   // ISODateTime — ISO 8601 format
  BtchBookg?: boolean;  // BatchBookingIndicator
  NbOfTxs:    string;   // Max15NumericText — numeric string (not number!)
  TtlIntrBkSttlmAmt?: ActiveCurrencyAndAmount;
  IntrBkSttlmDt?: string; // ISODate — YYYY-MM-DD
  SttlmInf:   SettlementInstruction11;
}

export interface CreditTransferTransaction50 {
  PmtId:          PaymentIdentification13;
  PmtTpInf?:      PaymentTypeInformation28;
  IntrBkSttlmAmt: ActiveCurrencyAndAmount;
  IntrBkSttlmDt:  string;  // ISODate
  SttlmPrty?:     Priority3Code;
  Dbtr:           BranchAndFinancialInstitutionIdentification8;
  DbtrAcct?:      CashAccount40;
  DbtrAgt:        BranchAndFinancialInstitutionIdentification8;
  CdtrAgt:        BranchAndFinancialInstitutionIdentification8;
  Cdtr:           BranchAndFinancialInstitutionIdentification8;
  CdtrAcct?:      CashAccount40;
  Purp?:          Purpose2Choice;
  RmtInf?:        RemittanceInformation21;
  UndrlygCstmrCdtTrf?: UnderlyingCustomerCreditTransfer6;  // for MT202COV
}

export interface ActiveCurrencyAndAmount {
  Ccy:    string;  // ActiveCurrencyCode — ISO 4217
  value:  string;  // DecimalNumber as string (NOT number — precision loss)
}

export interface BranchAndFinancialInstitutionIdentification8 {
  FinInstnId: FinancialInstitutionIdentification23;
  BrnchId?:   BranchData5;
}

export interface FinancialInstitutionIdentification23 {
  BICFI?:   string;   // BICFIDec2014Identifier — 8 or 11 char BIC
  ClrSysMmbId?: ClearingSystemMemberIdentification2;
  LEI?:     string;   // LEIIdentifier — Legal Entity Identifier (20 chars)
  Nm?:      string;   // Max140Text
  PstlAdr?: PostalAddress27;
  Othr?:    GenericFinancialIdentification1;
}

export interface CashAccount40 {
  Id:   AccountIdentification4Choice;  // IBAN or Other
  Tp?:  CashAccountType2Choice;
  Ccy?: string;
  Nm?:  string;  // Max70Text
}

export interface AccountIdentification4Choice {
  IBAN?: string;  // IBAN2007Identifier — validated format
  Othr?: GenericAccountIdentification1;
}

Parsing pacs.008 XML with fast-xml-parser

import { XMLParser } from 'fast-xml-parser';

// ISO 20022 XML uses namespaces like:
// xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.10"
// xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

const parser = new XMLParser({
  ignoreAttributes:    false,
  attributeNamePrefix: '@_',    // keep Ccy attribute as '@_Ccy'
  removeNSPrefix:      true,    // strip 'urn:iso:...' namespace prefixes
  isArray: (name) => [
    'CdtTrfTxInf',   // always array (1..n transactions)
    'SplmtryData',
    'Ustrd',          // Unstructured remittance lines
    'Strd',           // Structured remittance
    'RfrdDocAmt',
    'Cdtr',           // Can appear multiple times in some messages
  ].includes(name),
  tagValueProcessor: (tagName, tagValue) => tagValue.trim(),
});

function parsePacs008(xml: string): FIToFICustomerCreditTransferV10 {
  const doc = parser.parse(xml);

  // Navigate the ISO 20022 envelope:
  // 
  //   ...
  const envelope = doc?.Document?.FIToFICstmrCdtTrf;
  if (!envelope) throw new Error('Not a valid pacs.008 document');

  return envelope as FIToFICustomerCreditTransferV10;
}

// Handle the ActiveCurrencyAndAmount dual-representation:
// XML: 15000.00
// fast-xml-parser result: { '@_Ccy': 'USD', '#text': '15000.00' }

function parseAmount(raw: unknown): ActiveCurrencyAndAmount {
  if (typeof raw === 'object' && raw !== null) {
    return { Ccy: (raw as any)['@_Ccy'], value: String((raw as any)['#text'] ?? '') };
  }
  throw new Error('Invalid amount element');
}

// Full usage:
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.10">
  <FIToFICstmrCdtTrf>
    <GrpHdr>
      <MsgId>MSGID-20240315-001</MsgId>
      <CreDtTm>2024-03-15T10:00:00Z</CreDtTm>
      <NbOfTxs>1</NbOfTxs>
      <SttlmInf><SttlmMtd>CLRG</SttlmMtd></SttlmInf>
    </GrpHdr>
    <CdtTrfTxInf>
      <PmtId>
        <InstrId>PAY-2024-001</InstrId>
        <EndToEndId>E2E-REF-001</EndToEndId>
        <UETR>7f8ddb8e-0e3e-4e0a-9d09-93a27c0c6b5f</UETR>
      </PmtId>
      <IntrBkSttlmAmt Ccy="USD">15000.00</IntrBkSttlmAmt>
      <IntrBkSttlmDt>2024-03-15</IntrBkSttlmDt>
      <DbtrAgt><FinInstnId><BICFI>BARCGB22XXX</BICFI></FinInstnId></DbtrAgt>
      <Dbtr><FinInstnId><BICFI>BARCGB22XXX</BICFI></FinInstnId></Dbtr>
      <CdtrAgt><FinInstnId><BICFI>BNPAFRPPXXX</BICFI></FinInstnId></CdtrAgt>
      <Cdtr><FinInstnId><BICFI>BNPAFRPPXXX</BICFI></FinInstnId></Cdtr>
    </CdtTrfTxInf>
  </FIToFICstmrCdtTrf>
</Document>`;

const msg = parsePacs008(xml);
console.log(msg.GrpHdr.MsgId);       // 'MSGID-20240315-001'
console.log(msg.CdtTrfTxInf.length); // 1

Zod Validation: IBAN Check Digit, BIC Format, Amount Precision

import { z } from 'zod';

// IBAN validation: check digit via MOD-97 algorithm
function validateIban(iban: string): boolean {
  const normalized = iban.replace(/\s/g, '').toUpperCase();
  if (!/^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(normalized)) return false;

  // Move first 4 chars to end, convert letters to digits (A=10, B=11, ...)
  const rearranged = normalized.slice(4) + normalized.slice(0, 4);
  const numeric    = rearranged.split('').map(c =>
    /[A-Z]/.test(c) ? String(c.charCodeAt(0) - 55) : c
  ).join('');

  // MOD-97 on large number — process in chunks to avoid overflow
  let remainder = '';
  for (const chunk of numeric.match(/.{1,9}/g) ?? []) {
    remainder = String((parseInt(remainder + chunk) % 97));
  }
  return remainder === '1';
}

// BIC validation: 8 or 11 chars, AAAABBCCDDD format
const BIC_REGEX = /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/;

// Zod schemas for ISO 20022 boundary validation
const IbanSchema = z.string()
  .transform(s => s.replace(/\s/g, '').toUpperCase())
  .refine(validateIban, { message: 'Invalid IBAN check digit' });

const BicSchema = z.string()
  .toUpperCase()
  .refine(s => BIC_REGEX.test(s), { message: 'Invalid BIC/SWIFT code format' });

const Iso20022AmountSchema = z.string()
  .refine(s => /^\d+(\.\d{1,5})?$/.test(s), { message: 'Amount must be a decimal string' })
  .refine(s => parseFloat(s) > 0, { message: 'Amount must be positive' });

const Pacs008TransactionSchema = z.object({
  IntrBkSttlmAmt: z.object({
    Ccy:   z.string().length(3, 'Currency must be 3-character ISO 4217'),
    value: Iso20022AmountSchema,
  }),
  IntrBkSttlmDt:  z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Must be YYYY-MM-DD'),
  DbtrAcct: z.object({ Id: z.object({ IBAN: IbanSchema }) }).optional(),
  CdtrAcct: z.object({ Id: z.object({ IBAN: IbanSchema }) }).optional(),
  DbtrAgt:  z.object({ FinInstnId: z.object({ BICFI: BicSchema.optional() }) }),
  CdtrAgt:  z.object({ FinInstnId: z.object({ BICFI: BicSchema.optional() }) }),
});

// Usage at API boundary:
const result = Pacs008TransactionSchema.safeParse(msg.CdtTrfTxInf[0]);
if (!result.success) {
  console.error('ISO 20022 validation failed:', result.error.flatten());
} else {
  // result.data is typed and validated
}

Best Practices for Production

  • Never parse ISO 20022 amounts as JavaScript number: The ISO 20022 DecimalNumber type can have up to 18 decimal places. JavaScript's IEEE 754 double loses precision for amounts like 1234567890.123456789. Always store as string and use Decimal.js or BigDecimal for arithmetic. The XSD-generated types in this guide use string for all amount fields for this reason.
  • Use the UETR (Unique End-to-End Transaction Reference) as your idempotency key: The UETR (PmtId/UETR in pacs.008) is a UUID that uniquely identifies a payment across all banks in the processing chain. Store it as your primary deduplication key in the database. Unlike MsgId (unique per message, not per payment), the UETR follows the payment through MT202COV covers, returns, and investigations.
  • Generate types from the official XSD rather than hand-writing them: ISO 20022 message schemas have hundreds of types, complex optional/mandatory rules per element, and version-specific changes (pacs.008.001.08 vs .10 differ in several fields). Running xsd2ts or cxsd against the official XSD guarantees completeness and lets you regenerate when the schema is updated.
  • Handle both IBAN and Othr account identification: Not all accounts have IBANs — US ACH accounts use CashAccount40/Id/Othr with SchmeNm/Cd=BBAN. Your parser and Zod schemas must handle both Id.IBAN and Id.Othr branches of the AccountIdentification4Choice union, or you'll silently drop non-IBAN account references.

FAQ

Q: What is the difference between pacs.008, pain.001, and camt.053?
A: ISO 20022 organizes messages by business area: pain (Payment Initiation) — corporate-to-bank instructions like pain.001 Customer Credit Transfer Initiation; pacs (Payment Clearing and Settlement) — interbank settlement messages like pacs.008 FI-to-FI Customer Credit Transfer; camt (Cash Management) — bank statements and account reporting like camt.053 Bank-to-Customer Statement. A single customer payment flows through pain.001 → pacs.008 → camt.053 at different stages.

Q: How do I validate ISO 20022 XML against the official XSD?
A: Use Node.js with libxmljs2 or the Java-based Saxon processor for XSD 1.1 validation. The ISO 20022 XSDs use complex content models and identity constraints that basic XML validators may not support. For lightweight validation, use fast-xml-parser to parse and then apply Zod schemas for business rule validation — this handles 95% of practical errors without full XSD validation overhead.

Q: How do ISO 20022 messages relate to SWIFT MT during the migration?
A: SWIFT's coexistence translation service automatically converts MT103 ↔ pacs.008 and MT202 ↔ pacs.009 during the migration period. The UETR (tag 121 in MT block 3) is the bridge — it flows through both formats and enables end-to-end tracking. After the mandatory cutover date, only MX (ISO 20022) messages will flow over the SWIFT network for cross-border payments, but many domestic and proprietary networks will continue using MT for years.

Developer FAQ

Is the processing local-only?

Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.

Can I use this for enterprise projects?

Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.