Skip to main content

Pain001 is a Python Library for Automating ISO 20022-Compliant Payment Files Using CSV Data.

Project description

Pain001 logo

Pain001

Generate ISO 20022-compliant payment files from CSV, SQLite, JSON, or Parquet data.

PyPI version Python versions PyPI downloads Tests Coverage License


Contents

Getting started

Library reference

Operational


What is Pain001?

Banks reject malformed payment files. Pain001 takes the payment data you already have — a CSV export, a SQLite table, a JSON feed, a Parquet file — and turns it into ISO 20022 XML that validates against the official XSD schema before it ever reaches your bank.

It handles the parts that are easy to get wrong:

Concern How Pain001 handles it
Schema compliance Every file is validated against the official XSD before it is written
Monetary precision Amounts flow through decimal.Decimal end to end — no float rounding
Control totals NbOfTxs and CtrlSum are computed from the data, never trusted from input
Template drift Bundled template/XSD pairs are guard-railed; mismatches fail loudly
XML attacks All XML parsing goes through defusedxml — XXE and entity expansion are blocked
Large batches Streaming mode chunks input and emits one file per chunk
Scheme rules --scheme sepa-sct|sepa-sdd|sepa-inst|sepa-b2b|xborder-ct layers per-rulebook checks on top of XSD

Templates and schemas for every supported message type ship inside the package — point Pain001 at your data and it resolves the rest.


Install

Channel Command Notes
PyPI pip install pain001 Core library and CLI
PyPI + REST API pip install "pain001[api]" Adds FastAPI + Uvicorn server
PyPI + Parquet pip install "pain001[parquet]" Adds PyArrow for Parquet input
PyPI + Redis pip install "pain001[redis]" Distributed job store + rate limiter
PyPI + MCP pip install "pain001[mcp]" In-tree MCP server for LLM clients
PyPI + LSP pip install "pain001[lsp]" In-tree language server for CSV diagnostics
Source git clone https://github.com/sebastienrousseau/pain001 && cd pain001 && poetry install For development
Docker (GHCR) docker pull ghcr.io/sebastienrousseau/pain001:latest Multi-arch (linux/amd64, linux/arm64); CLI + api extra preinstalled

Requires Python 3.10 or later.

Docker

The image ships the CLI and the api extra so the REST surface works out of the box:

# CLI: generate a payment file
docker run --rm -v "$PWD:/data" -w /data \
  ghcr.io/sebastienrousseau/pain001:latest \
  generate -t pain.001.001.03 -d payments.csv -o out.xml

# REST API: launch the server
docker run --rm -p 8000:8000 \
  ghcr.io/sebastienrousseau/pain001:latest \
  serve --host 0.0.0.0 --port 8000

The image runs as a non-root pain001 user; bind-mount the directory you want the CLI to read or write.


Quick start

-t (message type) and -d (data file) are the only required flags — the template and XSD auto-resolve from the bundled registry:

pain001 -t pain.001.001.03 -d payments.csv
# -> writes pain.001.001.03.xml in the current directory (override with -o)

Override the template or schema only when you need a customised one:

pain001 -t pain.001.001.03 -m my-template.xml -s my-schema.xsd -d payments.csv

Validate without generating anything (CI pre-flight) — here the template and schema are auto-resolved from the bundled registry:

pain001 -t pain.001.001.03 -d payments.csv --dry-run
# -> exit 0 if the data would generate a valid file, 1 otherwise

Exit codes: 0 success, 1 validation or processing error, 2 invalid arguments.

One binary, a whole workflow

pain001 is a command suite. A bare invocation (or pain001 generate …) still produces XML exactly as before — every flag above is unchanged — and the sibling subcommands cover the rest of the lifecycle:

