Use this page with AI
Copy this into your coding assistant and add your request.
Read https://openiap.dev/docs/features/refund and https://openiap.dev/llms.txt. Follow the reading instructions, detailed reference, and linked guides relevant to my task before making changes.
Inspect my existing project and reuse its framework and conventions. Ask me for missing product decisions. Implement the requested behavior and run the applicable checks.
Show the working result, the commands and actual test results, and any remaining limitations. Keep your explanation brief.
My request: [describe what customers should be able to do]Refund
Handle refunds initiated by users or store-side actions. iOS supports in-app refund requests via StoreKit 2, while Android refunds are store-driven and require server-side detection.
TL;DR
- iOS 15+: Use
beginRefundRequestIOSto present an in-app refund sheet - Android: No client-side refund API. Auto-refunded after 3 days if not acknowledged
- Server-side: Subscribe to App Store Server Notifications V2 (Apple) and Real-time Developer Notifications (Google) to react to refunds
- Critical: Always revoke entitlements when a refund is detected
Platform Differences#
| Platform | Client API | Detection |
|---|---|---|
| iOS | beginRefundRequestIOS (iOS 15+) | App Store Server Notifications V2 (REFUND, REVOKE) |
| Android | None — store-driven | Real-time Developer Notifications — voidedPurchaseNotification for one-time products, subscriptionNotification.SUBSCRIPTION_REVOKED for subscriptions — plus server-side reconciliation via the Voided Purchases API |
Platform Implementation#
Overview#
iOS lets users request refunds directly from inside your app using StoreKit 2's refund sheet. The system handles the refund flow; your app receives the result via the returned status string.
- Requires iOS 15+
- Not available on tvOS
- The actual refund decision is made by Apple — the API only initiates the request
- For detection of approved refunds, use App Store Server Notifications V2
beginRefundRequestIOS#
Present the refund request sheet for a previously purchased product.
import OpenIap
let status = try await OpenIapModule.shared.beginRefundRequestIOS(sku: purchase.productId)
if let status {
switch status {
case "success":
print("Refund request submitted")
case "userCancelled":
print("User cancelled refund flow")
default:
print("Refund request status: \(status)")
}
} else {
print("Refund request status: nil")
}App Store Server Notifications V2#
Apple sends a server-to-server notification when a refund is approved. Subscribe to handle revocation reliably — the in-app status is just the request, not the final outcome.
| notificationType | Meaning |
|---|---|
REFUND | Apple refunded the user |
REFUND_DECLINED | Refund was declined |
REVOKE | Family Sharing access revoked (treat like a refund) |
CONSUMPTION_REQUEST | Apple wants consumption data to decide on a refund — respond within 12 hours |
// Server webhook handler (Node.js).
// In App Store Server Notifications V2, signedTransactionInfo is itself a
// signed JWS string — verify and decode it to read its fields.
app.post('/webhooks/apple', async (req, res) => {
const { signedPayload } = req.body;
const decoded = await verifyAndDecodeJWS(signedPayload);
if (decoded.notificationType === 'REFUND' || decoded.notificationType === 'REVOKE') {
const transactionInfo = await verifyAndDecodeJWS(
decoded.data.signedTransactionInfo,
);
await revokeEntitlement(transactionInfo.transactionId);
}
res.sendStatus(200);
});Testing#
beginRefundRequestIOScan be exercised in sandbox but the sheet may show limited UI- Use StoreKit's "Refund" command in Xcode StoreKit Configuration to simulate refunds
- Configure the sandbox URL for App Store Server Notifications in App Store Connect
Revoking Entitlements#
When a refund is detected — through a webhook, polling, or a manual process — revoke the user's entitlement and clean up downstream state.
async function revokeEntitlement(transactionId: string) {
// 1. Mark entitlement inactive in your database
await db.entitlements.update({
where: { transactionId },
data: { status: 'refunded', revokedAt: new Date() },
});
// 2. Restrict access in any cached/session state
await invalidateUserSessionsForTransaction(transactionId);
// 3. (Optional) Notify the user out-of-band
await sendRefundConfirmationEmail(transactionId);
}Related#
Native References#
- Apple · Transaction.beginRefundRequest(in:) — StoreKit 2 in-app refund sheet
- Apple · App Store Server Notifications V2 — server-to-server REFUND / REVOKE events
- Google · Issue refunds — Play Console refund flow
- Google · Voided Purchases API — fallback polling for voided orders