Your Checkout Redirect Is Not Payment Confirmation
The Redirect Is Only Part of the Customer Experience
A redirect is useful. It helps the customer know what happened after checkout. For example:
/payment/success/payment/cancel/order/thank-you
Those pages are good for user experience. But they should not be the only thing that decides whether an order is paid. Why? Because browsers are unreliable. A customer can close the tab. A phone connection can drop. A browser extension can block something. The redirect can be delayed. The customer can refresh the page. The success page can be opened more than once. A user can manually visit a URL if the logic is weak. That does not mean the payment was confirmed. It only means the page was visited.
Payment Confirmation Belongs on the Backend
The backend needs a trusted event from the payment system. That is usually a webhook. A cleaner flow looks like this:
- Customer starts checkout
- Backend creates payment session
- Customer is redirected to hosted checkout
- Payment provider processes payment
- Payment provider sends webhook to merchant backend
- Backend verifies webhook
- Backend updates order status
- Customer sees final status
The important part is this: Webhook received + verified = backend payment confirmation. The success page should not be the source of truth. The webhook should be.
This Applies to Every Ecommerce Stack
This is not only a WooCommerce problem. The same rule applies to:
- Shopify apps
- Custom PHP shops
- Laravel stores
- Next.js storefronts
- Node.js backends
- Headless ecommerce
- Marketplace platforms
- Digital product platforms
- SaaS checkout systems
- Mobile app backends
The frontend can guide the customer. The backend must confirm the money.
A Simple Bad Pattern
This is the risky version:
if current_url == "/payment/success":
mark_order_as_paid()
This is weak because the decision depends on a browser route. A better version:
on_webhook_received(event):
verify_signature(event)
if event.status == "paid":
mark_order_as_paid(event.order_id)
The customer redirect can still show a nice page. But the order should only become paid after a trusted backend event.
Webhooks Can Arrive Before or After the Redirect
A common reason developers get confused is timing. Sometimes this happens:
- Payment completed
- Webhook arrives
- Order marked paid
- Customer redirects to success page
Other times this happens:
- Payment completed
- Customer redirects to success page
- Webhook arrives later
- Order marked paid later
Both are normal. So the success page should handle both cases. For example: "Payment received. We are confirming your order." Then the page can poll the backend or show a pending confirmation state. The mistake is assuming the redirect and webhook will always happen in the same order. They will not.
Use Clear Internal Statuses
A good payment integration needs more than paid and failed. Real payment sessions have more states. For example:
createdpendingprocessingpaidfailedexpiredcancelledreviewrefunded
This gives your system more control. A checkout can be created but not paid. A payment can be pending. A provider can request review. A session can expire. A customer can cancel. A refund can happen later. If your system only understands success or failure, you will lose important information.
Do Not Trust the Frontend Amount
Another serious mistake is trusting payment data from the frontend. Bad idea:
{
"amount": 49.99,
"currency": "EUR",
"order_id": "123"
}
If this data comes directly from the browser and your backend does not verify it, you have a problem. The backend should calculate or load the real order amount from the database. Better flow:
- Frontend sends
order_id - Backend loads order from database
- Backend calculates amount
- Backend creates payment session
- Provider returns checkout URL
- Frontend redirects customer
The browser should not decide the final price. The backend should.
Verify the Webhook
A webhook endpoint should not blindly accept requests. At minimum, think about:
- Signature verification
- API key or token validation
- Expected event type
- Matching payment session ID
- Matching amount and currency
- Duplicate event handling
- Safe logging
- Internal status mapping
A webhook endpoint is a public URL. Treat it like one. Bad pattern:
on_webhook_received(request):
mark_order_as_paid(request.order_id)
Better pattern:
on_webhook_received(request):
verify_signature(request)
event = parse_event(request)
payment = find_payment(event.payment_id)
if payment.already_processed:
return success_response()
if event.amount != payment.expected_amount:
flag_for_review()
return success_response()
if event.currency != payment.expected_currency:
flag_for_review()
return success_response()
update_payment_status(event.status)
update_order_status(payment.order_id)
mark_event_processed(event.id)
return success_response()
The exact code depends on your stack, but the principle stays the same. Verify first. Update second. Log everything.
Make Webhook Processing Idempotent
Payment providers may send the same webhook more than once. That is normal. Your system must handle duplicates safely. A duplicate webhook should not create duplicate wallet credits, duplicate invoices, duplicate licenses, duplicate subscription periods, or duplicate order fulfillment.
Use something like:
event_id |
payment_id |
order_id |
processed_at |
status_before |
status_after |
|---|
Before applying a webhook event, check whether it was already processed:
if event_id_already_processed(event.id):
return success_response()
This one rule prevents many ugly bugs.
Return Success After Safe Handling
If your webhook endpoint receives a valid event and processes it safely, return a success response. If you return an error after processing, the provider may retry. That can be okay if your system is idempotent, but it creates noise. A clean webhook handler should:
- Receive event
- Verify event
- Process safely
- Store event
- Return success
If something is suspicious, you can still store it and flag it for review. Do not lose the event.
Your Customer Page Should Be Honest
The customer success page should not lie. If the backend has already confirmed the payment, show: "Payment confirmed. Your order is now being processed." If the webhook has not arrived yet, show: "Payment received. We are confirming the final status." If the customer cancelled, show: "Payment was not completed. You can try again or choose another method." If the session expired, show: "This payment session has expired. Please create a new checkout." Clear status messages reduce support tickets.
Logs Are Part of the Product
Payment logs are not just developer tools. They help merchants, support teams, and finance teams understand what happened. A useful payment log should show:
- Checkout session created
- Customer redirected
- Provider selected
- Payment method selected
- Webhook received
- Status changed from pending to paid
- Order updated
- Customer redirect completed
When something fails, the log should also show the reason if available. Without logs, support becomes guesswork. And with payments, guesswork is expensive.
Payment Routing Makes This More Important
If your system supports more than one provider or method, clean status handling becomes even more important. A payment may be routed to:
- Card
- Bank transfer
- Wallet
- Crypto
- Local payment method
- Manual payment link
- Fallback provider
Each route may have its own provider status names. Your internal system should normalize them. Example:
- Provider A:
completed→ internal:paid - Provider B:
success→ internal:paid - Provider C:
settled→ internal:paid - Provider D:
confirmed→ internal:paid
This keeps your merchant dashboard and shop integration clean. The merchant should not need to understand every provider's internal language.
A Practical Checklist
Before your payment integration goes live, ask:
- Can the backend create the payment session?
- Is the amount loaded from the database, not trusted from the browser?
- Is the customer redirected to a secure hosted checkout?
- Does the backend receive webhooks?
- Are webhooks verified?
- Are webhook events idempotent?
- Are amount and currency checked?
- Are internal statuses normalized?
- Can the order be pending while confirmation is still processing?
- Can the system handle expired sessions?
- Can the customer retry payment?
- Can support see a useful payment log?
- Can the merchant understand what happened without reading server logs?
If not, the integration is not finished. It only works when everything goes perfectly. Production ecommerce is not perfect.
Final Thought
A success page is not a payment confirmation. It is only part of the customer journey. The real confirmation should happen on the backend through verified webhook events, safe status handling, and clear order updates. That is true whether you are building for WooCommerce, Shopify, Laravel, Next.js, a custom PHP shop, or a headless ecommerce system. A good checkout should feel simple to the customer. But behind the scenes, the system needs to be strict. Confirm the payment on the backend. Keep the redirect for the customer experience. Log what happened. Handle retries safely. Never build a payment system that only works on the happy path.
EcomTrade24 Pay is built around hosted checkout, payment links, API/webhook automation, and Smart Routing for merchants who need more than one fragile payment path. https://pay.ecomtrade24.com
Comments
No comments yet. Start the discussion.