← ISO 20022: integration and operational fundamentals
09 / 10 · 60 MIN

Lab: validate a BAH header

Execute synthetic instances against public head.001.001.04 XSD and explain what validation establishes and leaves unproven.

Artifact and scope

This lab uses the unchanged public head.001.001.04 XSD obtained from the official catalogue. Python code and instances are DR originals; the schema is an external artifact identified in the provenance record. The objective is to validate synthetic AppHdr headers, not send payments. The inspected catalogue lists V04, while the BAH overview still mentions V03; for this rehearsal, the file and its targetNamespace determine the version. The version adopted by a real community still depends on its applicable contract. Do not replace that agreement with the newest date on a webpage.

Prepare a repeatable execution

Save run.py and head.001.001.04.xsd in the same directory. The official download appears in the lesson references. The script checks SHA-256 against the recorded value before calling xmllint; a mismatch stops the rehearsal for investigation. Python 3.13 and an available xmllint are required, or set DR_XMLLINT to the executable path. The observed run used Python 3.13.1 and libxml2 2.9.13. The program installs no tools and downloads no files during testing. It processes only synthetic fixtures generated in its own code; this is no recommendation of that runtime for a production gateway.

Actually executed boundaries

Predict every outcome before execution. Removing Fr, reversing Fr and To, leaving BizMsgIdr empty, or using 36 characters causes rejection under the selected schema. The 35-character case passes. This pair distinguishes an inclusive boundary from a generation defect without automatically truncating identities. Another fixture emits both FIId and OrgId inside Fr and fails the structural choice. An impossible date also fails. The report retains exit code and diagnostic for each instance. The runner treats tool failures separately: a missing schema or execution failure must not count as an expected message rejection.

Schema passed, contract still incomplete

Some fixtures pass specifically to expose limits. MsgDefIdr is bounded text in the XSD and accepts an arbitrary identifier; definition existence and suitability remain unproven. CreDt without a timezone and an empty FinInstnId also pass the tested base cases. Fictional profile DR-BAH-A adds an explicit timezone, agreed identifier, and BICFI for both parties. These are teaching rules, not universal ISO requirements. A five-character external code passes its length constraint without consulting a list. Likewise, synthetic BICs matching the pattern were not confirmed in any directory or community.

Structure does not prove authenticity

Sgntr contains a signature-namespace wildcard with processContents lax. In the rehearsal, with no signature schema loaded, an empty Signature element in that namespace passes header validation. No cryptographic operation, certificate, key, or trust chain was checked. Switching to strict structural validation would not by itself perform cryptographic verification either. This case helps a PM request the appropriate evidence from the security owner: define the verification point, trust material, covered data, and expected outcome. The lab only demonstrates the boundary between structural presence and the assurance incorrectly inferred from it.

Interpret and deliver evidence

Reserve ten minutes to prepare tools and files, fifteen to predict outcomes, twenty to execute and compare, and fifteen to explain two insufficient acceptances. Output includes 22 XSD cases and fifteen profile and planning checks, totaling 37. Retain hashes of script, schema, and fixtures, plus validator version and diagnostics. The command runs --schema against a local file, with catalogs disabled and --nonet on this runtime. These options do not constitute a parser security assessment. Proper closure is local evidence for recorded cases; full MDR rules, financial body, signature, counterparty, and operational acceptance require their own validation.

"""Original DR BAH schema workshop. Python 3.13 + xmllint.
Obtain head.001.001.04.xsd from https://www.iso20022.org/message/23104/download
Place the unchanged file next to this script. Run:
python3 run.py --output evidence.json
Trusted generated fixtures only. No messages sent or cryptographic checks.
"""
import argparse
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from xml.etree import ElementTree as ET

SCHEMA_SHA = '73d68e98ec079806a23f37494ed47662a74cebfae91807bfbaacda3728d65796'
NS = 'urn:iso:std:iso:20022:tech:xsd:head.001.001.04'
MSG = 'pacs.008.001.08'
BASE = f'''<AppHdr xmlns="{NS}">
<Fr><FIId><FinInstnId><BICFI>DRXXZZ00</BICFI></FinInstnId></FIId></Fr>
<To><FIId><FinInstnId><BICFI>DRYYZZ00</BICFI></FinInstnId></FIId></To>
<BizMsgIdr>DR-HEADER-001</BizMsgIdr><MsgDefIdr>{MSG}</MsgDefIdr>
<CreDt>2026-10-03T10:00:00Z</CreDt></AppHdr>'''
# Synthetic identifiers are not verified directory members or real participants.
checks = []

