How to Add Payments to a Product

The hard part of payments is not taking the money. It is knowing, reliably, what state every transaction is actually in.

How to Add Payments to a Product — Troiana insight cover

In short

Use a payment provider's hosted or embedded fields so card details never reach your servers, which removes almost all compliance burden. Treat the provider's webhooks as the source of truth for payment state rather than the browser redirect, because users close tabs and networks fail. Make every handler idempotent, and store your own record of each payment's state rather than querying the provider on demand.

Never handle card details

This decides everything else.

If card numbers touch your servers, you inherit a compliance burden that is expensive and ongoing. If they do not — because the provider's hosted page or embedded fields collect them directly — that burden shrinks to a much smaller self-assessment.

So: the card data goes from the customer's browser to the provider, never through you. Your server sees a token or an identifier, nothing more.

The only reason to do otherwise is a requirement so specific that you already know why. If you are unsure, you do not have it.

The flow, and where it breaks

A typical payment: your server creates an intent with the provider; the browser collects the details and confirms; the provider processes it, possibly with an authentication step; the customer returns to your site; the provider notifies your server.

Every step between the second and the last can fail in a way that leaves you uncertain.

The customer closes the tab after paying. The redirect fails. The authentication step happens in a banking app and the browser never returns. In each case the payment succeeded and your application does not know.

This is why the browser cannot be your source of truth.

Webhooks are the source of truth

The provider tells you what happened, out of band, retried until acknowledged.

Fulfil on the webhook, not the redirect. The redirect is a user-experience convenience — show a confirmation, say thank you. The webhook is the record. If those disagree, the webhook wins.

Verify the signature on every webhook. An unverified endpoint accepting payment notifications is an endpoint where anyone can claim a payment succeeded. Providers sign requests; check the signature against your secret before doing anything.

Respond quickly, process asynchronously. Acknowledge receipt, queue the work. Providers time out and retry, and slow handlers generate duplicates.

Make handlers idempotent. Retries are guaranteed by design, so the same event will arrive twice. Record processed event identifiers and skip repeats — the same discipline as idempotency keys on your own API.

Handle out-of-order delivery. A succeeded event can arrive before the created one. Handlers that assume sequence break in production.

Model the state yourself

Keep your own record of every payment, with its own status, rather than asking the provider each time you need to know.

At minimum: your identifier, the provider's identifier, amount, currency, status, and timestamps for each transition. That gives you an auditable history, works when the provider is unreachable, and lets you reconcile.

Store money as an integer in minor units with the currency alongside — never a float, for the reasons in schema design.

Record state transitions rather than only the current state. "When did this become refunded, and what preceded it" is a question you will be asked, and only a transition log answers it.

Subscriptions add time

One-off payments are a moment. Subscriptions are a state machine running for years.

The states that matter: trialling, active, past due, cancelled but active until period end, and cancelled. Conflating the last two is a common and expensive bug — a customer who cancelled should usually keep access until the period they paid for expires.

Let the provider own the billing schedule. Renewal dates, proration, and retry logic are genuinely intricate; reimplementing them is a large amount of work to arrive at something worse.

Handle payment failure deliberately. A failed renewal is not immediate cancellation. Providers retry over a period — decide what access looks like during that window, and communicate it. Silent downgrades generate support tickets and churn.

Refunds, disputes and the awkward cases

Refunds need a path in your own system, with a reason recorded. Partial refunds complicate any reporting built on payment totals, so decide early how they are represented.

Disputes arrive as webhooks and have deadlines. If nobody sees them, you lose by default. Route them somewhere a person looks.

Currency should be stored with every amount, and be careful about converting for reporting — the rate at transaction time is not today's rate, and using the wrong one makes historical figures drift.

Tax is jurisdictional and changes. Most providers offer tax handling; use it rather than encoding rules that will be wrong within a year.

Testing

Use the provider's test mode and their published test cards, which trigger specific outcomes — success, decline, insufficient funds, authentication required. Exercise each one.

Then test the failure cases that actually bite: a webhook arriving twice, a webhook arriving out of order, a customer closing the tab mid-payment, and a webhook arriving while your handler is already processing that payment.

Those four are where production payment bugs come from, and all four are easy to simulate before launch.

Before going live

Card details never reach your servers. Webhook signatures verified. Handlers idempotent and order-tolerant. Fulfilment driven by webhooks, not redirects. Your own payment records with transition history. Money stored as integers with currency. Disputes routed to a human. Test-mode cases exercised, including the failures.

If you are taking payments for the first time and want the state model reviewed before launch, book a call.

Common questions

Should I handle card details on my own server?

No. Use the provider's hosted page or embedded fields so card data goes from the customer's browser directly to the provider. That keeps card numbers off your infrastructure and reduces your compliance obligations from an expensive ongoing burden to a much smaller self-assessment.

Should I fulfil an order on the redirect or the webhook?

The webhook. Customers close tabs, redirects fail, and authentication sometimes happens in a banking app the browser never returns from — in all of those the payment succeeded and your application never learns from the browser. Treat the redirect as a user-experience convenience and the webhook as the record.

How do I secure a payment webhook?

Verify the provider's signature on every request before acting on it, using your webhook secret. An endpoint that accepts unverified payment notifications lets anyone claim a payment succeeded. Also respond quickly and process asynchronously, since slow handlers cause provider timeouts and duplicate deliveries.

Why do payment webhooks need to be idempotent?

Because providers retry until acknowledged, so the same event will arrive more than once by design — and events can arrive out of order, with a succeeded event preceding its created event. Record processed event identifiers and skip repeats, and never assume sequence.

How should I handle a failed subscription payment?

Not as immediate cancellation. Providers retry over a period, so decide explicitly what access looks like during that window and tell the customer what is happening. Also keep 'cancelled but active until period end' distinct from 'cancelled' — conflating them removes access customers have already paid for.

Have something worth building right?