ISO 20022 message parsing for Elixir. Currently covers camt.053 (Bank to Customer Statement), the highest-demand message type. More message types are planned.
# mix.exs
def deps do
[
{:ex_iso20022, "~> 0.1"}
]
endxml = File.read!("statement.xml")
case ISO20022.Camt053.parse(xml) do
{:ok, doc} ->
Enum.each(doc.statements, fn stmt ->
IO.puts("Account IBAN : #{stmt.account.iban}")
IO.puts("Currency : #{stmt.account.currency}")
closing = Enum.find(stmt.balances, &(&1.type == :closing_booked))
IO.puts("Closing bal : #{closing.amount} #{closing.currency} (#{closing.credit_debit})")
Enum.each(stmt.entries, fn entry ->
IO.puts(" #{entry.ref} #{entry.credit_debit} #{entry.amount} #{entry.currency}")
end)
end)
{:error, reason} ->
IO.inspect(reason, label: "parse error")
endUsing the bang variant when you are confident the input is valid:
doc = ISO20022.Camt053.parse!(xml)| Module | Message | Versions |
|---|---|---|
ISO20022.Camt053 |
Bank to Customer Statement | camt.053.001.02 – 014 |
More message types (camt.052, camt.054, pain.001, pacs.008, …) will be added in
subsequent releases. The top-level ISO20022.parse/1 dispatcher is already in place
and will route to the right module automatically once each type is implemented.
The top-level struct returned by parse/1.
| Field | Type | Description |
|---|---|---|
group_header |
GroupHeader |
Message-level metadata |
statements |
[Statement] |
One entry per account per period |
| Field | Type | Description |
|---|---|---|
message_id |
String |
Unique message identifier (max 35 chars) |
created_at |
DateTime |
UTC creation timestamp |
pagination |
map | nil |
%{page_number: String, last_page: boolean} |
| Field | Type | Description |
|---|---|---|
id |
String |
Statement identifier |
electronic_seq_number |
integer | nil |
Sequence number for gap detection |
legal_seq_number |
integer | nil |
Legal sequence number (present alongside electronic seq in some setups e.g. Goldman Sachs) |
created_at |
DateTime | nil |
Statement generation time |
from_to_date |
map | nil |
%{from: DateTime, to: DateTime} |
account |
Account |
Account identification |
balances |
[Balance] |
At least opening and closing booked balances |
transactions_summary |
map | nil |
%{total_entries, total_credit_entries, total_debit_entries} (from <TxsSummry> when present) |
entries |
[Entry] |
Booked transactions |
| Field | Type | Notes |
|---|---|---|
iban |
String | nil |
Present when account is IBAN-identified |
other_id |
String | nil |
Present when account uses a non-IBAN scheme (common in US) |
other_scheme |
String | nil |
Scheme code, e.g. "BBAN" |
type |
String | nil |
Proprietary account type, e.g. "VIRTUAL", "VIRTUAL_SUSPENSE", "IBDA_DDA" |
currency |
String | nil |
ISO 4217 alpha code, e.g. "EUR" |
servicer_bic |
String | nil |
BIC of the account-servicing institution |
servicer_name |
String | nil |
Name of the account-servicing institution |
name |
String | nil |
Account name |
amount is always a positive Decimal. The sign is expressed through credit_debit.
| Field | Type | Description |
|---|---|---|
type |
atom | See balance types below |
amount |
Decimal |
Positive amount |
currency |
String |
ISO 4217 alpha code |
credit_debit |
:credit | :debit |
:debit means overdraft |
date |
Date |
Balance reference date |
Balance type atoms:
| Atom | ISO code | Meaning |
|---|---|---|
:opening_booked |
OPBD |
Opening booked (mandatory) |
:opening_available |
OPAV |
Opening available (common in US/virtual account setups) |
:closing_booked |
CLBD |
Closing booked (mandatory) |
:closing_available |
CLAV |
Closing available |
:interim_booked |
ITBD |
Interim booked |
:interim_available |
ITAV |
Interim available |
:forward_available |
FWAV |
Forward available |
{:other, code} |
any | Unrecognised code |
| Field | Type | Description |
|---|---|---|
ref |
String |
Bank-assigned entry reference |
amount |
Decimal |
Positive amount |
currency |
String |
ISO 4217 alpha code |
credit_debit |
:credit | :debit |
Direction |
reversal |
boolean |
true if this cancels a prior entry |
status |
:booked |
Always :booked in camt.053 |
booking_date |
Date | nil |
Date posted to account |
value_date |
Date | nil |
Value date (may differ from booking date) |
account_servicer_ref |
String | nil |
Bank's own reference |
bank_transaction_code |
BankTxCode | nil |
ISO 20022 transaction classification |
additional_info |
String | nil |
Free-text entry description |
details |
[EntryDetails] |
Transaction-level detail blocks (batch entries) |
| Field | Type | Example |
|---|---|---|
domain |
String | nil |
"PMNT" |
family |
String | nil |
"RCDT", "ICDT", "RDDT" |
sub_family |
String | nil |
"XBCT", "ESCT", "SALA" |
proprietary_code |
String | nil |
Bank-specific code |
proprietary_issuer |
String | nil |
Issuer of the proprietary code |
Present when an entry groups multiple underlying transactions (batch payments).
| Field | Type | Description |
|---|---|---|
batch |
map | nil |
%{message_id, payment_info_id, number_of_transactions, total_amount} |
transaction_details |
[TransactionDetails] |
Individual transaction records |
| Field | Type | Description |
|---|---|---|
refs |
map | nil |
%{message_id, end_to_end_id, uetr, …} |
amount |
Decimal | nil |
Individual transaction amount |
currency |
String | nil |
ISO 4217 |
credit_debit |
:credit | :debit | nil |
Direction |
related_parties |
map | nil |
%{debtor, creditor, ultimate_debtor, ultimate_creditor} |
related_agents |
map | nil |
%{debtor_agent, creditor_agent} |
remittance_info |
tuple / nil | {:unstructured, text} or {:structured, %{ref, ref_type, …}} |
purpose |
String | nil |
ISO 20022 purpose code |
{:ok, %ISO20022.Camt053.Document{}}
# Malformed XML
{:error, {:parse_error, reason}}
# Namespace not recognised as a camt.053 variant
{:error, {:unsupported_version, "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09"}}
# Mandatory field absent from an otherwise valid document
{:error, {:missing_required_field, [:group_header, :message_id]}}
{:error, {:missing_required_field, [:statements, 0, :id]}}
# Field present but value could not be parsed
{:error, {:invalid_amount, "N/A", [:statements, 0, :entries, 1, :amount]}}
{:error, {:invalid_date, "32-01-2024"}}The path in missing_required_field and invalid_* errors follows the struct
hierarchy so it is straightforward to pinpoint the problematic node.
Real-world bank files use a wide range of schema versions:
urn:iso:std:iso:20022:tech:xsd:camt.053.001.02 # many European banks
urn:iso:std:iso:20022:tech:xsd:camt.053.001.04 # UK, SEPA migration era
urn:iso:std:iso:20022:tech:xsd:camt.053.001.08 # current SWIFT / TARGET2
urn:iso:std:iso:20022:tech:xsd:camt.053.001.11 # newer implementations
urn:iso:std:iso:20022:tech:xsd:camt.053.001.14 # latest ISO (2026)
ex_iso20022 detects the version from the root element's xmlns attribute and
normalises to the same struct regardless of input version. All versions 02 – 14 are
accepted. Documents lacking a namespace (some older senders) are also accepted.
MIT