Pain.Net 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Pain.Net --version 1.0.0
                    
NuGet\Install-Package Pain.Net -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Pain.Net" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Pain.Net" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Pain.Net" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Pain.Net --version 1.0.0
                    
#r "nuget: Pain.Net, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Pain.Net@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Pain.Net&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Pain.Net&version=1.0.0
                    
Install as a Cake Tool

Pain.Net

Maintained, tested, zero-dependency ISO 20022 SEPA payment-initiation for .NET. Generate and parse pain.001.001.09 (customer credit transfer) and pain.008.001.08 (customer direct debit), with NbOfTxs and CtrlSum computed for you on write and re-validated on read.

NuGet   MIT   Zero dependencies   Native AOT clean

Why

The long-time incumbent for SEPA XML in .NET, SepaWriter, has been abandoned since 2021. It is write-only, .NET Framework era, and no longer maintained. If you need to emit a payment file today you either fork a dead package or hand-roll System.Xml, and if you need to read a file back you are on your own.

Pain.Net is the maintained replacement: a small, tested, zero-dependency library that both writes and reads the two SEPA initiation messages, using only the in-box System.Xml.Linq. No transitive dependencies, Native-AOT clean, decimal money throughout.

Install

dotnet add package Pain.Net

Write a credit transfer (pain.001.001.09)

using PainNet;

var transfer = new CreditTransfer(
    MessageId: "MSG-001",
    CreationDateTime: new DateTimeOffset(2024, 3, 1, 9, 30, 0, TimeSpan.Zero),
    Debtor: new Party("Acme GmbH"),
    DebtorIban: "DE89370400440532013000",
    DebtorBic: "COBADEFFXXX",
    Transactions: new[]
    {
        new CreditTransferTransaction("E2E-1", 1234.56m, "EUR",
            new Party("Beta SARL"), "FR7630006000011234567890189", "BNPAFRPPXXX", "Invoice 1"),
        new CreditTransferTransaction("E2E-2", 78.90m, "EUR",
            new Party("Gamma BV"), "NL91ABNA0417164300", null, null),
    });

string xml = Pain.WriteCreditTransfer(transfer);

Write a direct debit (pain.008.001.08)

var debit = new DirectDebit(
    MessageId: "MSG-DD-001",
    CreationDateTime: new DateTimeOffset(2024, 3, 1, 9, 30, 0, TimeSpan.Zero),
    Creditor: new Party("Utility Co"),
    CreditorIban: "DE89370400440532013000",
    CreditorBic: "COBADEFFXXX",
    CreditorSchemeId: "DE98ZZZ09999999999",
    Transactions: new[]
    {
        new DirectDebitTransaction("DD-1", 42.00m, "EUR",
            new Party("Customer One"), "FR7630006000011234567890189", "BNPAFRPPXXX",
            MandateId: "MANDATE-1", MandateSignatureDate: new DateOnly(2023, 1, 10),
            RemittanceInfo: "Subscription"),
    });

string xml = Pain.WriteDirectDebit(debit);

Read one back

CreditTransfer parsed = Pain.ReadCreditTransfer(xml);

// Or non-throwing:
if (Pain.TryReadCreditTransfer(xml, out CreditTransfer? maybe))
{
    // maybe is populated and validated
}

The NbOfTxs / CtrlSum guarantee

Two fields cause more rejected SEPA files than any other: the transaction count (NbOfTxs) and the control sum (CtrlSum). If they disagree with the transactions actually in the file, the bank rejects the whole batch, and the bug is silent until then.

Pain.Net closes that gap from both sides:

  • On write, NbOfTxs and CtrlSum are always computed from the transactions themselves, in both the group header and each payment block. A caller-supplied total is never trusted, because there is no way to supply one.
  • On read, the stated NbOfTxs and CtrlSum are recomputed from the parsed transactions and compared. If either disagrees, ReadCreditTransfer throws PainValidationException (and TryReadCreditTransfer returns false). That mismatch is exactly the bug this catches.

All money is decimal, formatted invariant with two decimals and a Ccy attribute. CreationDateTime is caller-supplied and written as ISO 8601, so output is deterministic and testable.

Correctness

Every claim here is backed by the xUnit suite, which runs in seconds:

  • Credit-transfer writer produces one PmtInf, the right number of CdtTrfTxInf, the correct IBANs and amounts, and NbOfTxs / CtrlSum equal to the exact decimal sum.
  • Full round-trip: write then read recovers the message id, transaction count, and per-transaction end-to-end id, amount, and creditor IBAN.
  • Tamper tests: corrupting CtrlSum or NbOfTxs in valid XML makes the reader throw PainValidationException.
  • Direct-debit writer emits the right totals, mandate ids, signature dates, and SEPA SEPA / CORE codes.
  • Guard rails: empty MessageId, empty EndToEndId, empty MandateId, or a non-positive amount throw ArgumentException.
  • Structural: the root is Document in the pain.001.001.09 namespace URN with a CstmrCdtTrfInitn child.

Notes and limitations

  • Supports pain.001.001.09 and pain.008.001.08 only, on the SEPA profile (SvcLvl SEPA, direct debits CORE / RCUR, charge bearer SLEV).
  • The agents that SEPA makes mandatory are required and always emitted: the debtor and creditor BICs (DbtrAgt / CdtrAgt), and for direct debits the creditor identifier (CreditorSchemeId, written as CdtrSchmeId). Empty values are rejected with ArgumentException.
  • IBAN, BIC, and creditor identifier are passed through as strings and are not format-validated here beyond the non-empty check. Validate their structure with a dedicated library before serializing.
  • Amounts must have at most two decimal places; more are rejected with ArgumentException. Money is decimal throughout, formatted invariant with two decimals.
  • CreationDateTime is written with its UTC offset (yyyy-MM-ddTHH:mm:sszzz) and round-trips exactly; the reader also accepts the offset-less form.
  • The reader is provided for the credit-transfer message (ReadCreditTransfer / TryReadCreditTransfer), and validates both group-level and per-PmtInf NbOfTxs / CtrlSum; the direct-debit side is write-only in this release.
  • Other optional SEPA elements not modelled include ultimate parties and batch-booking flags. Add them if your bank requires them.
  • XML is produced schema-shaped in the correct element order with an explicit UTF-8 declaration and no byte-order mark; it is not validated against the official XSD at runtime.

License

MIT. Copyright Israel Iyonsi.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 86 8/21/2026
1.0.0 84 8/19/2026