Validation
Verify purchases with your own backend or a managed provider like IAPKit before granting entitlements. Always validate through a trusted server-side verifier; local StoreKit or Play Billing state alone can be bypassed. The OpenIAP Commerce Protocol specifies what a conforming verifier accepts and returns, so the answer does not depend on whose backend you use.
TL;DR
- Always verify with a trusted verifier before granting entitlements
verifyPurchase: Verify with explicit Apple, Google, or Horizon optionsverifyPurchaseWithProvider: Use IAPKit for managed validation- Error ≠ Invalid: Network errors don't mean the purchase is invalid
- Optional product data: Valid Apple and Google verification can opt into a public IAPKit client payload
verifyPurchase#
Verify a purchase with explicit platform options. The current API accepts apple, google, or horizon; it does not accept a Purchase object or a server URL. Keep Google and Horizon access tokens on trusted infrastructure whenever possible.
Signature
func verifyPurchase(_ options: VerifyPurchaseProps) async throws -> VerifyPurchaseResultExample
let result = try await OpenIapModule.shared.verifyPurchase(
VerifyPurchaseProps(
apple: VerifyPurchaseAppleOptions(sku: purchase.productId)
)
)
if case let .verifyPurchaseResultIos(iosResult) = result,
iosResult.isValid {
await grantEntitlement(purchase.productId)
}See: VerifyPurchaseProps
What is IAPKit?#

