The checks that only matter after you have real customers
A payment integration that works is easy to build. One that behaves correctly when the network does not, when a webhook arrives twice, or when a provider succeeds and forgets to mention it — that is the part discovered in production, usually by a customer. This is the list for whoever wrote the code. If you are checking the store rather than the integration, test your checkout before you go live is the version for that job.
1. Before the first line of code
- You can name the currencies you will charge in, and have confirmed each provider processes them.
- You know which payment methods your largest markets actually use, rather than assuming cards.
- You know whether your business category attracts holds or reserves at your provider.
- You have decided whether card data will ever touch your server — and if the answer is anything but "no", you have read how PCI scope works first.
2. Creating a charge
- Every charge carries an idempotency key.
- The key is derived from the order, not generated per HTTP request — you can reproduce it from data you already store.
- The key is stored with the order before the request is sent, not after the response comes back.
- You have tested that sending the same charge twice produces one payment.
- Amounts are handled in a type that cannot round — no floats anywhere between the cart and the provider.
- The amount sent matches the amount displayed, in the unit the provider expects. (Major units and minor units are both common, and the mismatch is a factor of one hundred.)
3. Confirming the payment
- Orders are marked paid from a server-side confirmation, never from the browser redirect.
- Your success URL does nothing but show a page — no state changes, no fulfilment.
- Every inbound webhook signature is verified before the payload is trusted.
- Signature comparison is constant-time.
- A webhook that fails verification is rejected, not logged and processed anyway.
- You have confirmed you are verifying against the raw request body — parsing and re-serialising the JSON first breaks the signature for every provider.
- Webhook handlers are idempotent: the same event delivered twice changes nothing the second time.
- Handlers tolerate out-of-order delivery and never move an order backwards from a later state.
4. When things fail
- You distinguish technical failures from hard declines from soft declines, and treat them differently — the three categories are in why payments fail.
- A hard decline is never retried through another provider.
- A timeout triggers a status query, not a blind retry.
- There is a defined maximum time an order may sit in an unknown state before a human sees it.
- You have deliberately tested the case where the provider charged the card and the response never arrived. This is the single most valuable test on this page.
- Your failover path — if you have one — carries the original idempotency key. If it does not, it is a double-charge generator; see how failover is actually built.
- The customer sees a message that tells them what to do next, not a raw provider error code.
5. Money correctness
- Refunds work end to end, and the money is visible as returned on the provider's side too.
- Partial refunds and partial captures behave, if you support them.
- A refund issued at the provider rather than in your system still reaches your order state.
- Disputes and chargebacks land somewhere a human will look.
- You can answer "what did we take yesterday" across every provider without a spreadsheet merge — reconciliation across providers is what this looks like when it is missing.
6. Risk and abuse
- There is a limit on payment attempts per customer in a window.
- There is a limit on distinct cards from one customer or one address.
- Repeated provider rejections trigger something automatically rather than waiting for a report — the pattern is described in card testing on signup forms.
- You know what your provider's decline rate threshold is, and you monitor against it.
7. Operations
- API keys live in a secret store, not in configuration files or environment dumps.
- Payment logs contain no card data, no full tokens and no webhook secrets.
- There is an alert on payment failure rate, not just on server errors — a provider outage often produces perfectly healthy 200s from your own stack.
- One person other than the author can find the payment logs and explain what a stuck order means.
- You have done one real payment with live keys, for the smallest amount possible, and refunded it.
8. Multi-provider readiness
Skip this section entirely if you run one provider and intend to keep it that way — that is a legitimate position and most of what follows is cost without benefit. It becomes relevant the moment a second provider is taking real money, or the moment you write a conditional in application code that chooses between providers.
- The routing decision is configuration you can change without a deployment, not a branch in a checkout controller.
- A retry that moves to a second provider keeps an identity your own system owns — a provider's idempotency key is scoped to that provider and does not travel.
- Provider health is tracked per provider, over a rolling window, counting technical failures only. A decline is the provider working correctly and must never mark it unhealthy.
- An unhealthy provider is re-tested after a cooldown rather than being written off permanently by a fifteen-minute incident.
- Every transaction carries one internal identifier assigned at capture, with the provider's own reference stored alongside rather than instead — how failover is actually built covers why this is load-bearing.
- You know that stored payment methods do not transfer between providers, and your renewal flow does not silently depend on them doing so.
- Your second provider has taken a real payment. An account you opened but never used is not a fallback.
9. Measurement
Everything above is correctness. This section is how you find out that something has drifted, which is the failure mode that costs the most over time because nothing alerts on it.
- Approval rate is reported split by country, by payment method and by provider — not as one blended figure that averages a healthy market with a broken one.
- First payments and renewals are counted separately. They have different approval rates and different failure modes, and combining them hides the one that costs existing revenue.
- The mix of decline reasons is tracked over time, not just the total. A shift in the mix is a signal well before the aggregate moves — why payments fail has the taxonomy.
- Technical failure rate is alerted on separately from declines. It is the only one of the two that is yours to fix.
- You can state how many cancellations last month began as a failed charge rather than a customer decision.
- Someone looks at these numbers on a schedule. A dashboard nobody opens is not measurement.
How to use this without stopping everything
A checklist this long invites two bad responses: doing all of it before shipping anything, or filing it and doing none of it. Neither is necessary, because the items are not equally urgent and they do not all become relevant at the same stage.
Sections 3 and 4 are correctness and never wait. Confirming payments server-side, verifying webhook signatures and carrying an idempotency key are wrong to omit on day one, not at scale. Each is a defect that is merely waiting for enough traffic to become an incident, and all three are cheap while there is one code path.
Section 6 becomes urgent the week a signup form is findable, usually faster than teams expect, and typically before any other item on this list.
Sections 5 and 9 arrive with the second provider, and are usually noticed a quarter after they arrived. Section 8 is only relevant once there is a second provider at all — attempting it earlier is building for a problem you do not have.
The practical way through it is a single pass, marking each item yes, no, or not yet relevant, and resisting the urge to fix anything during that pass. What matters is the resulting list of "no" answers in sections 3 and 4, which is almost always short and almost always contains something worth doing this week. The reasoning behind why these jobs exist at all is in what payment infrastructure has to do.
Explore each part of the stack
Each section above has a page behind it that explains the reasoning rather than just the check:
- Payment failover — detecting an unhealthy provider, rerouting, and why the retry must keep its identity.
- Orchestration versus a single gateway — whether you need any of this yet, and what the honest alternatives are.
- Accepting global payments — method, currency and settlement as three separate problems.
- Payments for hosting and web companies — what changes when revenue is mostly unattended renewals.
- The full infrastructure guide — all of the jobs above in one place.
How PaymentHood fits
Sections 2 and 3 are the ones teams most often get partly right, and they are the ones PaymentHood handles centrally. Charge creation is idempotent on your own order reference, checked before any provider is contacted. Provider webhook signatures are verified in the layer rather than by each integration. Orders are confirmed server-side against the provider, and the webhooks PaymentHood sends you are themselves signed with HMAC-SHA256 over a timestamped payload, so your own handler can verify them the same way — the scheme and sample verification code are in the API documentation (opens in new tab).
Sections 5 through 7 stay yours, as they should — they are about your business, not your payment layer. What changes is that you do the work once instead of once per provider.
Start from a layer that already passes sections 2 and 3
Create a free account and take a sandbox payment. The idempotency, signature verification and server-side confirmation are already in place.