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]
See an example request →

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 beginRefundRequestIOS to 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#

PlatformClient APIDetection
iOSbeginRefundRequestIOS (iOS 15+)App Store Server Notifications V2 (REFUND, REVOKE)
AndroidNone — store-drivenReal-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.

swift
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.

notificationTypeMeaning
REFUNDApple refunded the user
REFUND_DECLINEDRefund was declined
REVOKEFamily Sharing access revoked (treat like a refund)
CONSUMPTION_REQUESTApple wants consumption data to decide on a refund — respond within 12 hours
ts
// 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#

  • beginRefundRequestIOS can 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.

ts
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);
}

Native References#