IAPKit is open-source (MIT) purchase validation and entitlement infrastructure for the OpenIAP ecosystem. Its managed validation endpoint supports App Store, Google Play, Amazon Appstore, and Meta Horizon purchases. Instead of running your own backend that talks to each store's verification API, you forward the JWS, purchase token, Amazon receipt id, or Horizon entitlement payload to IAPKit and get a normalized verification response — so one-time in-app purchases are checked against the store's authoritative state. Amazon Fire OS and Vega OS both use the iapkit.amazon payload. Use the hosted version at kit.openiap.dev or self-host the source from packages/kit in this monorepo. It also implements the Commerce Protocol, so a server integration written against that contract, rather than against IAPKit's own API, moves to any backend serving the same profiles and binding.
Why use it
- Cross-store, one schema — same
VerifyPurchaseWithProviderResultshape for Apple, Google, Amazon, and Horizon. No per-platform JSON parsing. - Fraud-resistant — verifies receipts and purchase state against the store's authoritative server API, blocking common forged receipt and replay flows.
- Entitlement state, not raw receipts — IAPKit collapses raw store data into a single
statefield (entitled,pending,canceled(including refunds and revocations),expired,inauthentic, etc.) so your client and server can act on a single value. - No backend boilerplate — no service account JSON, no App Store private key rotation, no webhook plumbing required to get started.
- Public per-product rules — attach a small TOML, JSON, or text payload to each iOS or Android product and retrieve it during valid Apple or Google verification, or when the app opens.
When to roll your own instead
- You have strict data-residency requirements that disallow sending purchase tokens to a third-party.
- You already operate a hardened receipt-validation service and don't want another vendor in the path.
Get an IAPKit publishable key at kit.openiap.dev, then call verifyPurchaseWithProvider below.
verifyPurchaseWithProvider#
Verify a purchase using a provider like IAPKit.
Example
Fail closed when granting access: require IAPKit's store-verified productId and compare it with the product requested by the app. The local purchase ID is an expected value, not a fallback when verification omits the ID.
For Amazon, send that expected value as iapkit.amazon.expectedProductId. Amazon App Tester receipts also require enabling Allow Amazon App Tester / RVS Cloud Sandbox in the IAPKit project before passing sandbox: true. Handled Amazon results report exactly 'Sandbox' or 'Production' in environment; require the value expected by the build.
import OpenIAP
let result = try await OpenIapModule.shared.verifyPurchaseWithProvider(
VerifyPurchaseWithProviderProps(
iapkit: RequestVerifyPurchaseWithIapkitProps(
apiKey: "openiap-kit_pk_<your-publishable-key>",
apple: RequestVerifyPurchaseWithIapkitAppleProps(
jws: purchase.purchaseToken ?? ""
),
includeClientPayload: true
),
provider: .iapkit
)
)Optional product client payload
Set iapkit.includeClientPayload to true to request the product's public IAPKit payload. The default is false. IAPKit returns clientPayload only when Apple or Google verifies the receipt as valid, the store supplies the product ID, and the matching IAPKit product is currently present, not removed, and has a payload. Invalid receipts, absent or removed products, missing payloads, Horizon, and Amazon responses omit the field. A retained payload becomes eligible again after store sync re-pulls its matching product.
{
"store": "google",
"isValid": true,
"state": "entitled",
"productId": "premium_monthly",
"clientPayload": {
"format": "toml",
"body": "[access]\nmax_items = 10",
"version": 3,
"updatedAt": 1784160000000
}
}This is request/response retrieval, not an APNs or FCM push. To fetch rules when the app opens without verifying a new purchase, use the React Native or Expo kitApi product methods, or MAUI's KitApiClient, as described in Product client payloads.
State contract
Only entitled, pending-acknowledgment, and ready-to-consume make isValid true, but they are not interchangeable across stores or product types. Use the platform-aware flow below, and see the canonical IapkitPurchaseState reference for every state.
Error Handling Best Practice#
Recommended Pattern
This example targets App Store and Google Play builds. Fire OS and Vega OS builds must select the amazon verification branch; their consumables use ready-to-consume like Apple.
import { Platform } from 'react-native';
try {
// App Store / Google Play only; route Fire OS and Vega OS through Amazon.
const isApple = Platform.OS === 'ios';
// Resolve this from your app-owned catalog, never from the harmonized state.
const isConsumable = isConsumableProduct(purchase.productId);
const token = purchase.purchaseToken ?? '';
const result = await verifyPurchaseWithProvider({
provider: 'iapkit',
iapkit: {
apiKey: 'your-key',
...(isApple
? { apple: { jws: token } }
: { google: { purchaseToken: token } }),
},
});
const verified = result.iapkit;
const verifiedProductId = verified?.productId;
const productMatches =
verifiedProductId != null && verifiedProductId === purchase.productId;
const stateAllowsFulfillment = isConsumable
? isApple
? verified?.state === 'ready-to-consume'
: verified?.state === 'ready-to-consume' ||
verified?.state === 'entitled' ||
verified?.state === 'pending-acknowledgment'
: verified?.state === 'entitled' ||
(!isApple && verified?.state === 'pending-acknowledgment');
if (
verified?.isValid === true &&
productMatches &&
stateAllowsFulfillment
) {
if (isConsumable) {
// Persist idempotent delivery before consuming so it cannot be lost.
await deliverConsumable(verifiedProductId);
await finishTransaction({ purchase, isConsumable: true });
} else {
await grantEntitlement(verifiedProductId);
await finishTransaction({ purchase, isConsumable: false });
}
} else {
// IAPKit completed the check but did not verify this entitlement.
// Do not grant or finish this purchase.
recordRejectedVerification(purchase, verified);
}
} catch (error) {
// A network/server error is not proof that the purchase is invalid.
console.error('Verification failed:', error);
// Keep only access established by an earlier successful verification.
// Do not grant this purchase or finish it; retry verification later.
preservePreviouslyVerifiedEntitlements();
scheduleVerificationRetry(purchase);
}Purchase Identifier Usage#
Use the appropriate identifiers for content delivery and purchase tracking:
iOS Identifiers
| Product Type | Primary Identifier | Usage |
|---|---|---|
| Consumable | transactionId | Track each purchase individually |
| Non-consumable | transactionId | Single purchase tracking |
| Subscription | originalTransactionIdentifierIOS | Track across renewals (stays constant) |
Android Identifiers
| Product Type | Primary Identifier | Usage |
|---|---|---|
| Consumable | purchaseToken | Track each purchase |
| Non-consumable | purchaseToken | Track ownership status |
| Subscription | purchaseToken | Same token across renewals |
Idempotency: Use these identifiers to prevent duplicate content delivery.