slopware 101 · step 26 of 35
The money arrived. Where is the order?
by Damla Su Bilge · 4 min read
A customer can complete a payment while your database still shows an unpaid order. Each side may be working correctly in isolation; the missing part is how the two systems exchange and retain the result.
Before code: who can take the money
Taking money starts before any integration. Which payment providers can serve you depends on the country your business is registered in. The moment money moves, questions about invoicing, tax and consumer law start too. Those belong with an accountant and the provider's own terms, not in your code. This chapter covers the engineering part: making sure every payment the provider records becomes exactly one order in your database.
A redirect is not a receipt
The flow gets built wrong because a redirect looks like a return. The customer pays and comes back to a success page, and the code that writes the order sits on that page. But the trip home belongs to the browser, and the browser belongs to the customer. They may go through an extra authentication step with their bank, their connection may drop, or they may close the tab. The provider has the money, and your success page never ran.
So the order has to be decided between your server and the provider's. Taking iyzico as the example, there are two separate mechanisms, and it helps to keep them apart.
- The Checkout Form return. After payment, iyzico sends the customer's browser back to the callback URL you supplied. Treat that return as a hint that something happened. Your server then asks iyzico for the result of that checkout through the documented retrieve call, and decides from that answer, not from anything in the browser's request.
- Webhooks. iyzico can also notify your server directly, with a JSON POST to an endpoint you configure. That delivery does not depend on the browser returning, though it can still be delayed or fail.
Webhook signatures are a feature you enable on the merchant account. With the V3 signature, iyzico computes an HMAC-SHA256 over a documented, ordered set of fields, and that set depends on the flow. It is not a hash of the raw request body. Build the check from the current documentation for your flow, and do not copy a recipe from a blog post or from another provider.
What the handler must do before saying yes
Before reading the steps, think about what your endpoint promises when it answers 200. To the provider, a 2xx means you have the event and it can stop sending. So answer that only once the event is durably accepted.
- Verify the signature as documented. If verification fails, do not change any order.
- Find the order using your own reference, the one you sent when you created the payment.
- Match the details. Obtain the amount, currency and final status from the provider's authenticated result, retrieving the payment if the notification does not include all required details. Match them to the order you created. A valid signature on a payment for a different amount is still not this order's payment.
- In one database transaction, record the event identity under a unique constraint. Only a verified successful payment result may move the matching order into paid and create fulfilment work; pending or failed results take their own permitted transitions without fulfilment. Guard the order update by its current state and enforce a unique business key for fulfilment, such as the order and fulfilment kind, so a different event id for the same payment cannot enqueue the job again. Insert that job in an outbox table in the same database. Publishing directly to an unrelated queue is not part of the database transaction. If an event is already recorded and its transaction committed, report success without repeating its effects.
- Commit, then return 2xx.
A worker reads the committed outbox and performs fulfilment, such as sending a download link or notifying the warehouse. It can retry, so the fulfilment operation also needs to tolerate repeats. If it ran inside the webhook request and failed, you would either lose it or make the provider resend a payment you had already accepted.
Define allowed transitions for payment attempts and orders explicitly. A late pending notification must not overwrite a confirmed paid result. Refunds and disputes are separate events with their own transitions; “paid” does not mean no later state can exist.
Reconciliation
A notification can still go missing. Your endpoint might have been down through every retry, or a deploy might have broken signature checks for an afternoon. So run a reconciliation job that lists recent successful payments from the provider, compares them with your paid orders, and alerts on any mismatch.
Try the manual version in test mode: complete a payment, close the browser before its return page, and compare the provider's result with your order. Replay the test notification and check that fulfilment is still represented once. Then compare several test payments with their orders to exercise reconciliation.


