<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zero Lopp Labs</title>
    <description>The latest articles on DEV Community by Zero Lopp Labs (@zerolooplabs).</description>
    <link>https://dev.to/zerolooplabs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3810612%2Fa33b06b1-c0a7-48dd-9351-6a1e5398aa5a.png</url>
      <title>DEV Community: Zero Lopp Labs</title>
      <link>https://dev.to/zerolooplabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zerolooplabs"/>
    <language>en</language>
    <item>
      <title>Belgium's e-invoicing grace period ended. Here's the developer's playbook.</title>
      <dc:creator>Zero Lopp Labs</dc:creator>
      <pubDate>Tue, 28 Apr 2026 12:05:58 +0000</pubDate>
      <link>https://dev.to/zerolooplabs/belgiums-e-invoicing-grace-period-ended-heres-the-developers-playbook-44lk</link>
      <guid>https://dev.to/zerolooplabs/belgiums-e-invoicing-grace-period-ended-heres-the-developers-playbook-44lk</guid>
      <description>&lt;h2&gt;
  
  
  The grace period is over
&lt;/h2&gt;

&lt;p&gt;On 27 March 2026, Belgium's FPS Finance went on Radio 2 to confirm what most CFOs in Brussels were quietly dreading: the three-month tolerance period for B2B e-invoicing wouldn't be extended. Since 1 April 2026, the new rules are fully enforced.&lt;/p&gt;

&lt;p&gt;If your company sends invoices between Belgian VAT-registered businesses and you're still emailing PDFs, you're now exposed to a graduated penalty regime: €1,500 for the first offence, €3,000 for the second, €5,000 for the third — within a three-month escalation window. The Royal Decree of 8 July 2025 codified this in article 13ter of Royal Decree No. 1, with the penalty schedule in Royal Decree No. 44.&lt;/p&gt;

&lt;p&gt;This article isn't a legal explainer. It's a developer's playbook: what the law actually requires from your stack, why this lands on your engineering team and not just your finance department, and a working TypeScript path to compliance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the law actually requires
&lt;/h2&gt;

&lt;p&gt;Three things, in order of how often they trip up dev teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A structured electronic format.&lt;/strong&gt; PDFs are out. The mandate requires invoices in formats conforming to EN 16931 — primarily UBL 2.1 and CII 16B. Hybrid formats like Factur-X and ZUGFeRD are recognised for specific cases. In practice, for Belgium, you generate UBL 2.1 XML following the &lt;strong&gt;Peppol BIS Billing 3.0&lt;/strong&gt; specification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Peppol network as the default transmission channel.&lt;/strong&gt; You can't just email an XML file. Invoices flow through Peppol, a four-corner network of certified Access Points. Alternative channels (EDI, point-to-point) are allowed only by mutual agreement and only if they meet the same EN 16931 standards. The default Peppol track must always be available — your customer can require it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coverage scope.&lt;/strong&gt; All B2B transactions between Belgian-established VAT-registered taxpayers fall under the mandate. That includes foreign companies with a Belgian fixed establishment, and VAT groups. Out of scope: B2C, cross-border (until ViDA in 2030), and entities exclusively performing Article 44-exempt activities. Self-billing benefits from an extended tolerance until end of June 2026.&lt;/p&gt;

&lt;p&gt;Existing record-keeping rules still apply — but now to structured XML. Seven years of archival, machine-readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your engineering team owns this
&lt;/h2&gt;

&lt;p&gt;In most companies, e-invoicing was a finance problem. Print, sign, send. That's no longer true. The 2026 mandate is a systems integration problem first, and a finance problem second.&lt;/p&gt;

&lt;p&gt;You need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate UBL 2.1 XML that validates against EN 16931 schematron rules&lt;/li&gt;
&lt;li&gt;Look up your trading partners' Peppol identifiers&lt;/li&gt;
&lt;li&gt;Submit through a certified Access Point (you don't get to be one — that's a regulated entity with infrastructure requirements)&lt;/li&gt;
&lt;li&gt;Receive incoming invoices through the same network&lt;/li&gt;
&lt;li&gt;Handle status callbacks (accepted / rejected / errored) via webhooks&lt;/li&gt;
&lt;li&gt;Persist everything for seven years&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that lives on a finance team's laptop. It lives in your codebase, with your auth, your retry logic, your monitoring, your audit trail.&lt;/p&gt;

&lt;p&gt;If your invoicing today is an export-to-PDF function in your accounting page, the gap between "what you have" and "what the law requires" is a backend project, not a vendor selection.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4-corner Peppol model in plain English
&lt;/h2&gt;

