Validate ISO 20022 CBPR+ XML messages against SWIFT usage (business) rules.
Project description
cbpr-usage-rules
Validate ISO 20022 CBPR+ XML messages (the SWIFT cross-border payments profile) against the Usage / Business Rules that apply on top of the XSD schemas. These are the rules that the schema alone cannot express: cross-field constraints, cross-schema checks between the Business Application Header (BAH) and the message Document, conditional presence, code-list restrictions, and field-format algorithms (IBAN, LEI, BIC, country, currency).
Rules are versioned per year (currently 2025 and 2026) and organised by message type (pacs.008, pacs.009 and its COV/ADV variants, pacs.002, pacs.004, pain.001, camt.052, camt.054, and the STP variants).
This package is AI generated. The rule logic was derived, with review, from the published CBPR+ usage-guideline spreadsheets. Treat it as an aid, not as a substitute for the official SWIFT specifications. Verify against the source material before relying on results in production.
Installation
pip install cbpr-usage-rules
Requires Python 3.9+ and depends only on lxml.
Quick start
As a library
import cbpr_rules
result = cbpr_rules.validate_file("payment.xml", year=2025)
if result["valid"]:
print("Compliant")
else:
for v in result["violations"]:
print(f"{v['rule_number']} line {v['line']} {v['xpath']}")
print(f" {v['description']}")
validate_string is the same but takes XML text:
result = cbpr_rules.validate_string(xml_text, year=2026)
From the command line
# Human-readable (default). Exit code is non-zero if there are violations.
cbpr-validate payment.xml --year 2025
# Compact, one finding per line (compiler-style) — for tools and coding agents
cbpr-validate payment.xml --year 2025 --format compact
# JSON output (full machine-readable result)
cbpr-validate payment.xml --year 2025 --json # same as --format json
# Read from stdin
cat payment.xml | cbpr-validate --year 2026
# Also list the advisory (non-enforced) rules in full
cbpr-validate payment.xml --year 2025 --advisory
# Additionally schema-validate against an XSD (repeatable, results shown separately)
cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
Each reported violation shows the rule, why this instance failed (Problem:),
the offending XML from your file (Found:), and its line + xpath (At:).
For tools and coding agents, prefer --format compact (one finding per line,
parseable like a compiler/linter) or --json (the full structured result):
$ cbpr-validate payment.xml --year 2025 --format compact
payment.xml:7: violation [pacs.008:R2] From vs Instructing Agent: ABCDGB2LXXX != GGGGGB2LXXX | at /AppHdr/Fr/FIId/FinInstnId/BICFI
payment.xml:63: violation [pacs.008:VAL-IBAN] invalid IBAN: 'GB29NWBK60161331926818' | at /Document/.../DbtrAcct/Id/IBAN
INVALID: 5 violations (87 rules)
Each line is file:line: severity [rule] message | at xpath, and the final
VALID: / INVALID: line is a stable summary to grep for. With --xsd, schema
errors appear as schema-error lines and are counted in the summary.
Advisory rules are summarised as a count by default; pass --advisory to list them.
Example messages
For every supported message type the package bundles a minimum and a maximum example message — both pre-verified to pass the usage rules and the CBPR+ XSDs:
- min — only the mandatory fields (smallest valid message);
- max — every XSD-visible field populated (one representative occurrence; the richest single legal combination, since some fields are mutually exclusive).
# Print to stdout (full AppHdr + Document, wrapped in <Envelope>)
cbpr-validate --example max --year 2025 --type pacs.008
cbpr-validate --example min --year 2026 --type pacs.009_cov > template.xml
# Override the wrapper tag (default: Envelope)
cbpr-validate --example max --year 2025 --type pacs.008 --wrapper BusinessMessage
import cbpr_rules
xml = cbpr_rules.example_message(2025, "pacs.008", "max") # variant defaults to "max"
cbpr_rules.example_variants(2025, "pacs.008") # ['min', 'max']
The data is fictitious (synthetic, generated BICs/IBANs/LEIs) and does not reproduce any real sample — use them as starting templates, not real payments.
Generating identifiers
The example values above are produced by a small deterministic ID generator
(the "Counting Strings" algorithm — no random/uuid, same seed → same output).
Every generated id is structurally valid and passes the tool's own validators.
cbpr-validate --generate iban --country AT --seed s1 # ISO 13616 + mod-97
cbpr-validate --generate lei --seed acme # ISO 17442 + check digits
cbpr-validate --generate bic --bank JSBP --country GB # -> JSBPGB..XXX
cbpr-validate --generate uuid --count 3 # RFC 4122 v4 (UETR)
cbpr-validate --generate mid --country GB # message identifier
from cbpr_rules import generate_iban, generate_lei, generate_bic, generate_uetr
generate_iban("DE", seed="acme") # deterministic, valid German IBAN
generate_bic(country="GB", seed="acme")
IBAN generation supports the 27 EU countries plus GB.
The result object
Both validate_file and validate_string return a dictionary:
{
"valid": False, # True only if there are no VIOLATION-severity findings
"message_type": "pacs.008", # the rule set that was applied
"detected_message_type": "pacs.008",# auto-detected from the Document namespace
"year": 2025,
"rules_evaluated": 87,
"violations": [
{
"rule_number": "pacs.008:R41", # unique within the message type
"name": "CBPR_Interbank_Settlement_Currency_FormalRule",
"description": "The codes XAU, XAG, XPD and XPT are not allowed ...",
"detail": "commodity currency 'XAU' not allowed", # why this instance was flagged
"found": "<IntrBkSttlmAmt Ccy=\"XAU\">1000.00</IntrBkSttlmAmt>", # the offending XML
"xpath": "/RequestPayload/Document/FIToFICstmrCdtTrf/CdtTrfTxInf/IntrBkSttlmAmt",
"line": 39, # 1-based line in the source XML
"severity": "violation"
}
],
"advisory": [ # textual guidance that cannot be mechanically enforced
{"rule_number": "pacs.008:R4", "name": "...", "description": "..."}
]
}
Every violation carries the four required pieces of information: the xpath, the line number, a description, and a unique rule number.
Mid-level usage
Choosing the year
Pass year=2025 or year=2026. Rule loading is per-year and lazy — only the
requested year's modules are imported. You can validate against more than one
year in the same process simply by calling with different year values.
Message type detection and variants
The message type is auto-detected from the <Document> namespace, so you
normally don't pass it. Business variants that share a base namespace — STP
(pacs.008/pacs.009), COV and ADV (pacs.009) — cannot be told apart from
the XML alone, so select them explicitly when you want their stricter rule sets:
cbpr_rules.validate_file("cover.xml", year=2025, msgtype="pacs.009_cov")
cbpr-validate cover.xml --year 2025 --type pacs.009_cov
Wrapper tags
Messages arrive inside different envelopes (<RequestPayload>, <DataPDU>,
SWIFT SAA wrappers, and so on). The validator locates the AppHdr and
Document elements wherever they sit in the tree and ignores the surrounding
wrapper, so the same input validates identically regardless of envelope. Element
matching is by local name, so namespace prefixes never matter.
Severity
violation— a usage rule is broken; this makesvalidfalse.info— advisory guidance surfaced for awareness; it never fails validation and appears in the separateadvisorylist (textual rules that cannot be mechanically checked).
In-depth usage
Discovering the rule set
cbpr_rules.available(2025) # -> ['camt.052', 'pacs.008', ...]
cbpr_rules.list_rules(2025, "pacs.008") # -> [{rule_number, name, description, severity, enforced}, ...]
cbpr_rules.list_rules(2025, "pacs.008", enforced_only=True) # only the enforceable rules
cbpr_rules.list_rules(2025, "pacs.008", with_xpaths=True) # + "xpaths" each rule affects
cbpr-validate --year 2025 --list-types
cbpr-validate --year 2025 --type pacs.008 --list # all rules
cbpr-validate --year 2025 --type pacs.008 --enforced # enforceable rules only
The --list / --enforced output (and with_xpaths=True) annotates each rule with
the concrete fields it affects, so you can see exactly which element/attribute a
rule constrains — handy when several fields share a name (e.g. many Ctry fields, but
a rule targets only one):
pacs.008:VAL-IBAN - CBPR_Valid_IBAN
//IBAN
pacs.008:VAL-BIC - CBPR_Valid_Agent_BIC
/Document/FIToFICstmrCdtTrf/CdtTrfTxInf/InstgAgt/FinInstnId/BICFI
pacs.008:R41 - CBPR_Interbank_Settlement_Currency_FormalRule
/Document/FIToFICstmrCdtTrf/CdtTrfTxInf/IntrBkSttlmAmt/@Ccy
Paths are derived by running each rule against the bundled example messages
(best-effort): concrete element paths and @attribute targets where present, or a
//Name wildcard for a datatype rule whose element isn't in the examples.
Algorithmic field validation
Beyond the published rules, the following formats are validated with their standard algorithms wherever the corresponding fields appear:
| Field | Standard | Check |
|---|---|---|
| IBAN | ISO 13616 | structure + mod-97 check digits |
| LEI | ISO 17442 | structure + ISO 7064 mod-97-10 check |
| BIC | ISO 9362 | 8/11-char structure + valid country |
| Country | ISO 3166-1 alpha-2 | membership |
| Currency | ISO 4217 | membership |
The reference code lists are vendored, so validation never makes a network call.
Some of these check-digit algorithms and reference code lists were derived from Wikipedia (e.g. IBAN, LEI, and the ISO 3166 country codes). Verify against the authoritative ISO/registry sources before relying on them in production.
Optional XSD schema validation
You can additionally validate a message against one or more XSD schemas as a separate second result set. XSD files are not bundled with the package — supply your own path(s):
result = cbpr_rules.validate_file("payment.xml", year=2025, xsd="pacs.008.001.08.xsd")
# or several: xsd=["head.001.001.02.xsd", "pacs.008.001.08.xsd"]
cbpr-validate payment.xml --year 2025 --xsd pacs.008.001.08.xsd
Each XSD is auto-matched by its targetNamespace: a message schema validates the
Document, a head.001 schema validates the AppHdr. When (and only when) an XSD
is supplied, the result gains a separate top-level xsd block, and the CLI prints
a distinct XSD SCHEMA VALIDATION section:
"xsd": {
"checked": True,
"schema_valid": False, # all supplied schemas passed?
"schemas": [
{
"file": "pacs.008.001.08.xsd",
"target_namespace": "urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08",
"validated_element": "Document", # or "AppHdr" / "root"
"valid": False,
"errors": [{"message": "...", "line": 82, "xpath": "/Document/.../CdtrAgt"}]
}
]
}
Usage-rule results and schema results stay separate: the top-level valid reflects
only the usage rules, while schema validity is xsd.schema_valid. The CLI exit
code is non-zero if either the usage rules or the schema fail. With no --xsd,
nothing about XSD appears in the output.
Handling errors
validate_file / validate_string raise cbpr_rules.engine.ValidationError
for unparseable XML or when the message type cannot be determined and was not
supplied. The CLI turns these into a message on stderr and exit code 2.
CLI exit codes
| Code | Meaning |
|---|---|
| 0 | Valid (no violations) |
| 1 | Invalid (one or more violations) |
| 2 | Usage error / could not validate |
How the rules are organised
Each (year, message type) has a hand-authored Python module under
cbpr_rules/rules/y<year>/. Rules are built from a small library of reusable
combinators (presence, conditional presence, mutual exclusion, value matching,
length, code lists, address grace-period rules) plus bespoke functions for
cross-field and cross-schema logic. The XSD files in the source material are
informational only and are not reimplemented here.
Enforced vs advisory
Every published rule is registered. A rule is enforced (can produce a violation) when it can be checked deterministically from the message alone — this includes the formal pseudo-code rules and the mechanizable textual rules, such as:
- header ↔ message consistency (
MsgDefIdrmatches the Document definition;BizMsgIdrcarries the GroupHeaderMsgId); - no structured Postal Address value duplicated in an
AddressLine; AnyBICpresent ⇒Name/PostalAddressnot allowed;- Structured Remittance ≤ 9,000 characters;
- charge rules where instructed and settlement amounts share a currency and differ, and amount-total-equals-sum checks.
A rule stays advisory (severity: info, surfaced as guidance, never failing
validation) when it cannot be verified from the message in isolation — for
example anything that refers to a related or underlying message not present
(Original_*, related-BAH, COV/ADV UETR/E2E), depends on the SWIFT network or a
jurisdiction, or expresses a recommendation / bilaterally-agreed practice.
Checks are deliberately conservative: each skips when its inputs are absent
or ambiguous, so a compliant message is never failed spuriously.
Licence
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cbpr_usage_rules-0.6.0.tar.gz.
File metadata
- Download URL: cbpr_usage_rules-0.6.0.tar.gz
- Upload date:
- Size: 149.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3d0dbdcdebcc03eabdae62e382f85dce3bc43d21a670ab1bd37bef52ee46a0db
|
|
| MD5 |
6c1ce650454f7da40de66ffbe4417ce9
|
|
| BLAKE2b-256 |
067f4b9845a04ee5bc31635ab13fc9dd2923bbf61beca4a94e6cc2fac8c9b007
|
File details
Details for the file cbpr_usage_rules-0.6.0-py3-none-any.whl.
File metadata
- Download URL: cbpr_usage_rules-0.6.0-py3-none-any.whl
- Upload date:
- Size: 215.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d943c3954d0af76daedca7051d2035de41f4e56eab821f63a1e4a6c11fb83e3
|
|
| MD5 |
b3ccb94ba192b23ff158bfeab1dc334b
|
|
| BLAKE2b-256 |
4ed9b4c66928712eff039eccde16314cf12da1c3ed62114536bf714105e0b8c8
|