Back to BlogTroubleshooting

Rent Payments Silently Failing: Diagnosing the Money Path

Rupak Amin

Founder & Lead Engineer, RAITHub

8 min read

RAITHub ships and tests production software. See QA as a Service or talk to us.

When rent payments fail silently, your records and the payment provider have drifted apart. The usual causes: a webhook that never arrived, so money moved but your app never recorded it; a webhook processed twice, counting one payment twice; events applied out of order; or no daily reconciliation. Trace one payment end to end first, make webhook handling idempotent, then reconcile daily against the provider.

If you would rather have the money path audited and fixed for you, see how RAITHub would fix this below. First, how to find where a payment is lost.

Why do rent payments fail without anyone noticing?

Because the failure is in the gap between the payment provider and your database, and nothing in the tenant-facing flow surfaces it. The tenant's card is charged, so they see success; your app never got the confirmation, so it still shows the rent as due. Nobody looks until a tenant disputes a late fee. A payment system needs three things your records can be checked against: a webhook that reliably updates your database, protection against processing the same event twice, and a reconciliation job that compares your records with the provider's every day.

SymptomLikely causeFirst check
Tenant charged, rent still shows dueWebhook never delivered or failedDoes the event appear in the provider's webhook log as delivered?
One payment counted twiceWebhook processed twice, not idempotentDo you store and check the event ID before applying it?
Status flips back to unpaidEvents applied out of orderDo you act on an old event after a newer one?
Totals almost right, off by a littleFees, refunds or currency roundingAre amounts stored in minor units as integers?
Nobody knows until a disputeNo reconciliationIs there a daily job comparing your records to the provider?

How do you trace a single rent payment end to end?

Pick one payment a tenant says they made and follow it through every system: the provider dashboard (was it charged and settled?), the provider's webhook log (was the event sent, and did your endpoint return 200?), your application logs (did the handler run and commit?), and your database (is the payment row there, applied once, against the right lease?). The point where the trail stops is the bug. Most often it stops at the webhook log, where an event shows repeated failed delivery because the endpoint returned a 500 or timed out. The general checklist for this is in testing payments and webhooks end to end.

How do you make webhook handling idempotent?

Providers retry a webhook until your endpoint returns success, so the same event can arrive more than once. If your handler applies it each time, one rent payment is recorded twice. Make it idempotent: record each event's ID the first time you see it, and skip it if you have seen it before. Do the check and the write in one transaction so a retry mid-processing cannot double-apply.

// Idempotent webhook handler: record the event ID, apply once.
export async function handleRentPayment(event) {
  await db.$transaction(async (tx) => {
    // Unique constraint on event_id makes the insert the idempotency gate.
    const inserted = await tx.processedEvent
      .create({ data: { id: event.id } })
      .catch((e) => { if (isUniqueViolation(e)) return null; throw e })
    if (!inserted) return // already processed this exact event; do nothing

    await tx.rentPayment.create({
      data: {
        leaseId: event.data.leaseId,
        amountMinor: event.data.amount, // integer minor units, never a float
        providerRef: event.data.id,
        paidAt: new Date(event.data.created * 1000),
      },
    })
  })
}

Return 200 only after the transaction commits, so the provider stops retrying once, and only once, you have durably recorded the payment. Store money in minor units (cents, pence) as integers; floats lose precision and totals drift. The deeper patterns, including out-of-order events and the "200 but nothing updated" case, are in the short-let QA post's money section and the general webhooks guide above.

Why does reconciliation catch what webhooks miss?

Webhooks can be lost, delayed or misconfigured, so you cannot treat them as the only truth. A daily reconciliation job pulls the provider's list of settled payments for the period and compares it to your records: a payment the provider has but you do not means a missing webhook to replay; a payment you have but the provider does not means a bug or a test leak. Flag both for a human. This is the safety net that turns a silent failure into a next-morning alert, and it is standard in any system that collects rent. See how an isolated, per-landlord build keeps these records separate in multi-tenant property management SaaS.

Buy, build or hire?

OptionWhat you getChoose this when
A rent-collection or billing SaaSPayment handling, retries and reconciliation run by the vendorA standard rent tool fits your workflow and you do not need it inside your own product
Fix the money path yourselfIdempotent webhooks, minor-unit amounts and a reconciliation jobYou built the payment flow and can safely change the webhook handler
A custom money-path audit and fixEnd-to-end trace, idempotency, reconciliation, and a payments test suiteRent is failing silently and you cannot find where money is lost
A managed team on the platformThe build plus the tests that keep the money path correct on every releaseRent collection is core and must reconcile every day

How long does the fix take, and what is the risk of doing it yourself?

If you own the payment code, making the webhook handler idempotent, moving amounts to integer minor units and adding a daily reconciliation job is roughly two to five days, plus a payments test suite. The main risk of doing it yourself is changing the money path without tests: a fix that double-applies or skips a payment is worse than the original bug, because it corrupts the financial record. Reconcile historical data against the provider before and after any change, and gate the payment tests in CI.

How RAITHub would fix this

  • Scope: trace a failing payment end to end; make the webhook handler idempotent with an event-ID gate; store money in minor units; handle out-of-order and duplicate events; add a daily reconciliation job that flags gaps; backfill and verify historical records; add a payments suite gated in CI.
  • Timeline: a focused money-path audit and fix sits in the 2 to 4-week code-rescue range; backend-heavy reconciliation and accounting exports sit in the 6 to 12-week range, in phases.
  • What you receive: the reconciliation job, idempotent handlers, the payments tests in CI, a reconciled before-and-after, handover docs, the work on accounts you own, IP assigned to you and an NDA as standard.
  • Next step: a free 15-minute technical audit, then a fixed written quote.

RAITHub built PropDesk, with Stripe rent collection and 1,024 tests, and BlockEstate, a multi-tenant listings platform (6-week MVP, no MLS). See the SaaS development service, the real-estate industry page, how rent collection is built in building online rent collection, and the sibling post on fixing a double-booking race condition. To get your money path traced, book the free 15-minute audit.

Frequently asked questions

Why was my tenant charged but the rent still shows unpaid?

Almost always a webhook problem: the payment settled at the provider, but the event that should have updated your database never arrived, or your endpoint returned an error so the provider gave up retrying. Check the provider's webhook log for failed deliveries, then replay the missed event.

How do I stop a rent payment being recorded twice?

Make the webhook handler idempotent. Record each event's unique ID the first time you process it and skip it if you have seen it before, doing the check and the write in one transaction. Return success to the provider only after the transaction commits.

Why should I store rent amounts as integers?

Because floating-point numbers lose precision, so sums of many payments drift by small amounts that eventually fail reconciliation. Store money in minor units (cents, pence, paisa) as integers, and format to a currency only for display.

What is payment reconciliation and why do I need it?

A daily job that compares your recorded payments with the provider's settled payments and flags any difference. Webhooks can be lost or delayed, so reconciliation is the safety net that turns a silent failure into a next-morning alert instead of a tenant dispute weeks later.

Is it safe to change my payment code to fix this?

Only with tests and reconciliation. A careless change to the money path can double-apply or skip payments, which corrupts the financial record and is worse than the original bug. Reconcile historical data before and after, add a payments test suite, and gate it in CI.

rent payment failingstripe webhooksidempotencypayment reconciliationproptechmoney path

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.