Command Purpose
pain001 generate … Generate payment XML (default; accepts bare flags for backwards compatibility)
pain001 validate -t … -d … Validate data without generating XML — a named --dry-run for CI pre-flight
pain001 versions [--json] List the supported ISO 20022 message types
pain001 inspect <type> [--json] Show a bundled template's schema, category, and accepted formats
pain001 init <type> [-o file] Scaffold a starter CSV from the bundled example
pain001 serve [--host --port] Launch the REST API (requires pain001[api])
pain001 mcp Launch the in-tree MCP server over stdio (requires pain001[mcp])
pain001 init pain.001.001.03 -o my-payments.csv         # scaffold a starter CSV
pain001 validate -t pain.001.001.03 -d my-payments.csv  # pre-flight in CI
pain001 generate -t pain.001.001.03 -d my-payments.csv  # ship it

Supported messages

Message type Description
pain.001.001.03pain.001.001.12 Customer Credit Transfer Initiation, all ten ISO 20022 versions
pain.008.001.02 Customer Direct Debit Initiation

Each bundled message type ships with a Jinja2 template, the official XSD schema, and registry metadata. List them from the CLI:

pain001 versions                 # supported message types
pain001 inspect pain.001.001.12  # template + schema + accepted formats

Related tooling included in the package:

  • Version migration — map payment data between pain.001 versions via pain001.migration.VersionMapper().migrate_rows(rows, from_v, to_v).
  • pain.002 parser + builder — read the payment status reports your bank sends back, and build_pain002_report(...) to generate one (e.g. to simulate a bank in tests); the two round-trip.
  • camt.053 parser — read end-of-day bank statements.

Input formats

Format Extension Notes
CSV .csv Header row maps columns to template fields
SQLite .db, .sqlite Reads from a named table you specify (set the table via --config)
JSON .json Array of payment objects
JSON Lines .jsonl One payment object per line
Parquet .parquet Requires the parquet extra

All loaders normalise into the same internal representation, so the rest of the pipeline — validation, totals, rendering — is identical regardless of source.


Usage

CLI reference

These are the options of the generate command (the default), so they apply equally to pain001 … and pain001 generate …:

pain001 [generate] [OPTIONS]

  -t, --xml-message-type   ISO 20022 message type (e.g. pain.001.001.03)
  -m, --template           Jinja2 XML template (auto-resolved when omitted)
  -s, --schema             XSD schema for validation (auto-resolved when omitted)
  -d, --data               Payment data file (CSV, SQLite, JSON, JSONL, Parquet)
  -c, --config             Configuration file (YAML, TOML, or INI)
  -o, --output-dir         Output directory (default: current directory)
      --dry-run            Validate inputs without generating XML
      --streaming          Process input in chunks, one XML file per chunk
      --chunk-size         Rows per streaming chunk (default: 1000)
      --profile            Configuration profile or built-in preset
      --show-config        Print the resolved configuration and exit
      --emit-metrics       Emit timing and lifecycle metrics to stdout
      --scheme             Validate rows against a scheme rulebook
                           (sepa-sct, sepa-sdd, sepa-inst, sepa-b2b, xborder-ct)
      --explain            With --scheme, print a remediation hint per finding
      --scheme-format      Scheme output format: text (default) or json
  -v, --verbose            Detailed logging output
  -h, --help               Show help and exit
Scheme-aware validation (SEPA + cross-border)

XSD validation proves a file is well-formed; it does not prove the payment obeys the rules of the scheme it will clear through. --scheme layers a rulebook on top of XSD validation and reports structured, per-row violations:

pain001 -t pain.001.001.03 -d payments.csv --scheme sepa-sct --dry-run

Five profiles ship today — sepa-sct (SEPA Credit Transfer, pain.001), sepa-sdd (SEPA Direct Debit, pain.008), sepa-inst (SEPA Instant Credit Transfer, pain.001), sepa-b2b (SEPA Business-to-Business Direct Debit, FRST/RCUR-only + mandatory creditor identifier), and xborder-ct (generic cross-border, multi-currency, BIC-mandatory). Each profile checks currency, valid debtor/creditor IBANs (ISO 13616 / mod-97), BICs, the amount ceiling (100,000 EUR instant cap for sepa-inst), ISO 20022 character-set and field-length limits, and (for SDD/B2B) mandate id and sequence type. Add --explain for remediation hints, or --scheme-format json for machine-readable output. The REST API accepts a scheme field on /api/v1/validate and /api/v1/generate too. See SCHEMES.md for the full rule catalogue. From Python:

