Getting started
This page walks you from zero to a working sandbox purchase in under 30 minutes. It assumes you already have:
- A Capacitor 5 app
- Products configured in App Store Connect (sandbox testers OK) and/or Google Play Console (license testers OK)
- A backend you can deploy a few new endpoints to (or you can implement them as a custom
BackendAdapter) - An Attesto tenant with API key
If you're missing any of these, the Backend contract and Testing on sandbox pages cover them.
1. Install
npm install @nossdev/iap cordova-plugin-purchase
npx cap syncOptional: app-resume listener
If you want the library to automatically refresh entitlements when your app returns from background (the default), also install @capacitor/app:
npm install @capacitor/app
npx cap syncYou can disable this by passing options.refreshOnResume: false when creating the IAP instance.
2. Create the IAP instance
// src/services/iap.ts
import { createIAP } from '@nossdev/iap';
import type { EntitlementBase } from '@nossdev/iap';
// Define your entitlement shape (extends the base).
interface AppEntitlement extends EntitlementBase {
key: string; // e.g., 'premium', 'remove_ads'
productId: string;
expiresAt: string | null;
// Add app-specific fields the backend returns:
tier?: 'basic' | 'pro';
}
export const iap = createIAP<AppEntitlement>({
products: [
{
id: 'premium_monthly',
type: 'subscription',
androidPlanId: 'monthly-plan', // required for Android subs
},
{ id: 'remove_ads', type: 'product' },
],
backend: {
baseUrl: 'https://api.your-app.com',
endpoints: {
verifyApple: '/api/iap/verify/apple',
verifyGoogle: '/api/iap/verify/google',
entitlements: '/api/iap/entitlements',
restore: '/api/iap/restore',
},
getAuthHeaders: async () => ({
Authorization: `Bearer ${await getAuthToken()}`,
}),
},
});The factory validates your config with zod and throws IAPError(INVALID_CONFIG) on any structural issue — no surprises at runtime.
3. Initialize after auth is ready
// src/main.ts (or wherever your app boots)
import { iap } from './services/iap';
await iap.initialize();initialize() does the following:
- Loads any cached entitlements from local storage (instant warm cache)
- Recovers any unfinished transactions from prior sessions
- Wires the app-resume listener (if
refreshOnResume) - Schedules a background refresh if cache exceeds TTL
- Emits
ready
After initialize() returns, all read methods (hasEntitlement, getEntitlements, getEntitlement, getProducts) are safe to call.
4. Buy something
const result = await iap.purchase('premium_monthly');
switch (result.status) {
case 'success':
// Backend has validated. Entitlements are already updated in state.
console.log('Welcome to premium!');
break;
case 'cancelled':
// User dismissed the native purchase sheet.
break;
case 'pending':
// Android: payment awaiting external clearance (e.g. cash).
console.log('Payment is processing. We\'ll notify you when it clears.');
break;
case 'verification_failed':
// Backend rejected, transport error, etc. The unfinished entry stays
// for retry; library will auto-recover on next launch / refresh.
console.error('Verification failed:', result.error.message);
break;
case 'failed':
// Native error (network, store error, etc.). User can try again.
console.error('Purchase failed:', result.error.message);
break;
}The discriminated union is by design — UI code decides what to show without needing try/catch around every call.
5. Read entitlements anywhere
// Synchronous reads from the in-memory cache
if (iap.hasEntitlement('premium')) {
showPremiumFeatures();
}
const allEntitlements = iap.getEntitlements();
// → [{ key: 'premium', productId: 'premium_monthly', expiresAt: '...', ... }]
const premium = iap.getEntitlement('premium');
// → AppEntitlement | nullThese reads are O(1) on a frozen array. Safe to call inside reactive computeds — they don't trigger network.
6. React to changes
const unsubscribe = iap.on('entitlements-changed', ({ entitlements, previous }) => {
// Wire to your store / state management.
store.setEntitlements(entitlements);
});
// Later, when tearing down:
unsubscribe();See Vue + Quasar recipe and React recipe for full reactive store wiring.
7. Restore on a new device
// Wire to a "Restore Purchases" button.
async function onRestoreClick() {
try {
const { restored, entitlements } = await iap.restorePurchases();
if (restored === 0) {
alert('No previous purchases found.');
} else {
alert(`Restored ${restored} purchase(s).`);
}
} catch (error) {
alert(`Restore failed: ${error.message}`);
}
}restorePurchases() calls getOwnedTransactions() natively, batches to your backend's /restore endpoint, and updates entitlements from the consolidated response.
What's next
- Backend contract — implement the four endpoints in your backend (Attesto handles the receipt validation)
- Configuration — full schema reference
- Error handling — all
IAPErrorCodevalues and remediation hints - Testing on sandbox — sandbox tester accounts on iOS + Google license testers on Android