ISO 20022: pain.001
2022-12-31 by Evrim Ă–ztamur

ISO 20022: pain.001

I briefly referred to pain.001 in my project article for batch2sepa, and there are some overlaps (see: copy-pasted sections) between that article and this one. Nevertheless, the focus for this article is about using ISO 20022 in the real world.

By the end of this article, you will have a solid understanding of the core (i.e. required) components of a pain.001 file and will hopefully be able to generate ones yourself to use for your own business!

I should also make it clear that none of these are trade secrets per se. Despite having worked with the standard a lot over the past few years and contorted to the wills of various banks all across Europe (who occasionally interpret standards as one does a Jackson Pollock piece) the core specification maps quite well to what you see on your bank’s web portal when making payments manually.

My intention here is to demystify the lengthy ISO specifications by following the same example file I built when I started batch2sepa (and still do), explaining various parts as we go deeper through the XML tree.

Let’s get started!

What is ISO 20022 and why should I care?

ISO 20022 is a series of message formats, declared with XML schemas, to be used in data exchange between financial institutions.

It covers a very large variety of actions such as CorporateActionMovementConfirmation002 (‘sent by an account servicer to an account owner or its designated agent to confirm the posting of securities or cash as a result of a corporate action event’). It’s truly comprehensive, but most of it is of little use to us mortals.

If you are in Europe, you may have seen pages from your bank referring to ‘batch payments’ where idea is to make a lot of payments at once with the fewest number of clicks possible.

pain.001.001 (001), which is the CustomerCreditTransferInitiation message, enables us to declare transfers from the debtor account (i.e. you) to the creditor (e.g. your accountant or office landlord).

Most banks in the EU accept XML files in this format to be uploaded freely by their clients to make payments in bulk.

This standard is by no means new, and many European bookkeeping tools already support this, and deeper integrations (Exact Online, a popular one in the Netherlands, directly submits these files to your bank in your name after establishing an authorized connection) exist too.

What does it look like?

Here is an abridged sample file provided by ABN Amro, a Dutch bank (helpful!):

<?xml version="1.0" encoding="UTF-8"?>
<Document
    xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <CstmrCdtTrfInitn>
        <GrpHdr>
            {{ Group Header }}
        </GrpHdr>
        <PmtInf>
            {{ Payment Information }}
            <CdtTrfTxInf>
                {{ Transaction Information }}
            </CdtTrfTxInf>
            <CdtTrfTxInf>...</<CdtTrfTxInf>
            <CdtTrfTxInf>...</<CdtTrfTxInf>
            ...
        </PmtInf>
    </CstmrCdtTrfInitn>
</Document>

The namespace we’re dealing with for this Document is urn:iso:std:iso:20022:tech:xsd:pain.001.001.03. We immediately notice two identifiers there that we already saw before, iso:20022 and pain.001.001.03 (the 03 is a version number, talk about semver!).

I excluded the contents for certain sections for brevity here to not overfill your screen. These are: Group Header (GrpHdr), Payment Information (PmtInf), and Credit Transfer Transaction Information (CdtTrfTxInf). Who named these tags, I don’t know, but it looks exactly like how I used to name variables in Java when I started programming with it at the age of 10.

How do I fill it in?

These three sections are the main components of a 001 file. Let’s go through each one of them and see what we’re supposed to fill in all these fields. It looks daunting when you see it all together, but breaking it down makes it much more approachable.

Text fields