from pain001 import validate_scheme

rows = [{
    "payment_currency": "USD",                       # not EUR -> SEPA-CCY
    "debtor_account_IBAN": "DE89370400440532013000",
    "creditor_account_IBAN": "FR1420041010050500013M02606",
    "payment_amount": "100.00",
}]

result = validate_scheme(rows, profile="sepa-sct")
print(result.is_valid)             # -> False
for v in result.violations:
    print(v.rule, v.field, v.message)
    # -> SEPA-CCY payment_currency Currency must be EUR for sepa-sct

Need to clean spreadsheet text first? sanitize_to_charset transliterates to the ISO 20022 set (CaféCafe).

Dry-run validation in CI

--dry-run runs the full validation pipeline — file existence, schema resolution, data loading, field checks — and stops before XML generation. It is designed as a pre-flight gate:

pain001 -t pain.001.001.03 -d payments.csv --dry-run || exit 1

Exit code 0 means the data would generate a valid file; 1 means it would not, with the failures printed.

Streaming large batches

For batches too large to hold in memory, streaming mode chunks the input and writes one XML file per chunk, each with its own computed NbOfTxs and CtrlSum:

pain001 -t pain.001.001.03 -d payments.csv --streaming --chunk-size 500
REST API

Install the api extra and start the server:

pip install "pain001[api]"
pain001 serve --host 0.0.0.0 --port 8000   # or: uvicorn pain001.api.app:app

