Article focus
Starting point: one contract between the order and provider
Backend must not pass an arbitrary browser amount to a provider or change an order without a verifiable payment state.
The server validates the order, amount, currency and allowed action before it creates a payment attempt with an internal id and idempotency key. Only then does it create a safe redirect or checkout URL.
Internal order id, payment attempt id and provider payment id are stored together. This makes retries safe and lets support and accounting trace operations without guessing.
- Do not trust a final payment amount from the frontend.
- Keep order, payment attempt and refund as separate entities.
- Lock currency and rounding rules on backend.
- Return the same create-payment result for the same idempotency key.
Implementation: state machine and idempotent webhook handlers
A webhook can arrive more than once, later than return URL or out of order, so business state changes only through allowed transitions.
Backend validates signature, amount, currency, correlation id and event source, then checks whether the received transition is allowed. A late failed callback cannot replace paid, and a repeated paid event cannot deliver the order twice.
A Laravel job or Django worker can perform secondary actions asynchronously: notifications, CRM sync, fiscal workflow or access delivery. The critical webhook response stays fast even when a secondary system is unavailable.
- Store raw event and normalized verification result.
- Separate webhook intake from heavy background actions.
- Use retry policy and manual review for unresolved mismatches.
- Do not execute irreversible actions before verified paid.
After launch: refunds, observability and reconciliation
The team needs a ledger for payment attempts, refunds, retries and differences from a provider report.
A refund starts by validating the original payment, available amount and operator role. After the request is sent, backend creates a separate refund record and confirms the final result with the same safe status mechanism.
Structured logs, queue metrics and daily provider-report reconciliation expose not only API errors but also payments that succeeded at a provider yet did not reach an internal system.
- Do not log card data or secrets.
- Let operators search by order id, payment id and correlation id.
- Separate initiated from confirmed refunds.
- Track webhook retries and manual-review tasks separately.
FAQ
Do we need a payment attempt if an order already exists?
Yes. Order and payment attempt have different lifecycles: one order can have several attempts, a cancellation or a refund.
How should a repeated webhook be handled?
Validate its signature and event id, retain it for audit, then apply only an allowed idempotent state transition.
Can CRM sync run inside a webhook?
Prefer accepting and validating the event quickly, then run CRM sync through a durable queue with retry and error logging.