Products / Finance & accounting / REST API
SEPA & NACHA Payment File API
Generate, validate and convert SEPA payment files and US NACHA (ACH) files, and read the bank's answer. Every file is checked against the format's rules, with each problem explained in plain words.
JSON · CSV · pain.001 · pain.008 · pain.002 · NACHA → pain.001.001.09 · pain.008.001.08 · NACHA · validation report
Try it free with your own file → No sign-up. Your file is never stored.
SEPA and ACH live on RapidAPI (free plan) Questions or a format you need? Write to me
See it work
Pick a sample, or open the last tab and try it with your own file. Every result here is the engine's real output, nothing is mocked or edited.
A pain.001.001.09 credit transfer generated by the API: structured addresses, one invoice reference and one RF creditor reference. Every check passes.
pain001-v09-valid.xml 1469 bytes · pain.001.001.09
Input file
<?xml version="1.0" ?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09">
<CstmrCdtTrfInitn>
<GrpHdr>
<MsgId>PAYRUN-2026-10-23</MsgId>
<CreDtTm>2026-10-21T09:00:00</CreDtTm>
<NbOfTxs>2</NbOfTxs>
<CtrlSum>1284.06</CtrlSum>
<InitgPty>
<Nm>Acme GmbH</Nm>
</InitgPty>
</GrpHdr>
<PmtInf>
<PmtInfId>PAYRUN-2026-10-23-1</PmtInfId>
<PmtMtd>TRF</PmtMtd>
<NbOfTxs>2</NbOfTxs>
<CtrlSum>1284.06</CtrlSum>
<PmtTpInf>
<SvcLvl>
<Cd>SEPA</Cd>
</SvcLvl>
</PmtTpInf>
<ReqdExctnDt>
<Dt>2026-10-23</Dt>
</ReqdExctnDt>
<Dbtr>
<Nm>Acme GmbH</Nm>
<PstlAdr>
<TwnNm>Berlin</TwnNm>
<Ctry>DE</Ctry>
</PstlAdr>
</Dbtr>
<DbtrAcct>
<Id>
<IBAN>DE89370400440532013000</IBAN>
</Id>
</DbtrAcct>
<DbtrAgt>
<FinInstnId>
<BICFI>COBADEFFXXX</BICFI>
</FinInstnId>
</DbtrAgt>
<ChrgBr>SLEV</ChrgBr>
<CdtTrfTxInf>
<PmtId>
<EndToEndId>INV-2026-17</EndToEndId>
</PmtId>
<Amt>
<InstdAmt Ccy="EUR">1234.56</InstdAmt>
</Amt>
<Cdtr>
<Nm>Supplier BV</Nm>
<PstlAdr>
<TwnNm>Amsterdam</TwnNm>
<Ctry>NL</Ctry>
</PstlAdr>
</Cdtr>
<CdtrAcct>
<Id>
<IBAN>NL91ABNA0417164300</IBAN>
</Id>
</CdtrAcct>
<RmtInf>
<Ustrd>Invoice 2026-17</Ustrd>
</RmtInf>
</CdtTrfTxInf>
<CdtTrfTxInf>
<PmtId>
<EndToEndId>NOTPROVIDED</EndToEndId>
</PmtId>
<Amt>
<InstdAmt Ccy="EUR">49.50</InstdAmt>
</Amt>
<Cdtr>
<Nm>Office Supplies KG</Nm>
</Cdtr>
<CdtrAcct>
<Id>
<IBAN>DE02120300000000202051</IBAN>
</Id>
</CdtrAcct>
<RmtInf>
<Strd>
<CdtrRefInf>
<Tp>
<CdOrPrtry>
<Cd>SCOR</Cd>
</CdOrPrtry>
</Tp>
<Ref>RF18539007547034</Ref>
</CdtrRefInf>
</Strd>
</RmtInf>
</CdtTrfTxInf>
</PmtInf>
</CstmrCdtTrfInitn>
</Document>
Validation result
VALID 0 errors · 0 warnings
- PAYRUN-2026-10-23
- pain.001.001.09
- Payment blocks
- 1
- Transactions
- 2
- Total
- 1284.06 EUR
No problems found. Schema, SEPA character set, totals, IBANs, addresses and references all pass.
JSON response POST /v1/validate
{
"message_type": "pain.001.001.09",
"kind": "credit_transfer",
"profile": "epc",
"valid": true,
"error_count": 0,
"warning_count": 0,
"summary": {
"message_id": "PAYRUN-2026-10-23",
"payment_blocks": 1,
"transactions": 2,
"total_amount": "1284.06",
"currency": "EUR"
},
"issues": []
}
A 2009 pain.001.001.03 file whose header control sum has two digits swapped. A German bank may reject the whole file for this.
pain001-v03-wrong-total.xml 1815 bytes · pain.001.001.03
Input file
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03">
<CstmrCdtTrfInitn>
<GrpHdr>
<MsgId>OLD-2026-10-20-1</MsgId>
<CreDtTm>2026-10-20T10:00:00</CreDtTm>
<NbOfTxs>2</NbOfTxs>
<CtrlSum>1248.06</CtrlSum>
<InitgPty>
<Nm>Acme GmbH</Nm>
<Id><OrgId><BICOrBEI>COBADEFFXXX</BICOrBEI></OrgId></Id>
</InitgPty>
</GrpHdr>
<PmtInf>
<PmtInfId>OLD-2026-10-20-1-P1</PmtInfId>
<PmtMtd>TRF</PmtMtd>
<BtchBookg>true</BtchBookg>
<NbOfTxs>2</NbOfTxs>
<CtrlSum>1284.06</CtrlSum>
<PmtTpInf><SvcLvl><Cd>SEPA</Cd></SvcLvl></PmtTpInf>
<ReqdExctnDt>2026-10-23</ReqdExctnDt>
<Dbtr>
<Nm>Acme GmbH</Nm>
<PstlAdr><Ctry>DE</Ctry><AdrLine>Hauptstr. 1</AdrLine><AdrLine>10115 Berlin</AdrLine></PstlAdr>
</Dbtr>
<DbtrAcct><Id><IBAN>DE89370400440532013000</IBAN></Id></DbtrAcct>
<DbtrAgt><FinInstnId><BIC>COBADEFFXXX</BIC></FinInstnId></DbtrAgt>
<ChrgBr>SLEV</ChrgBr>
<CdtTrfTxInf>
<PmtId><EndToEndId>INV-2026-17</EndToEndId></PmtId>
<Amt><InstdAmt Ccy="EUR">1234.56</InstdAmt></Amt>
<CdtrAgt><FinInstnId><BIC>INGBNL2A</BIC></FinInstnId></CdtrAgt>
<Cdtr><Nm>Supplier BV</Nm></Cdtr>
<CdtrAcct><Id><IBAN>NL91ABNA0417164300</IBAN></Id></CdtrAcct>
<RmtInf><Ustrd>Invoice 2026-17</Ustrd></RmtInf>
</CdtTrfTxInf>
<CdtTrfTxInf>
<PmtId><EndToEndId>NOTPROVIDED</EndToEndId></PmtId>
<Amt><InstdAmt Ccy="EUR">49.50</InstdAmt></Amt>
<Cdtr><Nm>Office Supplies KG</Nm></Cdtr>
<CdtrAcct><Id><IBAN>DE02120300000000202051</IBAN></Id></CdtrAcct>
<RmtInf><Ustrd>Order 5521</Ustrd></RmtInf>
</CdtTrfTxInf>
</PmtInf>
</CstmrCdtTrfInitn>
</Document>
Validation result
INVALID 1 error · 1 warning
- OLD-2026-10-20-1
- pain.001.001.03
- Payment blocks
- 1
- Transactions
- 2
- Total
- 1284.06 EUR
ERROR · CONTROL_SUM GrpHdr/CtrlSum The message control sum is 1248.06; the amounts add up to 1284.06.
WARNING · OLD_VERSION pain.001.001.03 is the 2009 version. Banks are moving to the 2019 versions (pain.001.001.09 / pain.008.001.08); German banks' specification limited the old version to November 2025. Use /v1/convert to migrate the file.
JSON response POST /v1/validate
{
"message_type": "pain.001.001.03",
"kind": "credit_transfer",
"profile": "epc",
"valid": false,
"error_count": 1,
"warning_count": 1,
"summary": {
"message_id": "OLD-2026-10-20-1",
"payment_blocks": 1,
"transactions": 2,
"total_amount": "1284.06",
"currency": "EUR"
},
"issues": [
{
"severity": "error",
"rule": "CONTROL_SUM",
"path": "GrpHdr/CtrlSum",
"message": "The message control sum is 1248.06; the amounts add up to 1284.06."
},
{
"severity": "warning",
"rule": "OLD_VERSION",
"path": "",
"message": "pain.001.001.03 is the 2009 version. Banks are moving to the 2019 versions (pain.001.001.09 / pain.008.001.08); German banks' specification limited the old version to November 2025. Use /v1/convert to migrate the file."
}
]
}
A pain.008.001.02 direct debit with a mistyped creditor identifier and a mandate dated in the future.
pain008-v02-errors.xml 1663 bytes · pain.008.001.02
Input file
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.008.001.02">
<CstmrDrctDbtInitn>
<GrpHdr>
<MsgId>DD-2026-10-20-1</MsgId>
<CreDtTm>2026-10-20T10:00:00</CreDtTm>
<NbOfTxs>1</NbOfTxs>
<CtrlSum>25.00</CtrlSum>
<InitgPty><Nm>Sportverein Musterstadt e.V.</Nm></InitgPty>
</GrpHdr>
<PmtInf>
<PmtInfId>DD-2026-10-20-1-RCUR</PmtInfId>
<PmtMtd>DD</PmtMtd>
<NbOfTxs>1</NbOfTxs>
<CtrlSum>25.00</CtrlSum>
<PmtTpInf>
<SvcLvl><Cd>SEPA</Cd></SvcLvl>
<LclInstrm><Cd>CORE</Cd></LclInstrm>
<SeqTp>RCUR</SeqTp>
</PmtTpInf>
<ReqdColltnDt>2026-11-02</ReqdColltnDt>
<Cdtr><Nm>Sportverein Musterstadt e.V.</Nm></Cdtr>
<CdtrAcct><Id><IBAN>DE89370400440532013000</IBAN></Id></CdtrAcct>
<CdtrAgt><FinInstnId><BIC>COBADEFFXXX</BIC></FinInstnId></CdtrAgt>
<ChrgBr>SLEV</ChrgBr>
<CdtrSchmeId>
<Id><PrvtId><Othr><Id>DE98ZZZ09999999990</Id><SchmeNm><Prtry>SEPA</Prtry></SchmeNm></Othr></PrvtId></Id>
</CdtrSchmeId>
<DrctDbtTxInf>
<PmtId><EndToEndId>MEMBER-0001-2026-11</EndToEndId></PmtId>
<InstdAmt Ccy="EUR">25.00</InstdAmt>
<DrctDbtTx><MndtRltdInf><MndtId>M-001</MndtId><DtOfSgntr>2026-12-01</DtOfSgntr></MndtRltdInf></DrctDbtTx>
<DbtrAgt><FinInstnId><Othr><Id>NOTPROVIDED</Id></Othr></FinInstnId></DbtrAgt>
<Dbtr><Nm>Anna Schmidt</Nm></Dbtr>
<DbtrAcct><Id><IBAN>DE02120300000000202051</IBAN></Id></DbtrAcct>
<RmtInf><Ustrd>Membership November</Ustrd></RmtInf>
</DrctDbtTxInf>
</PmtInf>
</CstmrDrctDbtInitn>
</Document>
Validation result
INVALID 2 errors · 1 warning
- DD-2026-10-20-1
- pain.008.001.02
- Payment blocks
- 1
- Transactions
- 1
- Total
- 25.00 EUR
ERROR · CREDITOR_ID PmtInf[1]/CdtrSchmeId/Id/PrvtId/Othr/Id 'DE98ZZZ09999999990' is not a valid SEPA creditor identifier.
ERROR · MANDATE_DATE PmtInf[1]/DrctDbtTxInf[1]/DrctDbtTx/MndtRltdInf/DtOfSgntr The mandate signature date is in the future.
WARNING · OLD_VERSION pain.008.001.02 is the 2009 version. Banks are moving to the 2019 versions (pain.001.001.09 / pain.008.001.08); German banks' specification limited the old version to November 2025. Use /v1/convert to migrate the file.
JSON response POST /v1/validate
{
"message_type": "pain.008.001.02",
"kind": "direct_debit",
"profile": "epc",
"valid": false,
"error_count": 2,
"warning_count": 1,
"summary": {
"message_id": "DD-2026-10-20-1",
"payment_blocks": 1,
"transactions": 1,
"total_amount": "25.00",
"currency": "EUR"
},
"issues": [
{
"severity": "error",
"rule": "CREDITOR_ID",
"path": "PmtInf[1]/CdtrSchmeId/Id/PrvtId/Othr/Id",
"message": "'DE98ZZZ09999999990' is not a valid SEPA creditor identifier."
},
{
"severity": "error",
"rule": "MANDATE_DATE",
"path": "PmtInf[1]/DrctDbtTxInf[1]/DrctDbtTx/MndtRltdInf/DtOfSgntr",
"message": "The mandate signature date is in the future."
},
{
"severity": "warning",
"rule": "OLD_VERSION",
"path": "",
"message": "pain.008.001.02 is the 2009 version. Banks are moving to the 2019 versions (pain.001.001.09 / pain.008.001.08); German banks' specification limited the old version to November 2025. Use /v1/convert to migrate the file."
}
]
}
The live demo needs JavaScript, which seems to be switched off. The sample tabs show the engine's real output without it.
Free: 5 runs a day, no sign-up. Files that cannot be read do not count.
What it does
-
Generate from JSON or CSV
Send the account holder and the payments as JSON, or as a CSV with a header row. You get `pain.001.001.09` (credit transfers) or `pain.008.001.08` (direct debits, CORE or B2B, grouped by sequence type). A file is returned only if it passes every check.
-
Validate with plain explanations
`pain.001.001.03` / `.09` and `pain.008.001.02` / `.08` are checked against the ISO 20022 XSD (with line numbers) and the EPC customer-to-PSP implementation guidelines 2025. Each problem comes with its rule, its location and a plain explanation.
-
German bank profile
With `profile=de`, the German banks' rules (DFÜ-Abkommen Anlage 3 v3.7) are checked too: the wider character set (ÄÖÜäöüß & *), no byte order mark, UTF-8 only.
-
Convert 2009 files to 2019
`pain.001.001.03` → `.09` and `pain.008.001.02` → `.08`. Nothing is guessed: the result must pass the 2019 schema and the SEPA rules.
-
Read the bank's answer
A `pain.002` status report (any version) becomes a list of accepted and rejected payments, with the reason codes in plain language.
-
Totals recomputed exactly
`NbOfTxs` and `CtrlSum` are recomputed on the group and on every payment block. Amounts are never rounded: an amount with more than 2 decimals is refused.
-
US NACHA (ACH) files
Generate PPD, CCD, WEB and TEL files from JSON or CSV, and validate any NACHA file: 94-character records, routing check digits, trace numbers, entry hash, batch and file totals, each problem with its line and column.
-
ACH returns explained
A return or notification-of-change file becomes a list: each R or C code in plain words, what to do next, and the corrected account data. Entries can also be listed as JSON or CSV.
Specification
- Generate
- JSON or CSV →
pain.001.001.09·pain.008.001.08(CORE / B2B) - Validate
pain.001.001.03/.09·pain.008.001.02/.08- Convert
pain.001.001.03→.09·pain.008.001.02→.08- Status reports
pain.002(any version)- Rule sets
- ISO 20022 XSD · EPC implementation guidelines 2025 · optional German profile (DFÜ-Abkommen Anlage 3 v3.7)
- Checks
- SEPA character set · IBAN and creditor identifier check digits ·
NbOfTxs/CtrlSum· EUR only, 0.01–999,999,999.99, at most 2 decimals · names ≤ 70 · one unstructured or one structured remittance · service level, charge bearer, payment method · CORE or B2B, sequence type, mandate date · 2019 addresses · duplicate end-to-end IDs and possible duplicate payments (warnings) - Endpoints
POST /v1/generate·POST /v1/validate·POST /v1/convert·POST /v1/status·GET /v1/formats- ACH (NACHA)
- PPD · CCD · WEB · TEL generated; any NACHA file validated (full rules for PPD, CCD, WEB, TEL, CTX, COR)
- ACH checks
- 94-character records, printable ASCII, record order, blocks of 10 · file header (routing check digit) · service class 200 / 220 / 225 · transaction codes, prenotes · routing check digit · addenda count and sequence · trace numbers unique and ascending · entry hash, debit and credit totals, block count · effective date (weekends, Federal Reserve holidays) · Same Day ACH limit · 2026 descriptions PAYROLL / PURCHASE · possible duplicates (warnings) · optional: balanced batches
- ACH endpoints
POST /v1/ach/generate·POST /v1/ach/validate·POST /v1/ach/parse(JSON or CSV) ·POST /v1/ach/returns·GET /v1/ach/codes- Input
- Raw body (
application/xml, ACH:text/plain), base64, or a multipart fieldfile - Limits
- 10 MB per request
- Safety
- XML safety (no DTDs or entities); files are processed in memory and never stored
- Engine
- Deterministic, no AI or LLM. The same input always gives the same output.
A request and its answer
Validate a file with the German profile:
curl -X POST "https://<host>/v1/validate?profile=de" -H "Content-Type: application/xml" --data-binary @payments.xml
The answer for an old pain.001.001.03 file whose control sum was changed by hand:
{
"message_type": "pain.001.001.03",
"kind": "credit_transfer",
"profile": "de",
"valid": false,
"error_count": 1,
"warning_count": 1,
"summary": {
"message_id": "OLD-2026-10-20-1",
"payment_blocks": 1,
"transactions": 2,
"total_amount": "1284.06",
"currency": "EUR"
},
"issues": [
{
"severity": "error",
"rule": "CONTROL_SUM",
"path": "GrpHdr/CtrlSum",
"message": "The message control sum is 91284.06; the amounts add up to 1284.06."
},
{
"severity": "warning",
"rule": "OLD_VERSION",
"path": "",
"message": "pain.001.001.03 is the 2009 version. Banks are moving to the 2019 versions (pain.001.001.09 / pain.008.001.08); German banks' specification limited the old version to November 2025. Use /v1/convert to migrate the file."
}
]
}
Text outside the profile’s character set is converted when a file is generated, as EPC and the German banks
recommend (ü → ue, ß → ss, & → +, other accents removed), and reported in a CHARACTERS_CONVERTED warning.
Errors have one shape, {"error": {"code", "message", "issues"?}}, for example MALFORMED_XML, UNSAFE_XML,
UNSUPPORTED_FORMAT, INVALID_PAYMENT (with the issues) and CONVERSION_NOT_POSSIBLE.
ACH (NACHA)
The ACH endpoints are live on RapidAPI, on the same plan as SEPA. Validate a NACHA file:
curl -X POST "https://<host>/v1/ach/validate" -H "Content-Type: text/plain" --data-binary @payroll.ach
One issue from the answer for a file with a mistyped routing number:
{
"severity": "error",
"rule": "ROUTING_CHECK_DIGIT",
"line": 3,
"field": "check_digit",
"column": "12",
"found": "5",
"expected": "4",
"message": "231380105 is not a valid routing number: with these first 8 digits the check digit would be 4."
}
POST /v1/ach/generate builds one batch (PPD, CCD, WEB or TEL) from JSON or from entries_csv, and returns the file
only if it passes the validator. POST /v1/ach/returns explains every R and C code in a return or
notification-of-change file. See the free tools: NACHA file validator,
CSV to NACHA, ACH return codes explained.
Not affiliated with Nacha.
How it was tested
- Every rule has a test that breaks a valid file in exactly that way.
- Files produced by the independent open-source
sepaxmllibrary (pain.001.001.03 / .09, pain.008.001.02 / .08) pass the validator with no errors, and both tools agree on the totals. - Fuzzing: 36,000 mutated files, no crash.
- Speed: a 6 MB file with 20,000 transfers is validated in about 1.7 s.
- ACH: every rule has a test that breaks a valid file in exactly that way; 13 sample files from the independent Moov ACH project are valid for it too, and 5 broken ones are caught with the right rule; 30,000 mutated files, no crash; a 9.5 MB file (100,000 records) is validated in about 1 s.
- The container build runs the full test suite and fails if a single test fails.
Known limits
What it does not do yet, so you can decide before you integrate.
- No sending to banks (EBICS, bank APIs): out of scope.
- Bank-specific profiles other than German (Dutch, Austrian, Swiss SPS) are not included yet.
- The duplicate checks are warnings; a bank’s own duplicate detection may be stricter.
- The live demo on this page validates only; generating, converting and reading pain.002 are API calls.
- ACH: no IAT generation or IAT-specific rules, no CTX generation, no live routing directory lookup (a routing number with a valid check digit may still not exist).
- Not affiliated with Nacha. The ACH code explanations are our own summaries, not the text of the Nacha Operating Rules.
Privacy
Payment files are processed in memory and never stored. The response goes back to you and is not kept by the service.
Free tools with this engine
pain.001 validator
Check a pain.001 or pain.008 file against the ISO 20022 schema and the EPC rules, with every problem explained.
pain.001.001.03 to .09
Migrate 2009 SEPA files to the 2019 versions, checked against the new schema and the SEPA rules.
NACHA file validator
Check a NACHA ACH file record by record, with each problem's line, column and a plain explanation.
CSV to NACHA
Turn a payroll or vendor CSV into a NACHA file that passes every check before it goes to the bank.
ACH return codes explained
Every ACH return and notification-of-change code in plain words, with what to do next.
More in Finance & accounting
Bank Statement Reconciler
MT940 · MT942 · camt.052/053/054 · BAI2 · CODA · ZIP → JSON · CSV · hledger · Beancount · camt.053 · DATEV
Parses MT940, ISO 20022 camt, BAI2 and CODA files, normalizes every transaction and proves each statement adds up to the cent.
±0.00 every statement balance-checked to the cent