All articles
Playbook8 min read

Refund handling breaks in quiet ways, so test in-app purchase refunds in the sandbox before a real customer does

Your refund handling only runs after a customer is already gone, so a bug in it stays invisible until it costs real money. Both stores let you fire a refund in a test environment first. Here is how to test in-app purchase refunds on the App Store and Google Play before one is real.

An iPhone and an Android phone under a magnifying glass on a dark desk, standing in for testing in-app purchase refunds before they are real

Key takeaways

  • Xcode's StoreKit testing lets you refund a purchase locally by clicking the refund arrow in the Transaction Manager, which fires your app's Transaction.updates listener, but it never contacts Apple, so no App Store Server Notification is sent.
  • To test the server side on Apple, point a sandbox App Store Server Notifications V2 URL at your backend: a sandbox refund then delivers a real REFUND, and a refund request delivers a CONSUMPTION_REQUEST, to your server.
  • Apple's Request a Test Notification endpoint sends a notification of type TEST to your configured URL and returns a testNotificationToken, so you can confirm your webhook is reachable before any real event fires.
  • Apple's sandbox never retries a failed notification, so a webhook that is down when the sandbox fires drops the event with no second attempt, the same class of miss that costs you a real refund window later.
  • Google Play gives license testers a payment method called Test card, approves then charges back, which fires a PendingRefundReviewNotification moments after purchase so you can rehearse your 24-hour orders.reviewrefund response.
  • For a license tester on Google Play, an unacknowledged purchase is auto-refunded after 3 minutes instead of the 3 days production waits, so a broken acknowledgment path fails fast and loud in testing.
  • A refund handler you never tested is the one that keeps a refunded customer's paid access live, and from August 3, 2026 an untested Google Play chargeback response can cost you the purchase price less Play's service fee plus the bank's fee.

Your refund handling is the one code path that only runs after the customer is already gone. Nothing in your normal QA touches it, because to reach it you have to actually get refunded. So it ships untested, sits quiet for months, and then fails on a real refund, where the failure costs money instead of a red test. The fix is to stop treating a refund as something that happens to you and start triggering one on purpose. Both Apple and Google let you fire a refund in a test environment and watch your server react. Here is how to test in-app purchase refunds on the App Store and Google Play before a paying customer proves your handler was broken.

The three environments a refund can fire in, and only one is production

There are three separate places an Apple or Google refund can be triggered while you build, and they are not interchangeable. Two of them are yours to trigger on demand. The third is production, where you never want to meet a refund bug for the first time. The trap is assuming the easy one, local testing in Xcode, proves your whole pipeline. It proves your app. It says nothing about your server.

Xcode StoreKit testing is local, so it exercises your app and nothing else

Xcode's built-in StoreKit testing runs against a configuration file on your Mac, with no round trip to Apple. Open the StoreKit Transaction Manager from the debug bar, select a purchased transaction, and click the curved refund arrow. The transaction flips to refunded and your app's Transaction.updates listener fires, exactly as it would in the wild. You can also call beginRefundRequest to present the real refund sheet, and in the Xcode environment the issue you pick maps one to one onto a RevocationReason, with the refund applied immediately. This is the fastest way to prove your client cuts access the moment revocationDate goes non-nil. It is also the whole of what local testing can tell you, because nothing here ever reaches Apple's servers, so no App Store Server Notification is sent. Your backend does not learn a thing.

The sandbox is where your server finally hears about a refund

To test the half of your integration that decides money, your server, you need Apple's sandbox. Configure a sandbox App Store Server Notifications V2 URL in App Store Connect, sign a sandbox tester in on a device, and buy. Now a refund in the sandbox delivers a real REFUND notification to your backend, and a refund request on a consumable or auto-renewable delivers a CONSUMPTION_REQUEST, the same signed payload your production server will get. Before you trigger anything, call the Request a Test Notification endpoint. It tells the App Store server to send a notification of type TEST to your configured URL and hands you a testNotificationToken, which you pass to Get Test Notification Status to confirm delivery. If that round trip does not work, no real notification will either.

EnvironmentWhat it can triggerWhat it provesWhat it cannot do
Xcode StoreKit testingA refund via the Transaction Manager or the beginRefundRequest sheetYour app reacts to a refund locally, in secondsNever contacts Apple, so no server notification is sent
SandboxReal REFUND and CONSUMPTION_REQUEST to your server, plus a TEST notification on demandYour backend receives, verifies, and acts on the signed payloadDoes not retry a notification your endpoint fails to receive
ProductionEvery refund, for real moneyNothing you want to learn here firstYou cannot undo the cost of a bug

How to test in-app purchase refunds on the App Store

Run it in this order, from the cheap client check to the full server round trip. Each step exercises a different piece, and the later ones are the ones production actually bills you for.

  • Create an In-App Purchase key under Users and Access, Integrations, In-App Purchase in App Store Connect, and use it to sign your App Store Server API calls.
  • Point your sandbox App Store Server Notifications V2 URL at your backend, then call Request a Test Notification and confirm the TEST payload arrives and verifies against Apple's certificate chain.
  • In Xcode's Transaction Manager, refund a purchase and confirm your app drops the entitlement the instant revocationDate is set.
  • Sign in a sandbox tester, buy a consumable, request a refund, and confirm your server receives the CONSUMPTION_REQUEST and can assemble and send a Send Consumption Information reply well inside the 12-hour window.
  • Refund a sandbox purchase and confirm the REFUND notification reaches your server, that you revoke access or deduct the consumable balance, and that a repeated delivery of the same notification does not double-apply.