Endpoints are versioned under /api/v1; the unversioned /api/* paths remain as a backwards-compatible alias.

Method Endpoint Purpose
GET /api/v1/health Liveness check
POST /api/v1/validate Validate payment data without generating
POST /api/v1/generate Generate a payment file synchronously
POST /api/v1/generate/async Queue generation as a background job
GET /api/v1/status/{job_id} Poll an async job
GET /api/v1/download/{job_id} Download a finished file
DELETE /api/v1/jobs/{job_id} Cancel or clean up a job

Operational controls (all environment-driven, all off by default):

Variable Effect
PAIN001_API_KEY Require Authorization: Bearer <key> on every endpoint
PAIN001_RATE_LIMIT Per-client cap (e.g. 100/minute); pair with PAIN001_RATE_LIMIT_BACKEND=redis for cross-replica enforcement
PAIN001_RATE_LIMIT_BACKEND memory (default, in-process) or redis
PAIN001_RATE_LIMIT_REDIS_URL Redis URL for the distributed limiter (falls back to PAIN001_JOB_STORE_URL if unset)
PAIN001_JOB_STORE_DIR Persist async jobs to disk so they survive restarts
PAIN001_JOB_STORE_URL Redis URL for a fully distributed job store (use instead of _DIR)

Documentation surfaces: Swagger UI at /api/docs, ReDoc at /api/redoc, an interactive Scalar reference at /api/reference, and the raw OpenAPI document at /openapi.json. The same reference is hosted publicly: https://sebastienrousseau.github.io/pain001/api-reference.html.

Operability: liveness probe at /api/v1/health and Prometheus metrics at /metrics (build info, supported-type/scheme gauges, per-status job gauges, and HTTP request counters). See OPERATIONS.md for the runbook — config, scrape config, alerts, scaling, and incident playbook.

Client SDKs — generate a typed client in any language from the OpenAPI document:

python scripts/export_openapi.py openapi.json      # dump the schema
npx @openapitools/openapi-generator-cli generate \
    -i openapi.json -g python -o ./pain001-client   # or -g typescript-axios, go, ...
Python API — generate in memory (serverless)

For Lambdas, APIs, and queues, generate_xml_string returns the validated XML as a string instead of writing to disk. This snippet is fully self-contained — it uses the template, schema, and sample data that ship inside the package, so it runs as-is with no external files:

from pain001 import generate_xml_string
from pain001.constants import TEMPLATES_DIR
from pain001.csv.load_csv_data import load_csv_data

message_type = "pain.001.001.03"
bundled = TEMPLATES_DIR / message_type  # templates ship inside the package

# Load the bundled sample dataset; swap in your own list[dict] of rows.
payments = load_csv_data(str(bundled / "template.csv"))

xml = generate_xml_string(
    payments,
    message_type,
    str(bundled / "template.xml"),
    str(bundled / f"{message_type}.xsd"),
)

# `xml` is validated ISO 20022 XML, ready to return from a handler.
print(xml[:38])  # -> <?xml version="1.0" encoding="UTF-8"?>
Python API — generate to a file

process_files loads your data, renders the template, validates against the XSD, and writes the file — returning the path it wrote:

from pain001.core.core import process_files

output_path = process_files(
    xml_message_type="pain.001.001.03",
    xml_template_file_path="template.xml",
    xsd_schema_file_path="schema.xsd",
    data_file_path="payments.csv",  # path, or a list[dict] of payment rows
)

print(output_path)  # -> "pain.001.001.03.xml" — validated and on disk

Companion packages

Pain001 ships two interchangeable install paths for both its MCP and LSP integrations: an in-tree implementation that comes with pain001 itself (smaller feature set, no extra package), and a standalone PyPI package (richer surface, independently versioned).

MCP server

A Model Context Protocol server lets AI agents call Pain001 as first-class tools.

  • In-tree (pip install "pain001[mcp]", run pain001 mcp or pain001-mcp-builtin): the original server in pain001.mcp.server. Tools include list_supported_versions, inspect_template, generate_payment_file, validate_payment_data, plus a pain001://schema/{message_type} resource and a build_payment_batch prompt.
  • Standalone (pip install pain001-mcp, run pain001-mcp): the pain001-mcp companion package — sixteen tools including everything in-tree plus validate_records, validate_identifier (IBAN/BIC), generate_message, generate_message_async, generate_message_from_file, list_supported_formats, parse_camt053, parse_pain002, migrate_records (cross-version pain.001 mapping), validate_xml_against_schema (in-memory XSD validation), and sanitize_to_iso20022_charset (ISO 20022 Latin transliteration).

Register either with any MCP client (e.g. Claude Desktop):

{
  "mcpServers": {
    "pain001": { "command": "pain001-mcp" }
  }
}

(Use pain001-mcp-builtin for the in-tree variant.)

Language Server (LSP)

A pygls-based Language Server brings real-time help to editors.

  • In-tree (pip install "pain001[lsp]", run pain001-lsp-builtin): diagnostics for payment CSV files (invalid IBAN/BIC/currency cells, characters outside the ISO 20022 Latin set, missing required columns).
  • Standalone (pip install pain001-lsp, run pain001-lsp): the pain001-lsp companion package — six features for payment-data JSON files: diagnostics, completion, hover, a multi-record "add missing required fields" code action, two-space JSON formatting (textDocument/formatting), and a record-outline pane (textDocument/documentSymbol). Supports startup (initializationOptions.messageType) and live (workspace/didChangeConfiguration) message-type overrides.

Point your editor's LSP client at the pain001-lsp (standalone) or pain001-lsp-builtin (in-tree) command for the appropriate file type.


When not to use Pain001

  • You need message types beyond pain.001 / pain.008 generation. The camt.053 and pain.002 modules are parsers, not generators; other ISO 20022 families (camt.052, pacs.*) are out of scope.
  • You need bank connectivity. Pain001 produces and validates files; it does not transmit them. Pair it with your EBICS/SFTP/API channel.
  • Your data model is wildly non-tabular. The loaders expect row-shaped payment records. Deeply nested custom structures need flattening first.

Development

git clone https://github.com/sebastienrousseau/pain001
cd pain001
poetry install --with dev

The quality model is zero-trust: every gate runs locally and in CI, and the build fails if any regress.

Target What it runs
make lint Ruff lint + format check + interrogate + pydoclint
make type mypy in --strict mode
make test Full pytest suite with branch-coverage gate
make sec Bandit + pip-audit dependency audit
make perf pytest-benchmark performance suite
make mutate Mutation testing via mutmut
make check lint + coverage + security in one pass
make tollgates Dependency, XSD, idempotency, and env-parity gates

CI workflows:

Workflow Purpose
ci.yml Test matrix on Python 3.10 / 3.11 / 3.12
quality.yml Lint, types, complexity
security.yml Bandit + pip-audit + dependency review
codeql.yml Static analysis
docker.yml Multi-arch GHCR image build + smoke test
sdk.yml OpenAPI SDK generation + drift guard
nightly.yml Extended nightly suite
pr.yml Pull-request gate
docs.yml Build and deploy documentation

Current state (v0.0.53): 1,264 tests passing, 100% line + branch coverage against a 100% enforced floor, mypy --strict clean, 100% docstring coverage (interrogate). Coverage excludes only entry-point guards and genuinely-defensive barriers via # pragma: no cover; everything else is exercised.


Security

Pain001 treats payment data as hostile until proven otherwise:

  • XML parsing is routed through defusedxml; XXE, billion-laughs, and external entity resolution are rejected.
  • Path handling goes through a path validator that blocks traversal outside permitted directories.
  • Schema validation is mandatory — output that does not validate against the official XSD is never written as a success.
  • Amounts are Decimal throughout; control sums are recomputed, not echoed from input.
  • Dependencies are pinned via poetry.lock and audited by pip-audit, Bandit, and CodeQL in CI.

To report a vulnerability, please use GitHub private vulnerability reporting rather than a public issue.


Documentation


Contributing

Contributions are welcome — see the contributing instructions, how the project is run in GOVERNANCE.md, the architecture map, and where the project is headed in the ROADMAP.md. Need help? See SUPPORT.md. Unless you explicitly state otherwise, any contribution you submit is dual-licensed as below, without additional terms or conditions.

Maintainers wanted. Pain001 has a single maintainer today; that is the project's main risk. If you rely on it and can help review, triage, or co-maintain an area, see becoming a maintainer.

Thanks to all the contributors who have helped build Pain001.


License

Licensed under either of

at your option. See CHANGELOG.md for release history.


pain001.com · docs.pain001.com · PyPI

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pain001-0.0.53.tar.gz (202.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pain001-0.0.53-py3-none-any.whl (299.7 kB view details)

Uploaded Python 3

File details

Details for the file pain001-0.0.53.tar.gz.

File metadata

  • Download URL: pain001-0.0.53.tar.gz
  • Upload date:
  • Size: 202.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pain001-0.0.53.tar.gz
Algorithm Hash digest
SHA256 feb668905ab7b333850c4e4236b933a15249e0cfb91ea98f816710e10f08e0d8
MD5 45329c71a96a4e5de08c6bb7108d7160
BLAKE2b-256 7ab25e7f0560287af308a27687f838b553d1a78479e2ae5ef77121da500af464

See more details on using hashes here.

Provenance

The following attestation bundles were made for pain001-0.0.53.tar.gz:

Publisher: ci.yml on sebastienrousseau/pain001

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pain001-0.0.53-py3-none-any.whl.

File metadata

  • Download URL: pain001-0.0.53-py3-none-any.whl
  • Upload date:
  • Size: 299.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pain001-0.0.53-py3-none-any.whl
Algorithm Hash digest
SHA256 fb93dc84c80c891f482b2b3ea4283a6b24ebf1882d91ddc29b9b7be47558d435
MD5 8157318d84dcd009f22416b9fe856978
BLAKE2b-256 b4634eac5d176417a1a7f4cb774ca15da39dc215cbd048a9729b621af83b1ae6

See more details on using hashes here.

Provenance

The following attestation bundles were made for pain001-0.0.53-py3-none-any.whl:

Publisher: ci.yml on sebastienrousseau/pain001

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page