Turning accounts receivable into code: An automated dunning architecture.

8/1/2025
Turning accounts receivable into code: An automated dunning architecture.
If you examine the technical architecture diagrams of almost any modern enterprise software platform or high-growth B2B business, you will find extraordinary levels of engineering sophistication. Customer signups trigger asynchronous event streams in Apache Kafka; billing events are reconciled in real time across distributed microservices; and payment webhooks from Stripe or GoCardless trigger instant ledger updates in PostgreSQL.Yet if you follow the accounts receivable lifecycle to the moment an invoice becomes overdue, that pristine engineering architecture suddenly hits a brick wall. In hundreds of companies, the moment automated credit card charges fail or a B2B client exceeds their 30-day credit terms, the software stops. The system dumps an overdue flag into a relational table, and the process reverts entirely to manual human intervention.An accounts receivable specialist manually downloads an invoice PDF, drafts an email from a template, or prints a physical statement on an office desktop printer. In an era where every other operational workflow has been codified, credit control remains an un-automated, fragmented, human-driven operational bottleneck.It doesn't have to be this way. Credit control and dunning are deterministic state machines. An invoice has clear states, unambiguous aging transitions, and legally defined escalation requirements. In this architectural guide, we will explore how to turn accounts receivable into pure, maintainable code—building an automated, multi-tier dunning engine that bridges digital webhooks with physical Royal Mail delivery via LetterAPI.

The architectural problem: The manual gap in modern billing stacks

Why do software engineering teams struggle to fully automate accounts receivable? The challenge stems from the fundamental difference between standard transactional notifications and legal credit control.For standard transactional messaging (such as signup confirmations, password resets, or routine payment receipts), digital channels like SendGrid, Postmark, and Twilio provide perfect programmatic solutions. These notifications are low-stakes: if an email bounces or lands in spam, the user will eventually notice or request a password reset.Credit control, by contrast, is high-stakes, adversarial, and legally governed. When an invoice reaches 14, 28, or 45 days overdue, continuing to send emails is ineffective. The debtor is actively ignoring digital channels. Furthermore, if the dispute escalates to County Court proceedings, the business must prove formal legal service under the UK Civil Procedure Rules (CPR) and Section 7 of the Interpretation Act 1978.Because software engineers historically lacked an addressable API to bridge cloud software with physical Royal Mail post, they built their billing systems to simply stop at the physical boundary. They treated physical mail as an 'out-of-band operational exception.' This architectural gap creates severe data silos, eliminates observability, and forces operations teams to spend hundreds of hours performing manual post.

The deterministic credit control state machine: Modeling dunning in software

To turn accounts receivable into code, you must first model the invoice lifecycle as a deterministic, finite state machine (FSM).An invoice does not simply exist in 'paid' or 'unpaid' states. In a rigorous B2B credit control architecture, an invoice transitions through a series of discrete temporal and behavioral states:State 1: ISSUED (Day 0). The invoice is created, assigned a unique ledger identifier, and dispatched to the customer via digital email PDF with an Open Banking payment link.State 2: APPROACHING_DUE (Day -3). An automated digital reminder is dispatched confirming upcoming settlement.State 3: DUE (Day 0 of credit period). The invoice reaches its contracted maturity date.State 4: OVERDUE_DIGITAL_GRACE (Days 1 to 13). Automated digital reminders are dispatched via email at Day 3 and Day 7. If payment settles, the state transitions immediately to SETTLED.State 5: OVERDUE_PHYSICAL_BREAKER (Day 14). Digital dunning is paused. The state machine triggers an automated physical letter via LetterAPI. A formal account statement is printed on 90gsm bond paper, enclosed in a DL window envelope, and dispatched via Royal Mail First Class with a 2D Mailmark barcode.State 6: STATUTORY_INTEREST_ESCALATION (Day 28). If still unsettled, the state machine triggers a statutory late payment notice, calculating statutory interest (8% + base rate) and fixed compensation fees (£40 to £100) under the Late Payment of Commercial Debts Act 1998.State 7: PRE_ACTION_CLAIM_NOTICE (Day 45). The state machine triggers an automated, CPR-compliant Letter Before Action via Royal Mail First Class, attaching required Pre-Action Protocol documentation and establishing formal proof of service.State 8: LEGAL_ENFORCEMENT (Day 60). The claim data and LetterAPI proof-of-service certificates are automatically exported to legal counsel or Money Claim Online (MCOL) for court judgment.

Database schema: Structuring AR state persistence