&lt;p&gt;Peppol's architecture has four parties. Most articles describe it as a network. It's easier to think of it as a delivery contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ You ] → [ Your Access Point ] → [ Recipient's Access Point ] → [ Recipient ]
  C1            C2                          C3                       C4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C1 — You.&lt;/strong&gt; The supplier issuing the invoice. You generate UBL XML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C2 — Your Access Point.&lt;/strong&gt; A certified service provider that signs your message, validates it against Peppol BIS 3.0, and routes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C3 — Recipient's Access Point.&lt;/strong&gt; The buyer's certified provider, who validates and delivers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C4 — Recipient.&lt;/strong&gt; Your customer, whose accounting system receives the structured invoice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You only operate C1. C2 is a vendor choice — you connect to one Access Point, and that single connection gives you reach to every Peppol participant in the EU and beyond. The Access Point handles certificate management, dynamic discovery (looking up where to send a given recipient's invoice), and message-level acknowledgements.&lt;/p&gt;

&lt;p&gt;In Belgium, all in-scope businesses must be Peppol-capable, which in practice means having a contract with — or an integration into — a certified Access Point. Becoming an Access Point yourself is a multi-month project with annual fees, audits, and certificate authorities. Almost no SaaS does it directly.&lt;/p&gt;

&lt;p&gt;This is the same model that has run B2G invoicing in Belgium federally since 1 March 2024 (and regionally since 2017 for Flanders, 2020 for Brussels, 2022 for Wallonia), through the Mercurius platform. The 2026 mandate just extends the four corners to B2B. A five-corner variant arrives in 2028 for e-reporting — the fifth corner being FPS Finance itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  A TypeScript path to compliance
&lt;/h2&gt;

&lt;p&gt;Here's where &lt;a href="https://getpeppr.dev" rel="noopener noreferrer"&gt;getpeppr&lt;/a&gt; fits. We're a TypeScript-first wrapper around a certified Access Point, with the Stripe-style developer experience: a JSON object goes in, a Peppol-compliant invoice goes out.&lt;/p&gt;

&lt;p&gt;Install the SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @getpeppr/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialise the client. API keys are environment-prefixed — &lt;code&gt;sk_sandbox_...&lt;/code&gt; for test, &lt;code&gt;sk_live_...&lt;/code&gt; for production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Peppol&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@getpeppr/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;peppol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Peppol&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GETPEPPR_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send your first invoice. The shape mirrors the EN 16931 model — supplier and customer identification, line items with VAT rates, totals — but stays JSON-shaped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;peppol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INV-2026-001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme Corp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;peppolId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0208:BE9876543210&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;street&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123 Business Ave&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Brussels&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;postalCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Consulting services&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;vatRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Invoice sent! ID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, the SDK generates UBL 2.1 XML, validates it against Peppol BIS Billing 3.0 schematron rules, signs the envelope, and submits via a certified Access Point. You don't see the XML unless you ask for it.&lt;/p&gt;

&lt;p&gt;What's not shown above but matters in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Status updates via webhooks.&lt;/strong&gt; Peppol delivery is asynchronous. Your endpoint receives &lt;code&gt;invoice.accepted&lt;/code&gt;, &lt;code&gt;invoice.rejected&lt;/code&gt;, and &lt;code&gt;invoice.errored&lt;/code&gt; events with HMAC-SHA256 signature verification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Directory lookup.&lt;/strong&gt; Before sending, you usually want to verify the recipient is on the network. &lt;code&gt;peppol.directory.lookup("0208:BE...")&lt;/code&gt; returns capability metadata — country, registered name, supported document types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit notes and corrections.&lt;/strong&gt; All EN 16931 invoice types are supported (380 standard, 381 credit note, 383 debit, 386 prepayment, 389 self-billed). The same &lt;code&gt;.send()&lt;/code&gt; method handles them by setting &lt;code&gt;isCreditNote: true&lt;/code&gt; plus an &lt;code&gt;invoiceReference&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sandbox is free and unlimited. No real Peppol traffic is generated, but you get the full API surface, validation errors, and webhook events. When you switch the key prefix from &lt;code&gt;sk_sandbox_&lt;/code&gt; to &lt;code&gt;sk_live_&lt;/code&gt;, the only change in your code is the &lt;code&gt;apiKey&lt;/code&gt; value. Live pricing: Starter €49/mo (100 docs), Pro €149/mo (800 docs), Business €399/mo (2,000 docs), with overage from €0.20/doc.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's not in scope (yet)
&lt;/h2&gt;

&lt;p&gt;The 2026 Belgium mandate is intentionally narrow. If you operate beyond it, you have more time — but you're already on the same regulatory glide path.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;B2C transactions&lt;/strong&gt; stay outside the mandate. You can keep emailing PDFs to consumers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-border invoices&lt;/strong&gt; (intra-EU and extra-EU) are out of scope until the EU's "VAT in the Digital Age" (ViDA) Digital Reporting Requirements take effect on 1 July 2030.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Article 44-exempt entities&lt;/strong&gt; (mostly certain financial, medical, and educational services) don't have to issue structured e-invoices for their exempt activities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-billing&lt;/strong&gt; has an extended tolerance until end of June 2026 — that's two months from now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-existing invoicing fines&lt;/strong&gt; (late issuance, missing fields, incorrect VAT rounding) remain in force on top of the new technical-readiness penalty regime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Looking ahead, 1 January 2028 brings near-real-time e-reporting to FPS Finance via Peppol's five-corner model, replacing the annual customer listing. Your 2026 work is the foundation for it — same Access Point, same XML, plus a tax-authority recipient.&lt;/p&gt;

&lt;p&gt;If you're building B2B SaaS for the EU, the practical horizon isn't "Belgium 2026". It's "ViDA 2030". Belgium is operationalising now what the rest of the EU is preparing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;If you're a developer at a Belgian B2B SaaS, three things are worth doing this week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Map your invoice flow.&lt;/strong&gt; Where does PDF generation live in your codebase? How do you identify customers' VAT numbers? How do you persist invoices today? You need this before scoping any integration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify your customers' Peppol presence.&lt;/strong&gt; Many are already receiving invoices via Peppol from B2G suppliers since 2024. The Peppol Directory lookup is a one-line check.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Spin up a sandbox.&lt;/strong&gt; &lt;a href="https://getpeppr.dev" rel="noopener noreferrer"&gt;getpeppr.dev&lt;/a&gt; — free, unlimited, takes an afternoon to wire into a typical Node.js codebase. No call required, no demo to book. Just an API key.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The grace period is over. The first €1,500 fine could land on someone's desk this quarter.&lt;/p&gt;

</description>
      <category>peppol</category>
      <category>einvoicing</category>
      <category>typescript</category>
      <category>compliance</category>
    </item>
    <item>
      <title>SWIFT Is Killing MT940 — Here's How to Future-Proof Your Bank Statement Pipeline</title>
      <dc:creator>Zero Lopp Labs</dc:creator>
      <pubDate>Mon, 23 Mar 2026 17:06:11 +0000</pubDate>
      <link>https://dev.to/zerolooplabs/swift-is-killing-mt940-heres-how-to-future-proof-your-bank-statement-pipeline-267i</link>
      <guid>https://dev.to/zerolooplabs/swift-is-killing-mt940-heres-how-to-future-proof-your-bank-statement-pipeline-267i</guid>
      <description>&lt;p&gt;On November 22, 2025, SWIFT pulled the plug on legacy MT payment messages. Cross-border payments now run exclusively on ISO 20022's MX format.&lt;/p&gt;

&lt;p&gt;MT940 — the bank account statement format your reconciliation pipeline probably depends on — wasn't part of that first wave. It's a reporting message, not a payment message. But it's next. SWIFT has formally deprecated MT940, stopped maintaining it, and announced that disincentives for continued usage are coming.&lt;/p&gt;

&lt;p&gt;If your application parses bank statements, you need a migration plan. Here's what's actually happening, what the formats look like under the hood, and how to build a pipeline that handles both.&lt;/p&gt;




&lt;h2&gt;
  
  
  What MT940 Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;Before we talk about replacing MT940, let's look at what we're replacing. Here's a real MT940 statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:20:STMT2603230001
:25:NL91ABNA0417164300
:28C:15/1
:60F:C260322EUR1234,56
:61:2603220322D45,00NTRFNONREF//ACME-INV-2026-042
:86:999~00SEPA OVERBOEKING~20KENMERK: ACME-INV-2026-042
~21Acme Corp Ltd~22PAYMENT FOR SERVICES~23March 2026
~30DEUTDEDB~31DE89370400440532013000~32Acme Corp Ltd
~33Frankfurt
:62F:C260322EUR1189,56
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've never parsed this, here's a field-by-field breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;What's in it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:20:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transaction Reference&lt;/td&gt;
&lt;td&gt;Message identifier (max 16 chars)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:25:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Account ID&lt;/td&gt;
&lt;td&gt;Your IBAN or account number (max 35 chars)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:28C:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Statement Number&lt;/td&gt;
&lt;td&gt;Sequence number (&lt;code&gt;15/1&lt;/code&gt; = statement 15, page 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:60F:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opening Balance&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;C&lt;/code&gt; = Credit, &lt;code&gt;260322&lt;/code&gt; = 22 Mar 2026, &lt;code&gt;EUR&lt;/code&gt;, &lt;code&gt;1234,56&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:61:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Statement Line&lt;/td&gt;
&lt;td&gt;Date + D/C + amount + type code + reference (max 80 chars)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:86:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Information&lt;/td&gt;
&lt;td&gt;Free-text transaction details (up to 6 lines × 65 chars)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;:62F:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Closing Balance&lt;/td&gt;
&lt;td&gt;Same format as opening balance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;:86:&lt;/code&gt; field is where the real pain lives. Banks cram debtor names, payment references, creditor IBANs, and remittance information into a single text block. There is no universal standard for how this data is structured.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;~&lt;/code&gt; delimiters you see above? That's one bank's convention (common in SEPA countries). Other banks use &lt;code&gt;/&lt;/code&gt; prefixes, &lt;code&gt;?&lt;/code&gt; codes, or just dump everything as plain text. You end up writing bank-specific regex patterns to extract structured data — and they break every time the bank changes their layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CAMT.053 Looks Like Instead
&lt;/h2&gt;

&lt;p&gt;Here's the same transaction in CAMT.053:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Ntry&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Amt&lt;/span&gt; &lt;span class="na"&gt;Ccy=&lt;/span&gt;&lt;span class="s"&gt;"EUR"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;45.00&lt;span class="nt"&gt;&amp;lt;/Amt&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;CdtDbtInd&amp;gt;&lt;/span&gt;DBIT&lt;span class="nt"&gt;&amp;lt;/CdtDbtInd&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;BkgDt&amp;gt;&amp;lt;Dt&amp;gt;&lt;/span&gt;2026-03-22&lt;span class="nt"&gt;&amp;lt;/Dt&amp;gt;&amp;lt;/BkgDt&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;VlDt&amp;gt;&amp;lt;Dt&amp;gt;&lt;/span&gt;2026-03-22&lt;span class="nt"&gt;&amp;lt;/Dt&amp;gt;&amp;lt;/VlDt&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;AcctSvcrRef&amp;gt;&lt;/span&gt;ACME-INV-2026-042&lt;span class="nt"&gt;&amp;lt;/AcctSvcrRef&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;NtryDtls&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TxDtls&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;Refs&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;EndToEndId&amp;gt;&lt;/span&gt;ACME-INV-2026-042&lt;span class="nt"&gt;&amp;lt;/EndToEndId&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/Refs&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;RltdPties&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Cdtr&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;Nm&amp;gt;&lt;/span&gt;Acme Corp Ltd&lt;span class="nt"&gt;&amp;lt;/Nm&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;PstlAdr&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;TwnNm&amp;gt;&lt;/span&gt;Frankfurt&lt;span class="nt"&gt;&amp;lt;/TwnNm&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/PstlAdr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/Cdtr&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;CdtrAcct&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;Id&amp;gt;&amp;lt;IBAN&amp;gt;&lt;/span&gt;DE89370400440532013000&lt;span class="nt"&gt;&amp;lt;/IBAN&amp;gt;&amp;lt;/Id&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/CdtrAcct&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/RltdPties&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;RltdAgts&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;CdtrAgt&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;FinInstnId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;BIC&amp;gt;&lt;/span&gt;DEUTDEDB&lt;span class="nt"&gt;&amp;lt;/BIC&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/FinInstnId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/CdtrAgt&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/RltdAgts&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;RmtInf&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Ustrd&amp;gt;&lt;/span&gt;PAYMENT FOR SERVICES March 2026&lt;span class="nt"&gt;&amp;lt;/Ustrd&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/RmtInf&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/TxDtls&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/NtryDtls&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Ntry&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No regex. No bank-specific parsing rules. The creditor name is in &lt;code&gt;&amp;lt;Cdtr&amp;gt;&amp;lt;Nm&amp;gt;&lt;/code&gt;. The IBAN is in &lt;code&gt;&amp;lt;CdtrAcct&amp;gt;&amp;lt;Id&amp;gt;&amp;lt;IBAN&amp;gt;&lt;/code&gt;. The BIC is in &lt;code&gt;&amp;lt;CdtrAgt&amp;gt;&amp;lt;FinInstnId&amp;gt;&amp;lt;BIC&amp;gt;&lt;/code&gt;. The remittance info has its own dedicated &lt;code&gt;&amp;lt;RmtInf&amp;gt;&lt;/code&gt; element — with support for both unstructured text and structured sub-fields.&lt;/p&gt;

&lt;p&gt;The full CAMT.053 document wraps everything in a clear hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Document
  └─ BkToCstmrStmt (Bank-to-Customer Statement)
      ├─ GrpHdr       → Message ID, creation timestamp
      └─ Stmt         → One per account
          ├─ Acct      → IBAN, BIC, account name
          ├─ Bal[]     → OPBD, CLBD, CLAV, FWAV (all timestamped)
          └─ Ntry[]    → One per transaction
              └─ NtryDtls
                  └─ TxDtls → Refs, parties, agents, remittance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the technical comparison side-by-side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;MT940&lt;/th&gt;
&lt;th&gt;CAMT.053&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;Proprietary text (SWIFT FIN)&lt;/td&gt;
&lt;td&gt;XML (ISO 20022, XSD-validated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction details&lt;/td&gt;
&lt;td&gt;Packed into &lt;code&gt;:61:&lt;/code&gt; (80 chars) + &lt;code&gt;:86:&lt;/code&gt; (6×65 chars)&lt;/td&gt;
&lt;td&gt;Dedicated elements: &lt;code&gt;Refs&lt;/code&gt;, &lt;code&gt;RltdPties&lt;/code&gt;, &lt;code&gt;RltdAgts&lt;/code&gt;, &lt;code&gt;RmtInf&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remittance info&lt;/td&gt;
&lt;td&gt;Crammed into &lt;code&gt;:86:&lt;/code&gt;, bank-specific delimiters&lt;/td&gt;
&lt;td&gt;Structured &lt;code&gt;&amp;lt;RmtInf&amp;gt;&lt;/code&gt; with &lt;code&gt;&amp;lt;Strd&amp;gt;&lt;/code&gt; sub-fields, unlimited length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Balance types&lt;/td&gt;
&lt;td&gt;Opening (&lt;code&gt;:60F:&lt;/code&gt;) and Closing (&lt;code&gt;:62F:&lt;/code&gt;) only&lt;/td&gt;
&lt;td&gt;OPBD, CLBD, CLAV, PRCD, FWAV — all with timestamps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Currency&lt;/td&gt;
&lt;td&gt;Single currency per statement&lt;/td&gt;
&lt;td&gt;Multi-currency with exchange rate details per transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Character set&lt;/td&gt;
&lt;td&gt;SWIFT X charset (A-Z, 0-9, basic punctuation — no accents)&lt;/td&gt;
&lt;td&gt;Full UTF-8 / Unicode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date format&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;YYMMDD&lt;/code&gt; (ambiguous century)&lt;/td&gt;
&lt;td&gt;ISO 8601: &lt;code&gt;YYYY-MM-DD&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;Manual — hope the parser handles edge cases&lt;/td&gt;
&lt;td&gt;XSD schema validation built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Migration Timeline — What's Actually Happening
&lt;/h2&gt;

&lt;p&gt;There's a lot of confusion around "SWIFT is killing MT940" because the migration is happening in waves. Here's the accurate picture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What already happened (November 22, 2025):&lt;/strong&gt;&lt;br&gt;
SWIFT ended the MT/ISO 20022 coexistence period for &lt;strong&gt;payment messages&lt;/strong&gt;. MT103 (credit transfers) and MT202 (institution transfers) are formally retired. All cross-border payments now use ISO 20022 MX format exclusively via the FINplus service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's happening now (2026):&lt;/strong&gt;&lt;br&gt;
MT940 and other &lt;strong&gt;reporting messages&lt;/strong&gt; are deprecated and no longer maintained by SWIFT — but they haven't been withdrawn yet. J.P. Morgan's ISO 20022 FAQ puts it clearly: "Reporting and statement messages will not be immediately withdrawn from the FIN service. Although these message types are deprecated and no longer maintained by SWIFT, disincentives for their use will be introduced at a later date."&lt;/p&gt;

&lt;p&gt;Banks are transitioning on their own timelines. J.P. Morgan has been accepting CAMT.052/053/054 since Q4 2024. Bank of America completed its Fedwire ISO 20022 implementation in July 2025. 44% of banks are behind schedule on their November 2026 milestones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's coming (2027+):&lt;/strong&gt;&lt;br&gt;
SWIFT's roadmap targets full MT message retirement, including enquiry and investigation messages (MT199/MT299 → camt.110/camt.111) by November 2027. The reporting messages (MT940 → CAMT.053) will follow.&lt;/p&gt;

&lt;p&gt;The bottom line: MT940 still works today, but no one is maintaining or improving it. New features, new validation rules, new regulatory requirements — all of that goes into CAMT.053. Building on MT940 now is building on a dead-end.&lt;/p&gt;
&lt;h2&gt;
  
  
  Your Options as a Developer
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Option 1: Build It Yourself
&lt;/h3&gt;

&lt;p&gt;There are open-source libraries for individual formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mt940-rs&lt;/code&gt; (Rust) — MT940 parser&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pycamt&lt;/code&gt; / &lt;code&gt;camt_parser&lt;/code&gt; (Python) — CAMT.053 parsers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ofxstatement&lt;/code&gt; (Python) — OFX converter&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Cmxl&lt;/code&gt; (Ruby) — MT940 parser with extensible design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem: you need &lt;strong&gt;multiple libraries&lt;/strong&gt; plus glue code to normalize outputs into a common model. You need to handle bank-specific &lt;code&gt;:86:&lt;/code&gt; field variations, character encoding edge cases (MT940's SWIFT X charset vs. UTF-8), date format conversions (&lt;code&gt;YYMMDD&lt;/code&gt; → &lt;code&gt;YYYY-MM-DD&lt;/code&gt;), and amount parsing (comma vs. dot decimal separators).&lt;/p&gt;

&lt;p&gt;For a single-format, single-bank integration, this works. For anything multi-bank or multi-format, you're signing up for ongoing maintenance as banks change their implementations.&lt;/p&gt;
&lt;h3&gt;
  
  
  Option 2: Enterprise Aggregators
&lt;/h3&gt;

&lt;p&gt;Plaid (12,000+ institutions), Finicity (Mastercard), and Wise offer bank data APIs. But they solve a different problem — they connect to &lt;strong&gt;live bank accounts&lt;/strong&gt; via OAuth. If you already have statement files (SFTP drops, email attachments, file exports), these platforms are overkill. They're also expensive and come with heavy onboarding.&lt;/p&gt;
&lt;h3&gt;
  
  
  Option 3: A Dedicated Conversion API
&lt;/h3&gt;

&lt;p&gt;This is the gap. You have files in format A, you need them in format B. No bank connections, no OAuth flows, no aggregation. Just conversion — stateless, fast, spec-compliant.&lt;/p&gt;

&lt;p&gt;That's what we're building.&lt;/p&gt;
&lt;h2&gt;
  
  
  Introducing FinConvert
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://finconvert.dev" rel="noopener noreferrer"&gt;FinConvert&lt;/a&gt; is a REST API that converts bank statement files between financial formats. One endpoint, any supported format in, any supported format out.&lt;/p&gt;

&lt;p&gt;The core architecture uses a &lt;strong&gt;Universal Transaction Model&lt;/strong&gt;: every input format is parsed and normalized into a single internal representation, then serialized to the requested output format. This means adding new formats requires N+M adapters, not N×M conversion paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Currently supported:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Formats&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MT940, CAMT.053 (OFX, BAI2, QIF coming soon)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CAMT.053, CSV, JSON, OFX (MT940 output coming soon)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Design principles:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy-first&lt;/strong&gt; — No files are stored. Conversion is stateless. Your financial data is processed in memory and discarded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spec-compliant&lt;/strong&gt; — Output is validated against official SWIFT and ISO 20022 XSD schemas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; — Sub-200ms average conversion time. Pure computation, no I/O bottleneck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;London-hosted&lt;/strong&gt; — EU data residency for compliance-conscious teams.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Show Me the Code
&lt;/h2&gt;

&lt;p&gt;Convert an MT940 file to structured JSON:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;curl:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.finconvert.dev/v1/convert &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer fc_your_api_key"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@statement.mt940"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"output_format=json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; converted.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;TypeScript:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convertStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ConvertedStatement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output_format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.finconvert.dev/v1/convert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer fc_your_api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Conversion failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What you get back&lt;/strong&gt; — structured, typed, no regex required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NL91ABNA0417164300"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"opening_balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1234.56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"closing_balance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1189.56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"statement_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"15/1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-22"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transactions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-22"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-45.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEBIT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACME-INV-2026-042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAYMENT FOR SERVICES March 2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"creditor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Corp Ltd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"iban"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE89370400440532013000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"bic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEUTDEDB"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transaction_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MT940"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format_output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JSON"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That MT940 &lt;code&gt;:86:&lt;/code&gt; field with bank-specific &lt;code&gt;~&lt;/code&gt; delimiters? Parsed into clean, typed JSON. The creditor IBAN that was buried in &lt;code&gt;~31&lt;/code&gt;? Extracted into &lt;code&gt;creditor.iban&lt;/code&gt;. No bank-specific logic on your end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;Usage-based — you pay for what you convert:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Conversions/month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$49/mo&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Business&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$149/mo&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enterprise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The free tier is enough for testing and low-volume integrations. No credit card required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;FinConvert is currently in early access. We're onboarding developers from the waitlist and expanding format support based on demand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the roadmap:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OFX, BAI2, and QIF input support&lt;/li&gt;
&lt;li&gt;MT940 output (for systems that still require it during the transition)&lt;/li&gt;
&lt;li&gt;Auto-format detection — upload any file, we figure out what it is&lt;/li&gt;
&lt;li&gt;Batch conversion endpoint for bulk processing&lt;/li&gt;
&lt;li&gt;Bank-specific &lt;code&gt;:86:&lt;/code&gt; field profiles for higher extraction accuracy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building anything that touches bank statement data — accounting software, reconciliation tools, fintech integrations, ERP connectors — the MT940 deprecation is real. It still works today, but the writing is on the wall: SWIFT has stopped maintaining it, banks are migrating, and every new feature goes into CAMT.053.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://finconvert.dev" rel="noopener noreferrer"&gt;Join the waitlist at finconvert.dev&lt;/a&gt;&lt;/strong&gt; to get early access and lock in free-tier usage during beta.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://zerolooplabs.dev" rel="noopener noreferrer"&gt;Zero Loop Labs&lt;/a&gt; — the same team behind &lt;a href="https://sealtrail.dev" rel="noopener noreferrer"&gt;SealTrail&lt;/a&gt; (tamper-proof audit trails) and &lt;a href="https://pdfforge.dev" rel="noopener noreferrer"&gt;PDFForge&lt;/a&gt; (document generation API).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>api</category>
      <category>banking</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why your INSERT INTO audit_log proves nothing</title>
      <dc:creator>Zero Lopp Labs</dc:creator>
      <pubDate>Fri, 13 Mar 2026 11:11:08 +0000</pubDate>
      <link>https://dev.to/zerolooplabs/why-your-insert-into-auditlog-proves-nothing-1ohf</link>
      <guid>https://dev.to/zerolooplabs/why-your-insert-into-auditlog-proves-nothing-1ohf</guid>
      <description>&lt;p&gt;You've seen this pattern. You might have written it yourself. I know I did.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;audit_log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;action&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;resource&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metadata&lt;/span&gt;   &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time something important happens — a user signs a contract, an admin changes permissions, a payment goes through — you &lt;code&gt;INSERT INTO audit_log&lt;/code&gt;. Job done. You have an audit trail.&lt;/p&gt;

&lt;p&gt;Except you don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem nobody talks about
&lt;/h2&gt;

&lt;p&gt;That &lt;code&gt;audit_log&lt;/code&gt; table sits in the same database as everything else. Anyone with write access can do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Oops. Never happened.&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;audit_log&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'invoice.viewed'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'invoice.deleted'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Or just make it disappear entirely&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;audit_log&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'admin_42'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'permission.escalated'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No trace. No alert. No way to know it ever happened.&lt;/p&gt;

&lt;p&gt;This isn't a theoretical attack. It's the &lt;strong&gt;default state&lt;/strong&gt; of every audit log built on a regular database table. Your DBA can do it. A compromised admin account can do it. A SQL injection exploit can do it. And nobody will ever know.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But we have backups"
&lt;/h2&gt;

&lt;p&gt;Sure. But backups tell you what the data &lt;em&gt;was&lt;/em&gt;, not whether it was &lt;em&gt;tampered with&lt;/em&gt; between now and then. If someone edits a row on Monday and you check the backup on Friday, you're comparing against a backup that might already include the edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  "We use triggers and permissions"
&lt;/h2&gt;

&lt;p&gt;Better. But triggers can be disabled by superusers. &lt;code&gt;ALTER TABLE ... DISABLE TRIGGER ALL&lt;/code&gt; is one command. And PostgreSQL &lt;code&gt;SECURITY DEFINER&lt;/code&gt; functions can bypass row-level security.&lt;/p&gt;

&lt;p&gt;The fundamental issue isn't about access control. It's about &lt;strong&gt;provability&lt;/strong&gt;. Can you &lt;em&gt;prove&lt;/em&gt; — cryptographically, not just organizationally — that a log entry hasn't been modified since it was written?&lt;/p&gt;

&lt;p&gt;With a regular INSERT? No. You can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What auditors actually want
&lt;/h2&gt;

&lt;p&gt;When a compliance auditor asks "show me the audit trail for this transaction," they're not asking for a database dump. They want to know:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Completeness&lt;/strong&gt; — Are all events present? Can you prove nothing was deleted?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrity&lt;/strong&gt; — Can you prove nothing was modified after the fact?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ordering&lt;/strong&gt; — Can you prove the sequence of events hasn't been rearranged?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A plain &lt;code&gt;audit_log&lt;/code&gt; table answers none of these questions with certainty.&lt;/p&gt;

&lt;p&gt;SOC 2, HIPAA, PCI-DSS Requirement 10, GDPR Article 32 — they all expect or require audit records with demonstrable integrity. "We have a database table" doesn't cut it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hash chains: the fix
&lt;/h2&gt;

&lt;p&gt;The solution has existed since the 1990s (Haber &amp;amp; Stornetta, the paper that later inspired Bitcoin's blockchain). The idea is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each log entry includes a cryptographic hash of itself &lt;em&gt;and&lt;/em&gt; the previous entry.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event 1: hash("payload_1" + GENESIS_HASH)        → hash_1
Event 2: hash("payload_2" + hash_1)               → hash_2
Event 3: hash("payload_3" + hash_2)               → hash_3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if someone modifies Event 2, &lt;code&gt;hash_2&lt;/code&gt; changes. But Event 3 was computed using the &lt;em&gt;original&lt;/em&gt; &lt;code&gt;hash_2&lt;/code&gt;. The chain breaks. The tampering is immediately detectable.&lt;/p&gt;

&lt;p&gt;You can't silently edit a single row without invalidating every subsequent hash in the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;Here's a concrete example. You're logging invoice events in a SaaS app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SealTrail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sealtrail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SealTrail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SEALTRAIL_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Log an event — hash chain is built automatically&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finance_user_42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice.approved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inv_12345&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// "a1b2c3d4e5f6..."&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each event gets a SHA-256 hash computed from the canonicalized event payload (actor, action, resource, context), the previous event's hash, and the event timestamp.&lt;/p&gt;

&lt;p&gt;The hash and chain position are returned with every event. They're not just metadata — they're &lt;strong&gt;cryptographic proof&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification: the part that matters
&lt;/h2&gt;

&lt;p&gt;Logging isn't enough. The whole point is that you — or an auditor, or an automated compliance check — can &lt;em&gt;verify&lt;/em&gt; that nothing was touched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;evt_abc123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Event integrity verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Chain intact:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chainIntact&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TAMPER DETECTED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Expected:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;computedHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Got:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verification recomputes the hash from scratch. If the stored hash doesn't match the computed one, someone changed the data. If &lt;code&gt;chainIntact&lt;/code&gt; is false, someone broke the link between events — meaning an event was inserted, deleted, or reordered.&lt;/p&gt;

&lt;p&gt;This isn't trust. It's math.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DIY trap
&lt;/h2&gt;

&lt;p&gt;At this point you might be thinking: "I can build this myself. SHA-256, a &lt;code&gt;previous_hash&lt;/code&gt; column, done."&lt;/p&gt;

&lt;p&gt;I thought the same thing. Here's what I ran into:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrent writes.&lt;/strong&gt; Two events arrive at the same millisecond. Both read the same &lt;code&gt;previous_hash&lt;/code&gt;. Both compute their hash against it. You now have a forked chain. Fix: atomic transactions with unique constraints on chain position. Retry on conflict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canonical serialization.&lt;/strong&gt; &lt;code&gt;JSON.stringify({ a: 1, b: 2 })&lt;/code&gt; and &lt;code&gt;JSON.stringify({ b: 2, a: 1 })&lt;/code&gt; produce different strings, therefore different hashes. You need deterministic JSON canonicalization, or your verification breaks when key order varies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chain partitioning.&lt;/strong&gt; One global chain becomes a bottleneck. You need per-resource or per-tenant chains. But then you need to manage chain creation, head tracking, and cross-chain references.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pagination at scale.&lt;/strong&gt; Offset-based pagination breaks when events are being inserted. You need cursor-based pagination with stable sort keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry logic.&lt;/strong&gt; Rate limits, network failures, concurrent conflicts — your client needs exponential backoff with jitter, and it needs to handle 409 Conflict responses specifically for chain contention.&lt;/p&gt;

&lt;p&gt;It's solvable. But it's a week of work to build, and a lifetime of maintenance to keep secure. And if you get the canonicalization wrong, your entire chain is silently invalid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop building underpants
&lt;/h2&gt;

&lt;p&gt;There's a term in engineering for components that every team rebuilds from scratch despite it not being their core business: &lt;strong&gt;underpants&lt;/strong&gt; (as in, everyone needs them, nobody should be hand-stitching them).&lt;/p&gt;

&lt;p&gt;Your audit trail is underpants. It needs to work. It needs to be tamper-proof. But it's not what your users are paying you for.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://sealtrail.dev" rel="noopener noreferrer"&gt;SealTrail&lt;/a&gt; because I needed this for my own SaaS products and got tired of reimplementing it. It's an API — &lt;code&gt;npm install sealtrail&lt;/code&gt;, log events, verify integrity. Hash chains, cursor pagination, isolated chains per resource or tenant, and cryptographic verification are handled for you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Your entire audit trail integration&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SealTrail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sealtrail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SealTrail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SEALTRAIL_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Log (events are chained per chain — default or custom)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;document.signed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;documents&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;// optional — isolates chains per resource type&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Verify&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proof&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// proof.valid === true | false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines to log. One line to verify. Hash chain built automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;INSERT INTO audit_log&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Hash chain audit trail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Can admin silently edit?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No — hash changes, chain breaks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can rows be deleted?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Detectable — position gaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can order be changed?&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No — each hash includes previous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cryptographic proof?&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;SHA-256 chain verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance-ready?&lt;/td&gt;
&lt;td&gt;Questionable&lt;/td&gt;
&lt;td&gt;Verifiable record&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your audit log is a regular database table, it proves nothing. It's a convenience log, not an audit trail.&lt;/p&gt;

&lt;p&gt;If that's fine for your use case, carry on. But if you're handling financial data, healthcare records, legal documents, or anything where "we can prove this wasn't tampered with" matters — you need hash chains.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Sylvain, solo founder at &lt;a href="https://zerolooplabs.dev" rel="noopener noreferrer"&gt;Zero Loop Labs&lt;/a&gt;. I build developer tools. SealTrail is my latest — a tamper-proof audit trail API for developers. &lt;a href="https://sealtrail.dev" rel="noopener noreferrer"&gt;Try it free&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>typescript</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
