Core flows
Payments & ledger
Money has to be right every time, including when networks drop, webhooks arrive twice or the gateway is slow. Razorpay moves the money; a double-entry ledger in Postgres records exactly who is owed what.
Paying for a booking#
Swipe sideways to see the whole figure.
Webhooks: verified, de-duplicated, idempotent#
Verify signature
HMAC-SHA256Bloom filter pre-check
Redis bitmapRecord the event id
webhook_eventsApply once
def _hmac_ok(secret: str, message: bytes, signature: str) -> bool: expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature or "") # constant timedef verify_checkout_signature(self, *, order_id, payment_id, signature) -> bool: return _hmac_ok(self._key_secret, f"{order_id}|{payment_id}".encode(), signature)def verify_webhook_signature(self, *, body: bytes, signature: str) -> bool: return _hmac_ok(self._webhook_secret, body, signature)Reconciliation
If both the app confirmation and the webhook are lost, a booking could stay unpaid even though the driver paid. Every 5 minutes a job asks Razorpay what it actually recorded for orders still open after 5 minutes, and captures or fails them. The gateway is the source of truth for whether money moved.
Double-entry ledger#
Every money event is a ledger transaction: a set of entries that must sum to exactly zero (debits positive, credits negative). Balances such as “what can this host withdraw” are sums over entries, never a number that gets overwritten.
Worked example: a real booking from our tests
2 hours at ₹45/hour on a private listing: subtotal ₹90.00, GST 18% ₹16.20, driver pays ₹106.20. On completion the ledger records:
| Account | Debit (+) | Credit (−) | Meaning |
|---|---|---|---|
| driver_payments | ₹106.20 | cash received through the gateway | |
| gst_payable | ₹16.20 | tax owed to the government | |
| platform_revenue | ₹13.50 | 15% commission on ₹90 | |
| host_payable | ₹76.50 | the host's earnings | |
| sum | ₹106.20 | ₹106.20 | balanced: debits = credits |
The database itself refuses an unbalanced transaction, even if application code had a bug:
-- Double-entry invariant: every transaction must balance to zero.CREATE FUNCTION ledger_txn_balanced() RETURNS trigger LANGUAGE plpgsql AS $$BEGIN IF (SELECT COALESCE(SUM(amount), 0) FROM ledger_entries WHERE txn_id = NEW.txn_id) <> 0 THEN RAISE EXCEPTION 'ledger transaction % does not balance', NEW.txn_id; END IF; RETURN NULL;END $$;CREATE CONSTRAINT TRIGGER trg_ledger_balanced AFTER INSERT ON ledger_entries DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION ledger_txn_balanced();Host payouts and other events#
- Pay-at-venue bookings book the cash to
host_collectedinstead, so the host owes the commission rather than being owed the fare. - Late cancellations post the kept amount, split between the platform and the host the same way.
- A withdrawal moves money
host_payable → payouts_pending; marking it paid moves it toplatform_bank; a rejected payout returns it. - Refunds are target-based: the job brings a booking's total refunded amount up to a target, so a retried refund never pays out more than once.