To implement this state machine reliably, your database schema must track dunning milestones, postal consignments, and webhook timestamps with complete auditability.Here is a production-grade Prisma schema modeling invoices, dunning events, and LetterAPI postal dispatches:
schema.prisma — prisma
enum InvoiceStatus {
ISSUED
OVERDUE
IN_DUNNING
SETTLED
WRITTEN_OFF
REFERRED_TO_LEGAL
}
​
enum DunningStage {
DAY_7_DIGITAL_REMINDER
DAY_14_PHYSICAL_BREAKER
DAY_28_STATUTORY_NOTICE
DAY_45_LETTER_BEFORE_ACTION
}
​
model Invoice {
id String @id @default(uuid())
invoiceNumber String @unique
customerId String
customer Customer @relation(fields: [customerId], references: [id])
amount Decimal @db.Decimal(10, 2)
balanceRemaining Decimal @db.Decimal(10, 2)
dueDate DateTime
status InvoiceStatus @default(ISSUED)
dunningStage DunningStage?
statutoryInterest Decimal @default(0.00) @db.Decimal(10, 2)
statutoryFee Decimal @default(0.00) @db.Decimal(10, 2)
dunningEvents DunningEvent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
​
model DunningEvent {
id String @id @default(uuid())
invoiceId String
invoice Invoice @relation(fields: [invoiceId], references: [id])
stage DunningStage
channel String // 'EMAIL' | 'PHYSICAL_POST'
letterApiId String? // Consignment ID from LetterAPI
postageClass String? // 'first_class' | 'second_class'
mailmarkBarcode String?
dispatchedAt DateTime?
deliveredAt DateTime?
createdAt DateTime @default(now())
}
This normalized schema ensures that every communication event—digital or physical—is permanently captured in an unalterable audit log.

The dispatch pipeline: Event-driven worker implementation

With the schema in place, we can construct the event-driven background worker. Using a distributed task queue (such as BullMQ or AWS SQS), the worker evaluates delinquent accounts on a daily cron schedule and issues programmatic calls to LetterAPI.Here is the complete TypeScript implementation of the dunning queue worker:
dunningWorker.ts — typescript
import { Worker, Job } from 'bullmq';
import { LetterAPI } from '@letterapi/sdk';
import { prisma } from './prismaClient';
​
const letterapi = new LetterAPI(process.env.LETTERAPI_API_KEY!);
​
export const dunningWorker = new Worker('dunning-queue', async (job: Job) => {
const { invoiceId, targetStage } = job.data;
const invoice = await prisma.invoice.findUnique({
where: { id: invoiceId },
include: { customer: true }
});
​
if (!invoice || invoice.status === 'SETTLED') {
return { skipped: true, reason: 'Invoice settled or not found' };
}
​
// Execute physical dispatch via LetterAPI
const letter = await letterapi.letters.create({
recipient: {
name: invoice.customer.companyName,
attention: 'Head of Accounts Payable',
line1: invoice.customer.addressLine1,
line2: invoice.customer.addressLine2 || undefined,
city: invoice.customer.city,
postcode: invoice.customer.postcode,
country: 'GB'
},
pdfUrl: `https://billing.internal.co.uk/invoices/${invoice.id}/notice-${targetStage}.pdf`,
postageClass: 'first_class',
doubleSided: true,
color: true,
metadata: {
invoiceId: invoice.id,
invoiceNumber: invoice.invoiceNumber,
stage: targetStage
},
idempotencyKey: `dunning-${invoice.id}-${targetStage}`
});
​
// Record dunning event in database
await prisma.dunningEvent.create({
data: {
invoiceId: invoice.id,
stage: targetStage,
channel: 'PHYSICAL_POST',
letterApiId: letter.id,
postageClass: 'first_class',
dispatchedAt: new Date()
}
});
​
await prisma.invoice.update({
where: { id: invoice.id },
data: { dunningStage: targetStage }
});
​
return { success: true, consignmentId: letter.id };
});
Notice the strict use of the idempotencyKey parameter. In distributed architectures, queue workers can retry jobs upon network interruption. The idempotency key ensures that LetterAPI never prints or charges for duplicate letters.

Webhook ingestion: Real-time postal event reconciliation

The final architectural component is real-time webhook reconciliation. When Royal Mail scans the letter's 2D Mailmark barcode at distribution hubs, LetterAPI dispatches asynchronous webhooks to your server.Here is the Express webhook handler that consumes these events and updates the internal database:
letterApiWebhookHandler.ts — typescript
import express from 'express';
import { prisma } from './prismaClient';
​
const app = express();
​
app.post('/api/webhooks/letterapi', express.json(), async (req, res) => {
const { type, data } = req.body;
​
if (type === 'letter.dispatched') {
await prisma.dunningEvent.updateMany({
where: { letterApiId: data.id },
data: {
dispatchedAt: new Date(data.dispatchedAt),
mailmarkBarcode: data.mailmarkBarcode
}
});
console.log(`Letter ${data.id} in transit with Royal Mail.`);
}
​
if (type === 'letter.delivered') {
await prisma.dunningEvent.updateMany({
where: { letterApiId: data.id },
data: { deliveredAt: new Date(data.deliveredAt) }
});
console.log(`Letter ${data.id} delivered. Evidentiary proof logged.`);
}
​
res.status(200).json({ received: true });
});
This creates a closed-loop architecture where physical delivery events are synchronized directly with your accounting records.

The three levels of architectural failure in credit control

Leaving accounts receivable outside your core software engineering architecture inflicts severe damage across three distinct dimensions.The external problem is massive operational inefficiency and delayed cash realization. Invoices slip through manual cracks, human operators forget to send reminders, and aged debt balloons to unsustainable levels.The internal problem is developer and operations friction. Engineering leaders feel uncomfortable knowing that an essential commercial workflow relies on manual spreadsheets, desk printers, and franking machines, while finance staff are overwhelmed by administrative postroom duties.The philosophical problem is architectural inconsistency. Why build an automated, cloud-native technology business if your revenue collection relies on nineteenth-century manual labor? If an operational process can be written in code, it must be written in code.

Enter the guide: LetterAPI as code-addressable postal infrastructure

LetterAPI was built by software engineers, for software engineers. We believe that physical mail should be treated with the exact same architectural rigor as transactional email or SMS.We provide developer-first REST APIs, native SDKs for Node.js, TypeScript, Python, and Go, full sandbox environments, and real-time webhook event streams. Starting at just £0.86 per letter, LetterAPI enables you to turn accounts receivable into resilient, elegant code.Transform your credit control from a manual operational headache into an automated, highly observable engineering asset with LetterAPI today.

The 3-step action plan: Implementing automated AR architecture

Codifying your accounts receivable pipeline takes less than an afternoon:Step 1: Sign up and retrieve sandbox API keys. Create your developer account at letterapi.uk. Review our API documentation and verify your document geometry in our sandbox.Step 2: Implement the dunning queue worker. Connect your billing database or ERP to the LetterAPI SDK. Define your automated escalation thresholds (e.g. Day 14, Day 28, Day 45).Step 3: Deploy webhook handlers and monitor automated collections. Subscribe to LetterAPI webhook callbacks. Watch as overdue accounts settle their balances automatically upon physical letter delivery, without a single manual intervention.

The stakes: Fragile manual dunning vs. robust programmatic execution

Every week you leave credit control un-automated, your company pays a steep price in delayed cash flow, operational waste, and human error.The alternative is robust programmatic execution. By turning accounts receivable into code with LetterAPI, you eliminate manual friction, enforce contractual discipline, and build an automated cash collection machine that scales effortlessly with your business.Codify your accounts receivable today with LetterAPI.

Get started with LetterAPI today

Stop treating physical mail as an out-of-band operational exception. With LetterAPI, sending high-impact physical letters through Royal Mail is as simple, fast, and automated as sending an API request.Starting at just £0.86 per letter including commercial color printing, FSC-certified 90gsm paper, windowed envelope, and Royal Mail nationwide postage, LetterAPI is the modern postal infrastructure for software developers.Sign up for your free developer account at letterapi.uk today, claim your test credentials, and start building automated credit control into your application.

The financial controller's audit dashboard: Visualizing automated dunning in real time

One of the greatest benefits of turning accounts receivable into code is the radical transparency it unlocks for financial leadership.In a legacy finance department, knowing the status of overdue accounts requires asking credit controllers to cross-reference multiple spreadsheets, email sent folders, and paper franking books. By codifying the dunning lifecycle with LetterAPI, every communication event is emitted as a structured database record.Engineering teams can easily build real-time executive dashboards in Grafana, Retool, or internal admin portals displaying live AR health: the number of invoices currently at each dunning milestone, real-time postal delivery tracking maps powered by Royal Mail Mailmark scans, open banking settlement velocity, and automated DSO trend lines.Finance directors and CFOs gain instant, unassailable visibility into working capital performance, turning accounts receivable from an opaque administrative headache into a predictable, data-driven financial engine.

Handling edge cases: Address validation failures, PDF timeouts, and dead letter queues

In distributed software systems, true engineering robustness is measured not by how a pipeline behaves when everything goes right, but by how gracefully it handles edge cases and operational failures.When building an automated accounts receivable dunning pipeline, your architecture must anticipate three primary failure modes: address validation errors, PDF generation latency, and network timeouts.First, consider address validation failures: when your dunning worker submits a recipient address that cannot be validated against the Royal Mail Postcode Address File (PAF)—for example, if a customer entered an invalid postcode or omitted their building number—LetterAPI returns a structured HTTP 422 Unprocessable Entity error code with detailed PAF diagnostic flags.Your software must not simply crash or discard the job. Instead, the dunning worker routes the job to an isolated Dead Letter Queue (DLQ) in BullMQ or AWS SQS. A background handler intercepts the DLQ event, updates the invoice record with a 'NEEDS_ADDRESS_CORRECTION' status, and automatically dispatches an alert to your finance team's Slack channel via webhook: '⚠️ Action Required: Invoice #1042 cannot be posted. Postcode SW1A 1AA is invalid for 24 Victoria Street.'Second, consider PDF generation latency: if your PDF rendering microservice (using Puppeteer or Playwright) experiences memory spikes or takes more than five seconds to render a complex multi-page statement, the dunning worker must implement exponential backoff with jitter, ensuring that temporary rendering spikes do not cause permanent dunning drops.By architecting your AR engine with resilient error boundaries, dead letter queues, and automated alerting, you ensure that physical dunning runs with the fault tolerance of mission-critical banking infrastructure.

Idempotency deep dive: Preventing duplicate printing across distributed systems

In transactional financial systems, idempotency is an absolute, non-negotiable architectural requirement. In an email dunning system, if a network timeout causes a worker to retry and send two duplicate emails, the customer might be mildly annoyed. In a physical mail system, if a worker retries and prints two identical physical letters, your company pays double postage, wastes paper, and looks intensely unprofessional to the client.LetterAPI guarantees strict, cryptographic idempotency across our entire API ecosystem. Every POST request to /v1/letters accepts an optional but strongly recommended idempotencyKey header.When our API gateway receives a request, it performs an atomic Redis check using SET key value NX EX 86400 (setting the key with a 24-hour expiration only if it does not already exist). If a duplicate request arrives with the same idempotency key—even milliseconds later due to a concurrent worker retry—LetterAPI immediately returns the cached response from the initial successful creation, without re-queuing the document for print.To maximize reliability, your application should construct deterministic idempotency keys based on domain entities: for example, idempotencyKey: `dunning-${invoice.id}-${dunningStage}-${invoice.currentBillingCycle}`. This guarantees that no matter how many times your background workers restart, retry, or suffer network partitions, a physical letter is manufactured exactly once.

Accounting software integration patterns: Webhooks, polling, and event bridges for Xero and NetSuite

Modern engineering teams rarely build billing systems completely from scratch; they integrate with leading accounting engines and ERPs like Xero, QuickBooks Online, Sage Intacct, or NetSuite.To connect LetterAPI with these platforms, software architects employ two primary integration patterns: event-driven webhooks and scheduled cron polling.In modern platforms with robust webhook infrastructure (such as Stripe Invoicing or custom billing APIs), you can listen directly to payment failure events (e.g. invoice.payment_failed or customer.subscription.past_due). Your webhook consumer receives the event, calculates the customer's aging milestone, and issues a LetterAPI call in real time.For traditional ERPs like Xero or Sage 50 that rely on batch processing, the recommended architectural pattern is a lightweight Event Bridge worker. Every morning at 8:00 AM, a scheduled AWS Lambda function or Node.js container queries the Xero API (/api.xro/2.0/Invoices) for invoices matching Status: 'AUTHORISED' and DueDate < Today - 14 Days.The worker maps Xero customer contact records to LetterAPI recipient schemas, executes the physical dispatch, and writes the resulting LetterAPI consignment tracking ID directly back into the Xero invoice notes via the Xero API. This creates a seamless two-way synchronization between your core financial ledger and your physical postal delivery pipeline.

The event-driven financial ledger: Reconciling payments with Stripe, Xero, and Open Banking

The ultimate elegance of an automated accounts receivable architecture is closed-loop financial reconciliation.When LetterAPI generates a physical statement or overdue notice, our document rendering engine dynamically embeds an Open Banking Pay-by-Bank QR code on the bottom-right corner of the letter. The QR code contains an encrypted deep-link payload encoding the specific invoice ID, recipient reference, and exact settlement balance.When the corporate debtor receives the letter, they scan the QR code with their corporate smartphone banking app (such as Barclays, NatWest, HSBC, or Revolut Business). The payment is authenticated via biometric FaceID and transferred instantly over the UK Faster Payments rails.Within 1.5 seconds of payment completion, the Open Banking provider dispatches a settlement webhook to your billing server. Your software intercepts the webhook, marks the invoice as SETTLED in your database, automatically halts all scheduled downstream dunning jobs in BullMQ, and updates your Xero or NetSuite ledger via API. From the physical envelope landing on a desk to cleared cash in your bank account, the entire reconciliation lifecycle executes without a single human touch.