HomeProjectsBlogContact

Adding Payments to Your Web App: What Every Developer Should Know

Taking money online is mostly a solved problem, if you avoid the classic mistakes. Hosted checkout vs embedded forms, webhooks as the source of truth, money as integers and testing every edge case.

Adding Payments to Your Web App: What Every Developer Should Know

Adding payments sounds intimidating, and for good reason: bugs here cost real money and real trust. The good news is that modern payment providers handle the hardest parts. Your job is to integrate them correctly and avoid a handful of well-known mistakes.

Never handle raw card data yourself

Storing or even touching full card numbers puts you in scope for strict security requirements (PCI DSS). The simplest, safest approach is to let the payment provider handle card details entirely:

  • Hosted checkout: you redirect users to the provider's payment page, then they come back. Least work, least risk, and it usually supports many payment methods and wallets out of the box.
  • Embedded elements: the provider's secure fields appear inside your page (often in iframes), so card data goes straight to them and never touches your servers.

Either way, your servers only ever see tokens and IDs, never card numbers.

The basic flow

  1. Your server creates a payment or checkout session with the provider, including the amount, currency and your order ID.
  2. The user completes payment on the provider's page or in the embedded form.
  3. The provider redirects the user back to your success page.
  4. The provider sends your server a webhook confirming the payment.
  5. Your server marks the order as paid and fulfils it.

The success page is not proof of payment

This is the most important rule. Users close tabs, lose connections and press Back. Some will pay successfully and never see your success page. Others could load the success URL without paying at all.

Use the webhook as the source of truth. Fulfil orders (grant access, ship goods, send receipts) when a verified webhook confirms payment, not when someone visits a URL. The success page should just show "processing" or the current order status. (Handling webhooks reliably.)

Money is an integer

Never store money as floating-point numbers:

0.1 + 0.2   // 0.30000000000000004

Store amounts as integers in the smallest currency unit: cents, paise, pence. $49.99 becomes 4999. Always store the currency alongside the amount, and only format for display at the very end. Most payment APIs already work this way.

Calculate prices on the server

Never trust a price sent from the browser. A user can edit any request. The client sends what they want to buy (product IDs, quantities, coupon codes); your server looks up the prices, applies discounts and taxes, and creates the payment for that amount. (Input validation.)

Make it idempotent

Double-clicks and retries happen. Make sure one checkout can't charge twice:

  • Disable the pay button after the first click.
  • Pass an idempotency key when creating payments, so a retried request returns the original result instead of creating a second charge.
  • Make webhook processing idempotent, since the same event can arrive more than once.

Model order states explicitly

Orders aren't just "paid" or "not paid." Common states:

pending → paid → fulfilled, plus failed, cancelled, refunded and partially_refunded.

Record every state change with a timestamp. When a customer emails about a charge, you'll want a clear history.

Subscriptions add more cases

If you charge recurring payments, handle:

  • Trials and their end
  • Renewals succeeding and failing (cards expire, banks decline)
  • Retries and grace periods before cutting access
  • Upgrades, downgrades and proration
  • Cancellations at period end vs immediately

Most providers manage the billing schedule; you listen to their webhooks and update access accordingly. Many also offer a hosted customer portal for updating cards and cancelling, which saves you building it.

Test everything in test mode

Providers offer test API keys and test card numbers that simulate success, declines, insufficient funds, authentication challenges and disputes. Test:

  • Successful payment and fulfilment
  • Declined cards and the error message users see
  • Extra authentication steps (common in many regions)
  • Webhooks arriving late, twice or out of order
  • Refunds
  • Users abandoning checkout halfway

Keep test and live keys strictly separate, in environment variables. (Config best practices.)

Taxes, receipts and legal bits

  • Taxes vary by country and sometimes by state or province. Many payment providers offer tax calculation features; for anything beyond a simple setup, ask an accountant.
  • Receipts and invoices may have legal requirements in your region.
  • Refund and cancellation policies should be clear before people pay.

Security checklist

  • Card data never touches your servers
  • Prices calculated server-side
  • Webhook signatures verified
  • Fulfilment driven by webhooks, not the success page
  • Idempotency keys on payment creation
  • Amounts stored as integers with currency
  • Secret keys only on the server, never in frontend code
  • Payment endpoints rate limited (rate limiting guide)

FAQ

How do I accept payments on my website?

Use a payment provider's hosted checkout or embedded payment elements, create payments from your server with server-calculated amounts, and fulfil orders when a verified webhook confirms payment.

Should I store credit card numbers?

No. Let your payment provider store card details and give you tokens. Handling raw card data brings strict compliance requirements and serious risk.

How should I store money in a database?

As integers in the smallest currency unit (for example, cents) with a separate currency column, never as floating-point numbers.

Keep reading

More from the workbench

All articles →