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.
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.