Kolos Hetyéssy Software developer

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.

What it does

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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 field file
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 sepaxml library (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.

Read the full privacy notice →

Free tools with this engine

Free toolSEPA & NACHA Payment File API

pain.001 validator

Check a pain.001 or pain.008 file against the ISO 20022 schema and the EPC rules, with every problem explained.

Free toolSEPA & NACHA Payment File API

pain.001.001.03 to .09

Migrate 2009 SEPA files to the 2019 versions, checked against the new schema and the SEPA rules.

Free toolSEPA & NACHA Payment File API

NACHA file validator

Check a NACHA ACH file record by record, with each problem's line, column and a plain explanation.

Free toolSEPA & NACHA Payment File API

CSV to NACHA

Turn a payroll or vendor CSV into a NACHA file that passes every check before it goes to the bank.

Free toolSEPA & NACHA Payment File API

ACH return codes explained

Every ACH return and notification-of-change code in plain words, with what to do next.

REST API Live on RapidAPI (free plan)

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