03 Oct 2026 · 6 min read
Post-purchase ops: capture, refund and backoffice sync
Run Magento invoice, shipment and creditmemo flows with ERP sync, idempotent webhooks and daily reconciliation jobs
Part 7 of the Checkout & Payments series — the sale is not finished when the order is placed.
The warehouse never got the order
A merchant reconciling month-end found three lists that disagreed: Magento showed 412 orders, the gateway showed 409 captures, and the ERP showed 398 inbound sales orders. Fourteen orders never reached the warehouse, three refunds existed in Magento but not at the gateway, and two gateway refunds never became credit memos. Nobody had made a mistake. Each system was correct about the events it saw; the sync between them was best-effort code that failed silently. Post-purchase operations are that sync made reliable: capture, invoicing, shipment, refund, and the backoffice ledger that must agree with all of them.
Why this matters
Checkout creates intent; post-purchase moves money and goods. Capture takes the authorized funds. Invoicing records the receivable in Magento. Shipment releases inventory and tells the warehouse to pick. Credit memos reverse all of it, partially or fully. The ERP or OMS is the system of record for finance and fulfillment, so every one of these transitions must arrive there exactly once and in order. A missed invoice overstates nothing but understates revenue; a missed shipment leaves paid orders unpicked; a missed refund double counts revenue and invites chargebacks.
The failure mode is always the same: a synchronous call to the ERP inside the order-save observer that timed out once, retried never, and logged nowhere anyone looks. Reliability here means queues, idempotent consumers, and a reconciliation job that expects drift and closes it.
How it works
Separate the Magento document flow from the backoffice flow. In Magento the canonical chain is order to invoice to shipment, with credit memos branching off the invoice. Authorize at placement, capture at invoice: online invoice triggers the gateway capture, offline invoice records a capture taken elsewhere. Create the shipment when the warehouse confirms pick, not when the label prints, so inventory reservations in MSI relieve against reality. Partial invoices and partial credit memos carry line-level quantities and amounts; keep them exact because the ERP will aggregate them and any rounding shortcut compounds across partials.
Publish every transition as an event. The reliable pattern is a queue publisher on sales_order_invoice_save_after, sales_order_shipment_save_after, and sales_order_creditmemo_save_after that emits a message with entity_type, entity_id, increment_id, order_id, and a monetary snapshot. A consumer then calls the ERP with an idempotency key derived from entity_type + entity_id, so redelivery updates the same ERP record instead of creating a duplicate. Process messages in order per order_id so a credit memo never lands before its invoice.
Webhooks from the gateway close the loop on money. A capture-settled or refund-settled webhook must resolve to exactly one invoice or credit memo by gateway transaction_id before touching the ledger. Verify the signature, look up the transaction on the order payment, and skip already-closed matches. Route webhook handling through the same queue as the outbound sync so a burst of redeliveries serializes on the order lock instead of racing.
Sync the master data the documents reference. Payment method codes, tax rules, shipping methods, and SKU mappings must exist in the ERP before the first order message arrives. A new payment method enabled in Magento but unknown to the ERP mapping table parks every subsequent invoice in the dead-letter queue. Keep the mapping in config, version it, and alert on unmapped codes rather than dropping the message.
Reconcile daily, because queues and webhooks reduce drift but never eliminate it. Three queries close the books: Magento invoices without matching ERP sales invoices, gateway captures without Magento online invoices, and ERP credit notes without Magento credit memos. Run them as a scheduled job that posts a summary and the offending increment IDs to the ops channel. The job is not an admission of failure; it is the control that proves the sync held.
Useful ground truth while debugging:
SELECT increment_id, state, grand_total, total_invoiced, total_refunded
FROM sales_order WHERE created_at > DATE_SUB(NOW(), INTERVAL 7 DAY);
SELECT increment_id, state, grand_total FROM sales_invoice
WHERE order_id = :order_id;
SELECT increment_id, state FROM sales_creditmemo
WHERE order_id = :order_id;
Compare those three result sets against the gateway settlement report and the ERP sales journal for the same window. The mismatch tells you which direction the sync broke.
Pitfalls
- Syncing inside the order-save observer. A blocking ERP call in the request path couples checkout latency to ERP uptime and loses the message on timeout. Publish to a queue and acknowledge fast.
- Capturing before the invoice exists. Calling the gateway capture directly and creating the invoice later leaves money taken with no Magento document. Always capture through the invoice flow so documents and funds stay paired.
- Refunding offline when the gateway took the money. An offline credit memo adjusts Magento only; the customer is never repaid at the gateway. Match the credit memo type to where the funds actually are, online for gateway money, offline for cash or bank transfer.
- Retrying without an idempotency key. A blind retry of the ERP create call duplicates the sales order on every transient failure. Key every outbound call on the Magento entity and make the ERP upsert on that key.
- Skipping reconciliation because the queue looks healthy. Queue depth at zero proves nothing about messages that failed before publish or webhooks that never arrived. The daily three-way diff is the only proof the ledgers agree.
Field check on a live store
Run the three-way comparison for the last seven days today. List Magento invoices missing from the ERP, gateway captures missing from Magento, and ERP credit notes missing from Magento, each with increment IDs and amounts. For every hit, trace whether the message was never published, published but dead-lettered, or acknowledged but unmapped, and fix that tier rather than hand-editing the ledgers. Then force the failure paths in staging: kill the ERP endpoint mid-invoice, redeliver a refund webhook three times, and create a partial credit memo against a partial invoice. Each case must converge to one invoice, one credit memo, and one ERP record with matching totals.
Closing the series
This Checkout and Payments arc moved from cart to cash to warehouse: quote and totals discipline, payment integration, idempotent placement, defensive shipping rates, and finally the post-purchase ledger that must agree with the gateway and the backoffice. The throughline is that checkout never stands alone. Every click at the storefront becomes money movement, inventory relief, and an ERP record, and the series held each handoff to the same standard: exactly once, in order, and verifiable the next morning.
This was the final post in the Checkout & Payments series.