GUIDE / WEB STORE RECONCILIATION

Reconciling a game web store: three records, one daily match

A reconciliation routine for a game web store: the three records that must agree (your order table from the signed webhook, the store's order list, your wallet ledger), the daily three-way match, what each mismatch means, how refunds and retries fit, and what to keep for the auditor.

Updated

A web store adds a second place where your players pay, and so a second set of books. Reconciliation is the daily proof that every paid order became exactly one grant, that every grant has a paid order behind it, and that every refund reversed both. It's dull, it's an hour to set up, and it's what turns "I paid and got nothing" from a crisis into a lookup.

The three records

Record Where it lives Written by Source of truth for
Order table Your backend Your order.paid / order.refunded webhook handler What you were told was paid
Store order list Appside Studio → Orders The store, when checkout completes What the store says was paid
Wallet ledger Your game's backend Your grant code What the player actually received

All three carry the same order_id. That's the whole trick: one ID, three places, match on it.

The order table is the one most publishers don't have. Build it from the webhook: every order.paid inserts a row with order_id, player_id, sku, credits, price, currency, received time and the webhook event_id; every order.refunded updates the row's state and stores the refund time. Keep the raw webhook body next to the row; the auditor will ask. The webhook reference lists every field.

The daily three-way match

Run it once a day, for yesterday (UTC), automatically.

  1. Order table vs store order list. Export the store's orders for the day; compare by order_id. Every store order should have a row; every row should have a store order.
  2. Order table vs wallet ledger. Every paid row should have exactly one grant with the same order_id, player_id and credits. Every refunded row should have a grant and a reversal.
  3. Totals. Sum of paid prices per currency, minus refunds, in the order table, equals the same sum in the store's list.

Three queries. If all three come back empty, nobody needs to look.

What each mismatch means

Mismatch Likely cause Action
Store order, no order-table row Webhook not received or rejected (bad signature, 5xx) Check Studio → Integration → Health: last acknowledgement, failures in 24 h, dead letters. Appside retries with backoff for 72 h; fix the endpoint and the retry lands
Order-table row, no grant Your handler acknowledged before granting, or the grant failed Grant now, by order_id, inside the same transaction as the dedupe
Two grants, one order Dedupe keyed on something other than order_id (e.g. event_id, which differs for the refund) Reverse one; fix the key
Grant, no order anywhere Manual grant, a support tool, or a bug Investigate; this is the row that becomes a fraud case
Refunded in store, still granted order.refunded not handled Reverse the grant; handle the event (its event_id is <order_id>:refund)
Totals differ, rows match Currency or price mismatch on one row Compare price and currency field by field

Design rules that make the match boring

Refunds, retries and test orders

Refunds have their own page; for reconciliation the rule is that a refund is a state change plus a wallet reversal, both keyed on order_id.

Retries can arrive late. A delivery that failed on Monday can land on Wednesday, inside the 72-hour window; Wednesday's match will show a Monday order. Match on the order's paid time, not the time you received the webhook.

Test orders (mode: "test") flow through the same handler and the same match, which is the point: run the reconciliation on test data for a week before the store goes live, and the first live day has nothing new in it.

What to keep

For each order: the three rows, the raw webhook, your grant record, and any refund. For each day: the match results, including "nothing to report". That set answers every question a finance team, a payment provider or an auditor asks about web revenue, with the order_id as the thread.

Appcharge's framing of finance ops

Appcharge's post of June 4, 2025 lists what moving payments off the App Store adds to a finance team's plate: sales tax, chargebacks and support, and reconciliation across providers and currencies, and offers the choice between building on a PSP and using a merchant of record. Order reconciliation, the part above, is the piece that stays yours on any provider, because only your backend knows what was granted. It's one table, three queries and a daily job, and you can verify it with test orders before accepting player purchases.

Create your store free. No card needed to start; 3.9% per successful payment.

Frequently asked questions

What do I reconcile in a web store?

Three records that describe the same purchases. Your own order table, built from the signed order.paid and order.refunded webhooks. The store's order list in the Studio. And your game's wallet ledger, where the grant happened. Every paid order should appear once in all three with the same ID, pack and amount.

How often should I reconcile?

Daily, automatically, with a human only looking at the exceptions. A paid order with no grant is a support ticket within hours; a grant with no order is revenue leaking. Monthly is too late for either.

What if a webhook never arrived?

Appside retries with exponential backoff for 72 hours, and the order and its webhook are written in one transaction, so a paid order is never left without a delivery attempt. Studio → Integration → Health shows the last acknowledgement, failures in the last 24 hours and dead letters. Your dedupe on order_id makes a late delivery land exactly once.

Do refunds break the match?

Not if you treat them as their own row. order.refunded carries the same order_id, so the order stays in all three records with a refunded state and a reversal in the wallet ledger. Deleting refunded orders is what breaks reconciliation.

Does Appside handle payouts and tax reconciliation?

This page covers order reconciliation, the part your game owns. Payout statements and tax come from the payment side and from your own accounting; keep the order ID on every row so they join to the same table.

Sources

Related

SEE IT YOURSELF

Build your store
before the first call.

Try the live demo ↗Explore Studio
LET’S TALK

Book a demo

Connect with Oded to arrange a walkthrough of Appside.

Contact Oded on LinkedIn ↗