A smartphone held in a small bench vise under a work lamp with tweezers beside it, a device under test standing in for rehearsing a refund before it is real

How to rehearse a refund and a chargeback on Google Play

Google Play has no local mode like Xcode's. Everything runs against Google's servers, but license testers keep it free and safe. Add your test Google accounts as license testers in Play Console and they get a set of test payment methods that never charge real money. Google marks every test purchase with a notice across the middle of the buy dialog, and taxes are not computed. What matters for refund testing is which test instrument you pick, because each one drives a different outcome.

Test payment methodWhat it simulatesWhy you would use it
Test instrument, always approvesA clean successful purchaseSet up an order you can then refund or revoke
Test instrument, always declinesA failed paymentConfirm you grant nothing on a decline
Slow test card, approves after a few minutesA pending purchase that later succeedsExercise your PENDING handling before you grant access
Slow test card, declines after a few minutesA pending purchase that later failsConfirm a pending decline never leaks entitlement
Test card, approves then charges backA user-initiated chargebackFire a PendingRefundReviewNotification and rehearse your 24-hour response

Trigger a refund, a chargeback, and the acknowledgment auto-refund

  • Buy with the approve-then-charge-back test card, and a PendingRefundReviewNotification lands on your Real-time Developer Notifications topic moments later. Answer it with a single orders.reviewrefund call, because Google keeps only your first response.
  • Refund and revoke a test order from the Orders tab in Play Console to fire a VoidedPurchaseNotification, and confirm your server pulls the entitlement.
  • Leave a license tester's purchase unacknowledged on purpose. Google auto-refunds it after 3 minutes instead of the 3 days production allows, and emails you the cancellation, so a broken acknowledgment path shows up in minutes, not on day four in production.

What an untested refund path actually costs

A refund handler is not decoration. It is the code that stops you paying to serve someone who no longer pays you. When it fails silently, the refund still goes through, but the access, the balance, and the spend behind them do not stop.

Walk the money. When Apple or Google refunds a purchase, you return the sale price and the store returns its commission, so far the ledger is even. What does not come back is everything you already spent delivering the product: the compute behind a generated result, the model API calls, the storage for what the user saved, the payout you already sent a creator. A refund handler that never revokes access lets a refunded user keep spending those on your budget, with nothing in the system left to cut them off.

The two evidence windows make it sharper. A CONSUMPTION_REQUEST you never exercised in the sandbox is a reply you send malformed or late, and Apple often grants the refund by default when your answer does not land inside 12 hours. A Google Play chargeback response you never fired with the test card is a 24-hour window you fumble live, and from August 3, 2026 a lost Play chargeback costs you the purchase price less Play's service fee plus the bank's chargeback fee. Every one of those failures is reproducible for free in a test environment first. None of them is cheap in production.

Untested pathHow it fails in productionWhat it costs you
REFUND handlerA refunded user keeps accessThe compute, API calls, storage, and payouts you keep spending on them
CONSUMPTION_REQUEST replyMalformed, or sent after 12 hoursApple grants the refund by default, so you lose the sale and the spend
orders.reviewrefund responseMissed or wrong inside 24 hoursFrom August 3, 2026, the purchase price less Play's service fee, plus the bank's chargeback fee

A short checklist before you ship refund handling

You do not need a lab. You need to have watched each event hit your code once.

  • Your app drops access the moment a StoreKit transaction shows a revocationDate, confirmed in Xcode's Transaction Manager.
  • Your sandbox server URL receives a TEST notification and verifies it against Apple's certificates.
  • A sandbox REFUND revokes access or deducts the balance, and a repeat delivery does not double-count.
  • A sandbox CONSUMPTION_REQUEST produces a valid Send Consumption Information reply well inside 12 hours.
  • A Google PendingRefundReviewNotification from the chargeback test card produces exactly one orders.reviewrefund call.
  • An unacknowledged Google Play test purchase auto-refunds in 3 minutes and your reconciliation notices.

Run that list once and refund handling stops being the code you hope works. It becomes the code you have watched work.

Frequently asked questions

Can I test an App Store refund without a real purchase?
Yes. Xcode's StoreKit testing lets you refund a purchase locally through the Transaction Manager, with no real money and no App Store account, which fires your app's Transaction.updates listener. It does not send a server notification, so it tests only your app, not your backend.
Does local StoreKit testing send App Store Server Notifications?
No. Xcode's StoreKit testing runs entirely on your Mac against a local configuration and never contacts Apple's servers, so no App Store Server Notification, including REFUND or CONSUMPTION_REQUEST, is ever sent. Use the sandbox to test your server.
How do I test a Google Play chargeback response?
Use the license-tester payment method named Test card, approves then charges back. It fires a PendingRefundReviewNotification moments after purchase, the same notification a real bank chargeback sends, so you can rehearse your 24-hour orders.reviewrefund reply.
Why does my Google Play test purchase get refunded after a few minutes?
For license testers, Google auto-refunds a purchase after 3 minutes if your app has not acknowledged it, and emails you the cancellation. Production waits 3 days, but testers get the accelerated version so a broken acknowledgment path surfaces fast.
Does Apple's sandbox retry a failed refund notification?
No. The sandbox does not retry App Store Server Notifications, so if your endpoint is down when a sandbox refund fires, the notification is dropped with no second attempt. Confirm your URL is reachable with Request a Test Notification first.

Sources and further reading

RefundHalt

The refund autopilot for the App Store and Google Play

Keep reading

The next refund request is already on its way.

Set up RefundHalt in the time it takes to read another support email about a refund you didn't get to contest.