Building an Accounting API That Survives an Audit
A practical guide to the security and audit controls a financial API needs before Dutch businesses trust it with their books.
An accounting API that returns 200 OK on a payment write has told you almost nothing. It has not confirmed that the ledger recorded the entry, that the actor was authorized to make it, that the amount passed validation, or that anyone with authority approved it. For businesses in the Netherlands, where the tax authority expects records to be reconstructable and retained for seven years, that gap between "the request succeeded" and "the books are correct" is where audits go wrong.
Audit-ready is an operational design goal, not a certificate. It means the API preserves a complete chain: authenticated actor to authorized action to validated state change to approval to ledger result to tamper-evident evidence. Everything below is about building that chain deliberately rather than assuming authentication and TLS cover it.
What makes an accounting API "audit-ready"
An auditor reconstructing a transaction needs to answer a specific sequence of questions. Who initiated it? Were they permitted to? Did the change pass accounting validation? Who approved it? What did the ledger record? Can that record be altered after the fact without leaving a trace?
If your API cannot answer all six from its own logs and data model, it is not audit-ready, regardless of how clean the code looks. Most APIs handle the first question well and the rest poorly.
The controls that close the gap fall into a few groups: explicit accounting state, layered authorization, scoped authentication, idempotent writes, server-side validation, segregation of duties, and tamper-evident logging. They reinforce each other. Weak authorization undermines a strong audit log, because you can no longer trust that the logged actor had any business performing the action.
Model accounting states before you model endpoints
The most common design mistake is treating every financial record as a freely editable object with create, read, update, delete. Accounting does not work that way, and an auditor will notice immediately if a posted journal entry can be silently overwritten.
Each financially material resource needs an explicit state machine. A journal entry or payment typically moves through:
draft → submitted → approved → posted → settled
Corrections do not rewind that path. A posted entry that is wrong gets a linked reversal or a compensating adjustment. The original stays intact.
A workable set of rules:
- Drafts can be edited by authorized users.
- Submitted records can be edited only by permitted roles, or returned to draft.
- Approved records require controlled amendment and fresh approval.
- Posted records cannot be overwritten or deleted, only reversed or adjusted.
- Closed-period changes need elevated authorization, a stated reason, and an audit event.
An attempt at an illegal transition, say editing a posted entry, should return a stable error and still write an audit record. The failed attempt is evidence too.
This is where an immutable double-entry ledger earns its place. Nordlet's ledger is built so that every transaction lands as a balanced, permanent entry, with balance enforced by a database trigger at commit time, and corrections happen through reversals rather than rewrites. That constraint feels restrictive during development and turns into the thing your accountant trusts during an audit.
Authorize every request, not just authenticate it
Authentication proves who is calling. It says nothing about whether that caller may touch a particular invoice, journal, or legal entity. The OWASP API Security Project puts broken object-level authorization at the top of its risk list precisely because APIs expose object identifiers that make cross-tenant access easy when checks are missing.
Every request to a financial API should be authorized against a combination of factors, not just a token:
- authenticated actor
- client application or service account
- tenant
- legal entity
- resource ID
- requested operation
- current resource state
- amount or transaction threshold
Two practical rules matter more than the rest. First, derive tenant and user context from the authenticated identity, and reject any client-supplied tenant or role claim that conflicts with it. A caller should never be able to assert which company they are acting for. Second, use deny-by-default. If no policy explicitly grants an action, it fails.
Test both directions. Horizontal access, where one tenant reaches another tenant's data, and vertical access, where an ordinary user reaches approval, posting, or export functions. Both are common and both are catastrophic in accounting data.
Nordlet's role model, with owner, admin, accountant, manager, developer, and viewer roles across multiple companies under one account, exists to make this separation enforceable rather than aspirational. A developer role that can create draft invoices should not be able to release payments.
Scope tokens to business capabilities, not CRUD verbs
For delegated access, use current OAuth 2.0 security guidance rather than reusable passwords or broad static API keys. RFC 9700 is the current Best Current Practice and sets clear requirements for clients and authorization servers: validated redirect URIs, PKCE where applicable, issuer and audience checks, short access-token lifetimes, and refresh-token rotation with revocation.
The scoping decision is where accounting-specific thinking shows. Generic scopes like read and write are close to useless for segregation of duties. Scopes should map to capabilities:
journal:createandjournal:approveare separate scopes.- Creating a payment instruction does not grant permission to release it.
- Export permission is distinct from read permission, because bulk export is a data-leakage path.
Be precise about where a given product sits on that scale. Nordlet's API keys carry per-module scopes such as sales:read and sales:write, checked on every action, which separates modules and read from write but not create from approve; where you need that finer split today, enforce it in your own service ahead of the call.
For high-value operations, it is worth assessing whether the FAPI 2.0 Security Profile applies. It was designed for APIs protecting sensitive financial data and offers stronger guarantees than baseline OAuth. Most SMB accounting integrations will not need the full profile, but payment-release and bank-connection flows are the places to consider it.
Nordlet's passwordless sign-in via one-time email links removes the reusable-password problem for human users, which is one fewer credential to rotate, leak, or phish.
Make financial writes idempotent
Networks fail mid-request. A client sends a payment instruction, the connection drops before the response arrives, and the client retries. Without protection, you now have two payments.
Idempotency keys solve this. The client sends a unique key with each write; the server records the key and its result. A retry with the same key returns the original result instead of creating a second entry. Nordlet accepts an Idempotency-Key header on every mutating call, replaying the stored response on a retry with the same payload and rejecting the same key with a changed payload, and any accounting API that skips this feature will eventually produce duplicate ledger entries that someone has to untangle by hand.
Pair this with server-side validation. Never trust the client to check that debits equal credits, that a VAT rate is valid for the country and period, or that an account exists. Validation runs on the server, before the state transition, and a rejected write produces a stable error and an audit record.
Tamper-evident audit trails and Dutch retention
The audit log is the evidence layer, and it has different requirements from ordinary business data. It should record the authorization decision, policy version, actor, object, operation, previous and new state, and reason code, without ever recording secrets or full payment credentials.
The critical property is tamper-evidence. If an administrator can quietly edit or delete audit records, the log proves nothing. Append-only storage, cryptographic chaining, or write-once retention all address this. The point is that a modification should be detectable.
Retention deserves its own policy, separate from business-record deletion. In the Netherlands, the general retention period for administrative records is seven years, and for records relating to immovable property it runs to ten. Your ordinary data-deletion and GDPR erasure workflows must not be able to destroy records or audit evidence that a legal retention rule still covers. Define legal hold, archival, and deletion rules for audit evidence independently, and map them to the jurisdictions you operate in.
Period locking supports this at the accounting layer. Once a month is closed, entries in it cannot change without elevated authorization and a logged reason. Nordlet enforces period locking so closed months stay closed, rejecting any posting dated inside a locked period even through the API, which is one of the first things a Dutch auditor checks.
Secure the edges: webhooks, exports, and integrations
The core API is usually the best-defended part of the system. The edges leak.
Webhooks are a forged-request risk. An event like sale_invoice.paid should be signed so the receiver can verify it came from you, and receivers should treat delivery as at-least-once and use the event ID for deduplication. Nordlet signs each webhook delivery with an HMAC signature over the raw body and retries failed deliveries with exponential backoff, so a receiver has to verify the signature and deduplicate by event ID rather than assume exactly-once delivery.
Exports are a bulk data-leakage path. Report exports in XLSX, PDF, or JSON pull large volumes of financial data in one operation, so they need their own permission, rate limits, and audit logging. Third-party integrations you consume should be treated as untrusted input, validated the same way you validate direct client requests.
Audit-readiness checklist for a Dutch accounting API
Use this before you claim an integration is audit-ready:
- Explicit state machine on every financial resource; posted entries cannot be overwritten
- Object-level and legal-entity authorization on every request, deny-by-default
- Tenant and role context derived from the token, not client claims
- OAuth 2.0 per RFC 9700; capability-based scopes; separate create and approve
- Segregation of duties enforced in roles, not just documented
- Idempotency keys on all financial writes
- Server-side accounting and VAT validation before state transitions
- Append-only, tamper-evident audit log with reason codes
- Retention and legal-hold rules mapped to Dutch periods (seven years, ten for property)
- Period locking on closed months
- Signed webhooks with deduplication; scoped, rate-limited, logged exports
What we would build first
If we were embedding accounting into a Dutch platform tomorrow, we would start with the state model and object-level authorization, before writing a single business endpoint. Those two decisions constrain everything downstream, and retrofitting them into a system that treated ledger entries as editable rows is genuinely painful. Idempotency and the audit log come next, because they are cheap to add early and expensive to add after data is flowing.
The rest — scoped tokens, signed webhooks, period locking — follows more easily once the foundation holds. All of it can be exercised against a sandbox company before any of it touches production data.
FAQ
Does an audit-ready API mean the business is compliant?
No, and it is worth being precise about this. Audit-readiness is a design property of the system: the ledger, controls, and evidence chain are built so an audit can succeed. Compliance with a specific tax, accounting, or security framework is a separate assessment that maps those controls to a named standard. A well-built API makes compliance work easier; it does not replace it.
How long do accounting records need to be retained in the Netherlands?
The general obligation is seven years for administrative records, extending to ten years for records tied to immovable property. Build retention and legal-hold rules that your ordinary deletion workflows cannot override.
Is a static API key enough for a financial integration?
For server-to-server automation with narrow scope it can work, but reusable static keys are hard to rotate and dangerous when leaked. For delegated access and any human-facing flow, OAuth 2.0 with short-lived, capability-scoped tokens is the stronger choice.
Why does idempotency matter so much for accounting specifically?
Because a duplicate write in most systems is an annoyance, and a duplicate write in accounting is a wrong number in the books that someone has to find and reverse. Idempotency keys make retries safe instead of risky.
Further reading
- OWASP API Security Project for the current API risk categories
- RFC 9700: OAuth 2.0 Security Best Current Practice for delegated-access requirements
- Real-time EU VAT for developers in the Netherlands for what the Dutch tax picture looks like in practice
- API conventions for scopes, idempotency, errors and webhook signing