Before we proceed, it’s important to clarify that despite the files are encoded in UTF-8, the contents of text fields are moderated. The allowed characters are [A-Za-z0-9] for alphanumeric basis, plus /*-?:().,'+ and space (U+0020). Unless stated otherwise, assume max. 35 characters for them.

Group Header (GrpHdr)

The purpose of this header section is to identify the file and include checksums. Very simple.

<MsgId>000001</MsgId>
<CreDtTm>2012-05-02T14:52:09</CreDtTm>
<NbOfTxs>1</NbOfTxs>
<CtrlSum>386.00</CtrlSum>
<InitgPty>
    <Nm>Client, Inc.</Nm>
    <OrgId>
        <Othr>
            <Id>AAA12345</Id>
        </Othr>
    </OrgId>
</InitgPty>

MsgId is the MessageIdentification (text) field. You may use a format such as M.YYYY-MM-DD.RRRRRR where YYYY-MM-DD is a date such as 2022-12-31, and RRRRRR is a random alphanumeric string. I recommend the latter component as you may have erroneous files that you will have to re-upload, and some banks are known to complain about repeat use of codes (as a part of deduplication efforts, makes sense). There are similar ID fields showing up later, M. is a prefix for this field specifically. Again, this is a non-standard value and you can use anything unique.

CreDtTm is the CreationDateTime (date-time) field, it’s an ISO 8601 timestamp without a time zone, e.g., 2022-12-31T10:02:55. No Z at the end!

NbOfTxs is the NumberofTransactions (integer) field , and CtrlSum is the ControlSum (decimal) field. Although this example will include only one transaction, in the spirit of batching payments, multiple transactions are certainly possible. The control sum is the total of all transactions in the entire file.

InitgPty is the InitiatingParty (text) field, its first-level child is Nm the Name (text) field, which is the name of the debtor (the party making the payments). The OrgId field is usually optional, but it is bank-specific. It contains an OrganizationIdentification value provided by your bank in their required format (i.e. it doesn’t have to be OrgId.Othr.Id per se) to match the file against your account internally, used as a form of a second confirmation atop your IBAN (which you will supply next). I added this one despite ABN Amro not requiring it, as I encountered many Irish banks, for example, doing so.

Payment Information (PmtInf)

Without going too much into detail and to keep this article simple, pretend that there can only be one PmtInf in a single 001 file. The specification declares this as a [1..n] tag, which means that you need to have at least one, but can have as many as you want. For practical purposes, just one.1

To refresh your memory, the following fields are first-level on the PmtInf tag:

<PmtInf> // [1..n]
    {{ Payment Information }} // We are here!
    <CdtTrfTxInf> // Also [1..n]!
        {{ Transaction Information }}
    </CdtTrfTxInf>
    <CdtTrfTxInf>...</<CdtTrfTxInf>
    <CdtTrfTxInf>...</<CdtTrfTxInf>
    ...
</PmtInf>

The following tags are essentially headers on the payment instruction itself:

<PmtInf>
    <PmtInfId>12345</PmtInfId>
    <PmtMtd>TRF</PmtMtd>
    <NbOfTxs>1</NbOfTxs>
    <CtrlSum>386.00</CtrlSum>
    <PmtTpInf>
        <SvcLvl>
            <Cd>SEPA</Cd>
        </SvcLvl>
    </PmtTpInf>
    <ReqdExctnDt>2012-05-02</ReqdExctnDt>
    <Dbtr>
        <Nm>Debtor</Nm>
    </Dbtr>
    <DbtrAcct>
        <Id>
            <IBAN>NL10ABNA1234567890</IBAN>
        </Id>
    </DbtrAcct>
    <DbtrAgt>
        <FinInstnId>
            <BIC>ABNANL2A</BIC>
        </FinInstnId>
    </DbtrAgt>
    <ChrgBr>SLEV</ChrgBr>
    ...

PmtInfId is the PaymentInformationIdentification (text) field, same notes as MsgId. You can use a prefix such as PI. for this instead.

PmtMtd is the PaymentMethod field and is always TRF. Check your bank specification for further information.

NbOfTxsand CtrlSum work in the same way as described above, but for this level’s transactions only.1

PmtTpInf is the PaymentTypeInformation field and contains the SEPA declaration as shown above. This is always SEPA for in-EU payments, however your bank may accept various other codes.2 Check your bank specification for further information.

ReqdExctnDt is the RequestedExecutionDate (date) field. It’s also in ISO 8601, e.g., 2022-12-31. This field is used by your bank to decide the day on which the funds will be withdrawn from your account and sent to the creditors. The date on which you upload the document does not have to be the same as the execution date.

Dbtr is the same as the InitgPty field for our purposes.1 If you provided an OrgId before, you may omit it in this field.

DbtrAcct is the DebtorAccount (IBAN) field. It contains an IBAN as the value of DbtrAcct.Id.IBAN. The IBAN must be [A-Z0-9], that is all capital letters and no spaces.3 DbtrAgt is the DebtorAgent (BIC) field, it contains the BIC to identify your own bank in the SWIFT network. As usual, check your bank specification for further information.

ChrgBr is the ChargeBearer (text) field. It contains a code telling your bank how the transaction costs should be shared between the debtor and the creditor. For SEPA payments, there are no associated fees and the value is always SLEV.4

Transaction Information (CdtTrfTxInf)

Finally, the money! The transaction information tags contain the individual payment details that our batch consists of. It includes the bank information of the creditor, the amount to be transferred, and the payment reference text. As mentioned before, this is a [1..n] tag, meaning that you can include as many as you want and have the money to pay for.

<CdtTrfTxInf>
    <PmtId>
        <EndToEndId>Our reference: 123456</EndToEndId>
    </PmtId>
    <Amt>
        <InstdAmt Ccy="EUR">386.00</InstdAmt>
    </Amt>
    <Cdtr>
        <Nm>Creditor</Nm>
    </Cdtr>
    <CdtrAcct>
        <Id>
            <IBAN>NL91RABO1234567890</IBAN>
        </Id>
    </CdtrAcct>
    <CdtrAgt>
        <FinInstnId>
            <BIC>RABONL2U</BIC>
        </FinInstnId>
    </CdtrAgt>
    <RmtInf>
        <Ustrd>Ref. 2012.0386</Ustrd>
    </RmtInf>
</CdtTrfTxInf>

PmtId is the PaymentIdentification (text) field, same notes as MsgId. You can use a prefix such as P. for this instead. It can also, as a first-level tag, optionally include InstrId, which is also a text field. The exact handling of these two tags is a bit ambiguous, as from my experience neither is exposed to the creditor. EndToEndId field is usually enforced to not having any spaces in it too, but this may differ from bank to bank.

Amt is the Amount field, and it contains the InstdAmt InstructedAmount (decimal) field with the attribute Ccy="EUR" attribute to declare the currency. The currency is usually enforced to be always EUR for SEPA payments, but it can be any other ISO 4217 3-letter currency code (e.g. USD, GBP, CHF).

Cdtr is the same as the Dbtr field, but it contains the name of the creditor instead. Same with the CdtrAcct and CdtrAgt v.s. DbtrAcct and DbtrAgt.

RmtInf is the RemittanceInformation field, and it contains the Ustrd Unstructured (text) field for the description of the payment. You may include any message here with a max. 140 character limit (a la Twitter?), usually the invoice numbers you’re paying.

Ta-da!

That’s about it for all the main components of a 001 file. Just copy-pasting the above and replacing the contents of the fields with the correct details should already lead you to a valid credit transfer file and allow you to make payments.

How can I generate one?

batch2sepa still uses the same method for generating 001 files, which is an XML file that contains the basic file with templating tags for Jinja2. Jinja is quite powerful and you don’t necessarily need something at that calibre, even Go’s html/template package will suffice without much pre-processing in place.

This should go without saying, but I made this mistake myself: I highly recommend sanitising your inputs, as although some banks are generous with their validation criteria, yours may not be. Formatting your IBAN/BIC fields correctly, declaring empty fields as required and not omitting them, and stripping non-ASCII characters are common sense, and you should put in the effort.

This is especially important if the script or tool you’ll be building will be used by people who can’t read the XML exports to guess where the errors might be coming from, and aren’t programmers (or have no access to the source to make changes anyway).

The danger of outputting malformed XMLs and making incorrect payments is not too real, on the other hand. Unless you make blatant mistakes like simply including the wrong IBAN, there are few things that can go wrong. Make sure you use ‘.’ instead of ‘,’ for your decimals if you’re coming from one of those weird countries.5

Further details and YOUR_BANK’s quirks

If you are interested in learning more about 001, the XMLdation website has some more information available on it, including XSD (XML Schema Definition) files for reference.

In order to validate your XML files for free, you can use the Mobilefish validator that uses similar XSDs and point out basic errors in your files. This will not catch all issues with it though, and your bank may have more stuff to complain about and print cryptic errors to misguide you with.

So, I heavily recommend (again!) referring to your bank’s own specification documents.

For example, ABN Amro in the Netherlands provides a supplemental document and reference XML files on their website for SEPA payments. Such supplemental documents will help you figure out what to do when a field is empty, how to make payments outside of the EU/EEA, and various rejection criteria.

Rabobank also has extensively annotated format description documents matching the original ISO specification, which make debugging your exports much, much easier.

My experience working with 001 files showed me that it’s a rather well-designed specification, and despite its occasional gotchas it most certainly is a testament to what solid open banking standards can look like (not looking at you, PSD2).

Have fun, and pay safe!


  1. There are several reasons for having multiple PmtInf tags. Most commonly encountered ones are using a single batch to make payments from multiple bank accounts (which is why the GrpHdr does not specify much, the meat and potatoes are all in PmtInf), or having multiple execution dates (you may want to pay some creditors sooner and others later). ↩︎ ↩︎ ↩︎

  2. Such as NURG (Non-urgent Payment), URGP (Urgent Payment), SDVA (Same Day Value), and SEPA (Single European Payments Area). These alternative codes are for banks which use 001 files also for making international payments, and the exact implementation details will usually be supplied by them. Rabobank, for example, uses the non-SEPA codes for their Wereldbetalingen (international payments) product, and using these codes require you to supply additional tags elsewhere in the document for supplementary information, like purpose codes (i.e. the reason for this payment, like taxes (TAXS), gas bill (GASB), or commodities (CMDT)). Certain recipient countries expect these codes to be filled in, and your bank will validate them for you. ↩︎

  3. The exact pattern is [A-Z]{2,2}[0-9]{2,2}[a-zA-Z0-9]{1,30} if you would like to validate. If you’re using Python, I highly recommend the python-stdnum package, which includes IBAN/BIC validators. ↩︎

  4. For international payments, your bank may require alternatives such as creditor (CRED), debtor (DEBT), or shared (SHAR). I have no clue what SLEV stands for, ‘service level’? ↩︎

  5. I am half-Turkish and half-Belgian, both of which use commas for their decimals. I think it cancels out to a period. ↩︎