def check(name, actual, expected):
 assert actual == expected, (name,actual,expected)
 checks.append(dict(name=name,actual=actual,expected=expected,passed=True))

def profile_errors(xml):
 """Invented DR-BAH-A profile, separate from base schema validation."""
 root=ET.fromstring(xml);n={'h':NS};errors=[]
 if root.findtext('h:MsgDefIdr',namespaces=n)!=MSG: errors.append('message contract')
 date=root.findtext('h:CreDt',default='',namespaces=n)
 if not re.search(r'(Z|[+-][0-9]{2}:[0-9]{2})$',date):errors.append('explicit timezone')
 for party in ('Fr','To'):
 if not root.findtext(f'h:{party}/h:FIId/h:FinInstnId/h:BICFI',namespaces=n):errors.append(party+' identification')
 return errors

def route_gaps(required, evidence, bundle):
 # Coverage under an original fictional release contract; not statistical assurance.
 observed={x['route'] for x in evidence if x['bundle']==bundle and x['passed']}
 return sorted(set(required)-observed)

def main:
 parser=argparse.ArgumentParser;parser.add_argument('--output',required=True);args=parser.parse_args
 schema=Path(__file__).with_name('head.001.001.04.xsd')
 if hashlib.sha256(schema.read_bytes).hexdigest!=SCHEMA_SHA:raise SystemExit('Unexpected schema bytes; investigate before updating the pin.')
 exe=os.environ.get('DR_XMLLINT') or shutil.which('xmllint')
 if not exe:raise SystemExit('xmllint is required; no schema checks were performed.')
 version=subprocess.run([exe,'--version'],capture_output=True,text=True,check=True)
 env={**os.environ,'XML_CATALOG_FILES':'','SGML_CATALOG_FILES':''}
 root=ET.fromstring(schema.read_bytes);x='{http://www.w3.org/2001/XMLSchema}'
 assert not any(root.findall(x+name) for name in ('import','include','redefine'))
 fr='<Fr><FIId><FinInstnId><BICFI>DRXXZZ00</BICFI></FinInstnId></FIId></Fr>'
 to='<To><FIId><FinInstnId><BICFI>DRYYZZ00</BICFI></FinInstnId></FIId></To>'
 nozone=BASE.replace('10:00:00Z','10:00:00');othermsg=BASE.replace(MSG,'not-a-message-definition')
 emptyid=BASE.replace('<BICFI>DRXXZZ00</BICFI>','')
 cases=[
 ('baseline',BASE,True),
 ('required sender absent',BASE.replace(fr,''),False),
 ('sender receiver sequence reversed',BASE.replace(fr+'\n'+to,to+'\n'+fr),False),
 ('empty message identity',BASE.replace('DR-HEADER-001',''),False),
 ('thirty six character identity',BASE.replace('DR-HEADER-001','A'*36),False),
 ('thirty five character identity',BASE.replace('DR-HEADER-001','A'*35),True),
 ('message identifier is only bounded text',othermsg,True),
 ('invalid calendar date',BASE.replace('2026-10-03','2026-02-30'),False),
 ('timezone absent at base schema',nozone,True),
 ('prefix alias',re.sub(r'<(/?)([A-Za-z][A-Za-z0-9]*)',r'<\1h:\2',BASE).replace('xmlns=','xmlns:h='),True),
 ('different header namespace',BASE.replace('head.001.001.04','head.001.001.03'),False),
 ('both sender choice branches',BASE.replace('</Fr>','<OrgId/></Fr>'),False),
 ('empty financial institution at base schema',emptyid,True),
 ('malformed BIC shape',BASE.replace('DRXXZZ00','bad'),False),
 ('unexpected root child',BASE.replace('</AppHdr>','<Extra/></AppHdr>'),False),
 ('lax signature structure is not verification',BASE.replace('</AppHdr>','<Sgntr><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"/></Sgntr></AppHdr>'),True),
 ('signature wrong namespace',BASE.replace('</AppHdr>','<Sgntr><Signature xmlns="urn:dr:wrong"/></Sgntr></AppHdr>'),False),
 ('boolean true',BASE.replace('</AppHdr>','<PssblDplct>true</PssblDplct></AppHdr>'),True),
 ('boolean yes',BASE.replace('</AppHdr>','<PssblDplct>yes</PssblDplct></AppHdr>'),False),
 ('external code length five',BASE.replace('</BICFI>','</BICFI><ClrSysMmbId><ClrSysId><Cd>ZZZZZ</Cd></ClrSysId><MmbId>DR-MEMBER</MmbId></ClrSysMmbId>',1),True),
 ('external code length six',BASE.replace('</BICFI>','</BICFI><ClrSysMmbId><ClrSysId><Cd>ZZZZZZ</Cd></ClrSysId><MmbId>DR-MEMBER</MmbId></ClrSysMmbId>',1),False),
 ('sender choice absent',BASE.replace(fr,'<Fr/>'),False),
 ]
 results=[]
 with tempfile.TemporaryDirectory(prefix='dr-bah-schema-') as td:
 for index,(name,xml,expected) in enumerate(cases):
 p=Path(td)/f'case-{index:02}.xml'p.write_text(xml)
 run=subprocess.run([exe,'--nonet','--noout','--schema',str(schema),str(p)],env=env,capture_output=True,text=True,timeout=10)
 if run.returncode not in (0,3):raise AssertionError((name,'unexpected tool failure',run.returncode,run.stderr))
 check('XSD '+name,run.returncode==0,expected)
 results.append(dict(name=name,xml=xml,expectedValid=expected,exitCode=run.returncode,diagnostic=run.stderr.replace(td,'<temporary>'),sha256=hashlib.sha256(xml.encode).hexdigest))
 check('custom profile baseline',profile_errors(BASE),[])
 check('custom profile requires timezone',profile_errors(nozone),['explicit timezone'])
 check('custom profile binds message identifier',profile_errors(othermsg),['message contract'])
 check('custom profile requires sender identifier',profile_errors(emptyid),['Fr identification'])
 routes=['A-in','A-out','B-in','B-out']
 evidence=[dict(route=p,bundle='release-1',passed=True) for p in routes]
 check('complete route evidence',route_gaps(routes,evidence,'release-1'),[])
 check('outbound gap despite other routes',route_gaps(routes,evidence[:-1],'release-1'),['B-out'])
 check('duplicate route cannot fill gap',route_gaps(routes,evidence[:-1]+[evidence[0]],'release-1'),['B-out'])
 check('changed bundle needs applicable evidence',route_gaps(routes,evidence,'release-2'),sorted(routes))
 failed=[{**e,'passed':e['route']!='A-in'} for e in evidence]
 check('failed route stays uncovered',route_gaps(routes,failed,'release-1'),['A-in'])
 # Fictional 90 minute window; reserve 15 rollback +10 reconciliation.
 check('latest safe rollback decision minute',90-15-10,65)
 check('remaining after decision at minute 70',90-70,20)
 check('late decision cannot fit reserved recovery',90-70>=15+10,False)
 check('candidate at minute 60 leaves five minute reserve',90-60-15-10,5)
 activation=lambda tested,approved,contract,staffed:all((tested,approved,contract,staffed))
 check('tests alone do not authorize activation',activation(True,False,True,True),False)
 check('agreed fictional activation conditions',activation(True,True,True,True),True)
 output=dict(scope='Actual local XSD validation of original synthetic BAH headers against pinned public head.001.001.04; custom profile and release arithmetic only. No full-message, MDR, community, directory, signature verification, settlement or real cutover acceptance.',python=sys.version.split[0],validator=(version.stdout+version.stderr).strip,schemaSha256=SCHEMA_SHA,runnerSha256=hashlib.sha256(Path(__file__).read_bytes).hexdigest,passed=len(checks),checks=checks,schemaCases=results)
 Path(args.output).write_text(json.dumps(output,indent=2)+'\n');print(json.dumps({'passed':len(checks),'schemaCases':len(cases),'schemaSha256':SCHEMA_SHA}))
if __name__=='__main__':main
IN PRACTICE

Case: the same header passes XSD and fails two teaching-profile conditions. The report must preserve both observations and identify the responsible rule.

Common pitfalls

Green schema result treated as full acceptance; signature presence treated as authenticity; text patterns treated as directories; tool failure treated as functional rejection.

Related topics: Messages, headers, and states · Versions, extensions, and interoperability · Testing, diagnosis, and operations

Take this idea with you

Pin artifacts and execute positive and negative boundaries while separating structure, profile, semantics, authenticity, and business effects.

Create account

Reference: BusinessApplicationHeaderV04 XSD · BigSavant ISO20022 fundamentals professional assessment2026.10

ISO 20022 is a trademark of the International Organization for Standardization (ISO). bigsavant.com is an independent preparation platform and is not affiliated with, associated with, sponsored, authorised or endorsed by ISO. Content and questions are original, are not official exam questions, and completing our tests does not award or guarantee any certification. Names are used only to identify the subject. All other trademarks belong to their respective owners.