Payment Integration: What Founders Get Wrong
Executive TL;DR
- The browser is not a witness. A user closing the tab, a flaky network or a mobile app backgrounding itself all break redirect-based confirmation — so treat the signed webhook as the only source of truth and let the redirect be a nice-to-have.
- Idempotency is not optional. Every charge request carries a key, every webhook handler is safe to run twice, and every order transition is conditional on current state. Without this, a retry is a second charge and a duplicate webhook is a second shipment.
- Store money as integer minor units plus a currency code, never as a float, and keep your own ledger of what you believe happened. Reconcile it against the provider's settlement report on a schedule — the gateway dashboard is not your accounting system.
The first payment integration is almost always successful. The customer pays, the redirect to the success page appears, and the order appears. The problems come in patterns a few weeks after the support tickets: a customer charged twice, an order marked paid that was never captured, a refund showing in the gateway but not in the product, and a month-end total that doesn’t match the bank.
All those traces are to the same mental model. Code treats it like any other function. Call it. Get a result. Move on. But a payment is a conversation between a card network, an issuing bank, a provider, and sometimes a 3-D Secure challenge in a diffIt can take seconds or minutes, succeed after your request times out, or be interrupted if the user closes the tab. middle of it.
Which means the thing you built your success logic around—the redirect back to your site—is the least reliable signal in the system. It disappears when the user closes the tab, loses signal in a lift, or pays via a UPI or wallet app that never comes back to your page. Meanwhile, the provider told you the truth reliably via webhook, and nobody was listening.
So the good question, before selecting a provider or comparing fees, is how much of this machinery you want to own.
Hosted checkout, embedded fields, direct API, or an orchestration layer — how deep should your first integration go?
| Hosted checkout | Embedded fields / SDK | Direct server-side API | Orchestration layer | |
|---|---|---|---|---|
| Time to launch | Days — a redirect, a webhook handler and a ledger | One to two weeks — fields, states, 3-D Secure handling in your UI | Longest, and most of the time goes on compliance rather than code | Weeks, and only sensible once you already run one provider well |
| PCI scope | Smallest — card data never touches your servers or your DOM | Still small if the fields are provider-hosted iframes; verify which SAQ applies to your exact integration | Largest — accepting raw card data pulls your systems into full PCI assessment | Depends on each connected provider; the orchestrator does not remove your obligations |
| Checkout UX control | Limited — provider's page, your logo and colours | High — your layout, your validation, your error copy | Total, including every edge case you must now design | High, but every provider behaves slightly differently underneath |
| Engineering and ops burden | Low — the provider maintains the hard parts | Moderate — you own client state, retries and challenge flows | High and permanent — vaulting, network rules, authentication regimes | Highest — routing logic, per-provider quirks, reconciliation across several settlement files |
| Lock-in | Moderate — the checkout page is theirs, but your ledger is yours | Moderate, with front-end work to redo on a switch | High — you have built around one provider's primitives | Lowest in theory; that is the whole pitch |
| Local methods and auth support | Best — UPI, wallets, bank redirects and 3-D Secure handled for you | Good, with more cases you must handle in your own UI | You implement and certify each one yourself | Good, and the main reason to adopt it — different providers for different markets |
| Fails when | Checkout conversion genuinely depends on an in-page flow | You assume the SDK's client callback means the money arrived | You are not a payments company and did not need to be | Adopted before you have volume, giving you two integrations to debug instead of one |
When to pick each option
Try hosted checkout first. For a first integration, it is almost always correct. Card data never reaches your systems, so your PCI scope stays at its smallest, and the provider maintains 3-D Secure, local methods, retries, and the dozens of bank-specific behaviors you have never heard of. You spend your week building the things that are really yours—the webhook handler, the order state machine, the ledger—not re-creating a card form. The only legitimate reason to go beyond it is evidence, not esthetics: a measured drop-off at the redirect.
Embedded fields for when checkout is core to the experience—marketplaces, subscriptions with upsells, anything where leaving your page costs you conversions. The fields themselves are still hosted by the provider, so your compliance position is unchanged, but you are now responsible for client-side state and every failure mode in your own UI: a declined card, an abandoned 3-D Secure challenge, and a network drop mid-confirmation. Intentionally build those states, and still treat the client callback as a hint, not a confirmation.
Direct server-facing API route only if you have a very specific and unavoidable reason (unusual flow, platform model the SDKs don’t cover, provider with no hosted option in your market). Accepting raw card details raises your PCI obligations significantly and permanently; you assume authentication regimes that change by regulation. Most teams that take this path are addressing a product problem with a compliance project.
Build orchestration layer when you have real volume and a concrete reason: a second market that requires a local provider, need to failover when one gateway degrades, or acquirer pricing leverage. Before that, there are two integrations to maintain for the benefit of a routing diagram nobody needs yet.
Rendering diagram…
// Three properties make this safe: the signature is verified, the event is
// deduplicated by the provider's ID, and the state transition is conditional.
// Skip any one of them and you will ship money bugs.
export async function handleWebhook(req: Request, res: Response) {
// 1. VERIFY FIRST. An unverified endpoint is a public API for marking orders
// paid. Use the raw body — parsed JSON will fail signature checks.
let event;
try {
event = provider.verifyWebhook(req.rawBody, req.header('signature'), SECRET);
} catch {
return res.status(400).end(); // never 200 an unverified event
}
// 2. DEDUPLICATE, then acknowledge fast. Providers retry on timeouts, so slow
// processing inside the request is how you get the same event five times.
const isNew = await events.insertIfAbsent(event.id, event); // unique on event.id
res.status(200).end();
if (!isNew) return;
// 3. TRANSITION CONDITIONALLY. Webhooks can arrive out of order — a refund
// before its capture is normal. The guard, not the handler order, protects you.
await db.tx(async (tx) => {
const order = await tx.orders.lockById(event.data.orderId);
const next = transition(order.status, event.type); // returns null if invalid
if (!next) return; // already paid, or impossible — ignore
await tx.orders.update(order.id, { status: next });
// Money moves in the ledger, in integer minor units. Never a float:
// 0.1 + 0.2 !== 0.3, and that error compounds across a million orders.
await tx.ledger.record({
orderId: order.id,
amountMinor: event.data.amountMinor, // 123450
currency: event.data.currency, // 'INR'
kind: event.type, // capture | refund | chargeback
providerEventId: event.id, // the audit trail
});
// Fulfilment is a consequence of state, never of a redirect or a client callback.
if (next === 'captured') await tx.outbox.enqueue('fulfil_order', { orderId: order.id });
});
}
// The reconciliation job is the other half: each day, compare ledger captures
// against the settlement report. An unexplained difference is a bug you want to
// find in a daily alert, not in a quarterly audit.MVP development planner
Scope, budget, and launch plan
Choose the product, platforms, and modules. The range, phases, and team update as you go. It is a planning figure, not a quote.
Newsletter
One architecture teardown or evaluation, every other week
Practical write-ups about framework choices, migrations, and scaling decisions, written for people who have to defend the call in a meeting.
Newsletter
Get the 2026 Tech Stack Guide
Join the KarmaKoders newsletter for architecture notes, stack evaluations, and build playbooks.