Never rely on client-side frontend callbacks (e.g. browser redirects) to mark invoices as paid. Always treat incoming asynchronous webhooks as the source of truth, verified with raw-body HMAC signatures, deduplicated via distributed Redis locks, and committed to an immutable double-entry ledger.
The Payment Intent & 3D Secure Lifecycle
By using Stripe Elements or Razorpay Standard Checkout, raw credit card numbers never touch your application servers, dramatically reducing your compliance scope to the simplest PCI-DSS Self-Assessment Questionnaire (SAQ-A).
Production Webhook Handler & Ledger Transaction
Stripe and Razorpay will retry webhooks for up to 72 hours if your endpoint experiences network timeouts. The code below guarantees idempotency and updates our financial ledgers within an atomic database transaction:
// stripe-webhook.ts - Production Stripe Webhook Handler with Dual-Entry Ledger
import { buffer } from "micro";
import Stripe from "stripe";
import { pool } from "../../db";
import Redis from "ioredis";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-06-20" as any,
});
const redis = new Redis(process.env.REDIS_URL!);
export async function handleStripeWebhook(req: any, res: any) {
const buf = await buffer(req);
const sig = req.headers["stripe-signature"];
let event: Stripe.Event;
// 1. Cryptographic HMAC Signature Verification
try {
event = stripe.webhooks.constructEvent(buf, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err: any) {
console.error(`Webhook signature verification failed.`, err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// 2. Atomic Idempotency Check: Prevent duplicate payment settlement
const idempotencyKey = `stripe_event:${event.id}`;
const isNewEvent = await redis.set(idempotencyKey, "PROCESSED", "EX", 86400 * 30, "NX");
if (!isNewEvent) {
console.warn(`Duplicate Stripe event received: ${event.id}`);
return res.status(200).json({ received: true, deduplicated: true });
}
// 3. Process Payment Intent Succeeded with Dual-Entry Accounting
if (event.type === "payment_intent.succeeded") {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
const client = await pool.connect();
try {
await client.query("BEGIN");
// Update Order Status
await client.query(
`UPDATE orders SET payment_status = 'PAID', updated_at = NOW() WHERE payment_intent_id = $1`,
[paymentIntent.id]
);
// Record Double-Entry Ledger Transactions
const amountCents = paymentIntent.amount_received;
const currency = paymentIntent.currency;
const orderId = paymentIntent.metadata.orderId;
// Debit Cash/Receivables Account
await client.query(
`INSERT INTO ledger_entries (account_id, entry_type, amount_cents, currency, reference_id)
VALUES ('ACCOUNT_STRIPE_CLEARING', 'DEBIT', $1, $2, $3)`,
[amountCents, currency, orderId]
);
// Credit Revenue Account
await client.query(
`INSERT INTO ledger_entries (account_id, entry_type, amount_cents, currency, reference_id)
VALUES ('ACCOUNT_SALES_REVENUE', 'CREDIT', $1, $2, $3)`,
[amountCents, currency, orderId]
);
await client.query("COMMIT");
} catch (dbError) {
await client.query("ROLLBACK");
// Release idempotency key so Stripe retry can succeed
await redis.del(idempotencyKey);
throw dbError;
} finally {
client.release();
}
}
res.status(200).json({ received: true });
}Double-Entry Accounting Architecture
In financial software, a single balance column (like users.balance = 500) is an anti-pattern. Every financial movement must consist of matching Debit and Credit entries such that the sum of all debits always equals the sum of all credits ($\sum Debits = \sum Credits$).
| Event | Account | Entry Type | Amount |
|---|---|---|---|
| Customer purchases $100 plan | Stripe Clearing (Asset) | DEBIT | +$100.00 |
| Customer purchases $100 plan | Subscription Sales (Revenue) | CREDIT | +$100.00 |
| Stripe charges 2.9% fee ($2.90) | Payment Processing Fees (Expense) | DEBIT | +$2.90 |
| Stripe charges 2.9% fee ($2.90) | Stripe Clearing (Asset) | CREDIT | -$2.90 |
Automating Refunds, Chargebacks, and Disputes
When a customer initiates a chargeback with their credit card issuer, payment gateways emitcharge.dispute.created events. Your application should automatically lock sensitive SaaS features, send a notification email with dispute documentation upload links, and post a provisional chargeback loss entry to your accounting ledger.
Nightly Settlement Reconciliation
Every 24 hours, download the gateway payout balance report and compare it against your internalledger_entries table. Automated cron jobs flag discrepancies (> $0.01) immediately, catching currency conversion rounding errors, gateway fees, and unrecorded refunds.
Fintech Integration Checklist
✓ Raw body buffer used for HMAC signature validation
✓ Redis idempotency key locks prevent double-billing on retries
✓ Double-entry ledger architecture records debit/credit pairs
✓ 3D Secure 2.0 (3DS) enabled for SCA compliance in Europe & India
✓ All monetary amounts stored as integers in minor currency units (cents/paise)
✓ Automatic dispute webhooks alert support and lock delinquent accounts
✓ Daily automated reconciliation script validates gateway payouts
✓ Secret keys guarded in KMS/Vault with restricted developer access
Build Secure Fintech & Payment Solutions with Endurance Softwares
We engineer PCI-compliant payment gateways, subscription billing engines, multi-currency wallets, and automated reconciliation systems for enterprise platforms.
Consult With Our Fintech EngineersFrequently Asked Questions
Why store currency amounts as integers instead of floating point numbers?
Floating-point numbers in computer memory (IEEE 754) suffer from binary rounding errors (e.g., 0.1 + 0.2 = 0.30000000000000004). Always store monetary values in the smallest currency unit (e.g., cents, pence, paise) as integer BIGINT columns.
What is the difference between Stripe and Razorpay?
Stripe is the global standard for international cards, SEPA, ACH, and Apple Pay. Razorpay is the leading gateway in India with specialized support for UPI AutoPay, NetBanking across 50+ Indian banks, and RBI recurring mandates.
How do we handle multi-currency payments?
Lock the foreign exchange rate at the time the payment intent is created. Record the transaction amount in both the presentment currency (what the customer paid) and your base settlement currency in the ledger.