Webhooks, Idempotency, and Reliable Writes

Every financial API will eventually receive the same request twice. The only question is what happens next.

This document explains the reliability model behind Paprel's write path: why retries are assumed rather than treated as edge cases, how idempotent writes and pre-ledger validation keep the books correct under them, what the signed webhook contract gives downstream systems, and what recovery looks like when a consumer loses events.

Retries are the normal case

Distributed systems retry. That's not a failure mode — it's how they achieve reliability at all. Your integration will see duplicates from at least three directions:

  • Timeouts. Your request reaches Paprel, the journal posts, and the response is lost on the way back. Your HTTP client sees a timeout and — correctly — retries.
  • Redeliveries. Webhook delivery is at-least-once by nature. A consumer that acknowledges slowly or restarts mid-handler will see the same event again.
  • Resubmits. A user double-clicks a button. A queue worker crashes after the API call but before marking the job done, and the job runs again. An agent re-executes a step.

Now consider what a naive accounting API does with the timeout case. The payment request arrives twice; two payment journals post. The invoice is double-paid, cash is overstated, and receivables go negative. Nobody notices until reconciliation — days or weeks later — and by then the bad entry is buried under everything posted since. In a ledger, a duplicate write isn't a duplicate row; it's an audit problem.

So the design constraint is simple: the write path must be safe to retry, because it will be retried.

The write model: idempotent in, balanced or rejected

Paprel's writes are idempotent: resubmitting the same request returns the original result instead of creating a second one. Idempotency keys deduplicate retries — resubmit the same journal post and you get the original 201 back, not a duplicate entry. The timeout scenario above resolves the way you'd want: the client retries, receives the result of the first (successful) write, and moves on. Resubmitting a payment returns the original result instead of double-paying the invoice.

The second half of the model is what happens before anything reaches the ledger. Every journal — whether posted directly via POST /v1/accounting/journals or generated by an invoice, expense, payment, adjustment, or settlement — is validated for balance first. Debits must equal credits, or the write is rejected outright. There is no code path that lets an unbalanced entry through, and there is no "post now, fix later" mode.

Put together, the write path looks like this:

Text
request
   │
   ▼
idempotency check ──── seen before? ──▶ return original result
   │ new
   ▼
balance validation ─── unbalanced? ──▶ reject (nothing written)
   │ balanced
   ▼
ledger write (journal posts)
   │
   ▼
signed webhook out (content hash included)

The ordering matters. Validation happens before the ledger, so a rejected request leaves no partial state to clean up. The webhook fires after the write, so an event always describes something that actually happened in the books.

Webhooks: signed, hashed, verifiable

Every state change emits a signed webhook with a content hash, so downstream systems can verify exactly what happened — that the event came from Paprel and that the payload wasn't altered in transit. An event that fails verification should be dropped, not processed.

Signing is symmetric with the write side: journal posts carry an x-data-signature request header (see POST /v1/accounting/journals in the API reference), and events going out carry their own signature and content hash. Integrity is checked at both edges, not assumed in the middle.

And because delivery is at-least-once, consumers should be idempotent too. The same property you rely on when writing to Paprel is the property your webhook handler owes its own database. The pattern is the standard one: process each event once by its identity — record the identity of every event you've handled, and when one arrives that you've already seen, acknowledge it and do nothing.

A useful litmus test: if every event arrived exactly twice, would your system's state be identical to receiving each one once? If yes, you've built it right.

Recovery without redelivery machinery

What happens when a consumer is down for an hour and misses events?

The honest answer: Paprel does not offer a mechanism to re-emit historical webhooks, and we think that's the right call. Webhooks are a notification channel, not a source of truth. Treating an event stream as the authoritative record means your correctness now depends on perfect delivery bookkeeping — which is exactly the fragile machinery the ledger exists to make unnecessary.

The source of truth is the ledger, and it's fully readable:

  • GL export. The full general ledger is exportable as standard CSV / JSON — at any time, for any period, no engineering review. Every journal posted emits a signed webhook event, and the full history can be rebuilt any time from this export.
  • Journals read API. GET /v1/accounting/journals returns the journal list with filtering (date range, account, reference, and more — see the API reference), so a consumer can re-derive its state for any window programmatically.

So the recovery procedure after an outage is: note the window you were down, read the journals for that window (or pull the export for the period), and reconcile your local state against what the ledger says. Because your handler is idempotent by event identity, re-deriving state from the ledger and continuing to consume live webhooks can't conflict — anything you've already processed is a no-op. This is the same property auditors rely on: the books are reconstructible from the ledger itself, not from a log of notifications about it.

The contract, in one table

PropertyWhat it means for you
Idempotent writesRetry freely; resubmitting returns the original result
Balanced-or-rejectedUnbalanced journals never reach the ledger; rejections leave no partial state
Signed webhooks + content hashVerify origin and integrity before acting on any event
At-least-once deliveryBuild consumers that process each event once by identity
GL export + journals read APIRebuild state from the ledger for any period — the ledger, not the event stream, is the source of truth

Where to go next:

Evaluate Paprel

Build in sandbox, launch with a production trial

Use sandbox for developer testing with no billing. When you are ready for real workflows, start production on a monthly plan with a 14-day free trial.

API-First Delivery
Audit-Ready Controls
Sandbox And Guided Rollout
We use cookies to improve your experience. Manage preferences or accept all.