Getting started
This page walks you from zero to a working sandbox purchase in under 30 minutes. It assumes you already have:
- A Capacitor 7+ app (also runs on Capacitor 8)
- 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 @nosslabs/iap @capgo/native-purchases
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 '@nosslabs/iap';
import type { EntitlementBase } from '@nosslabs/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({ productId: '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.
Pre-attaching a user identifier (optional)
If your app has a logged-in user at purchase time, pass an appUserId so it travels through StoreKit / Play Billing and reaches your backend on both the verify response and the eventual webhook. Eliminates the verify/webhook race on the backend side — see Attesto's integration guide for the join pattern.
// (a) Plain string — your app already has the UUID.
await iap.purchase({
productId: 'premium_monthly',
appUserId: currentUser.iapUuid,
});
// (b) Async fetcher — your backend mints+saves the UUID per user the first
// time it's asked, returns the same UUID on later calls. iap calls the
// fetcher fresh on every purchase; backend owns the idempotency.
await iap.purchase({
productId: 'premium_monthly',
appUserId: async () => {
const r = await fetch('/api/iap/uuid', { method: 'POST', headers: authHeaders() });
return (await r.json()).uuid;
},
});
// (c) Async fetcher with `ctx` — equivalent to (b) when your UUID endpoint
// uses the same auth as your IAP backend. iap awaits
// `backend.getAuthHeaders()` and passes the result as `ctx.authHeaders`,
// so you don't have to redefine a helper. Convenience only — ignore the
// parameter and close over your own auth state when your UUID endpoint
// uses different auth.
await iap.purchase({
productId: 'premium_monthly',
appUserId: async ({ authHeaders }) => {
const r = await fetch('/api/iap/uuid', { method: 'POST', headers: authHeaders });
return (await r.json()).uuid;
},
});The supplied value (literal or fetcher-returned) must be a UUID v4. iap validates before passing to native; non-UUIDs throw IAPError(INVALID_APP_USER_ID). Apple requires UUIDs for appAccountToken; we enforce the same on Android for a consistent contract.
For purchases without a logged-in user (guest flows), simply omit appUserId — your backend falls back to mapping by Attesto's subject.key (see the integration guide).
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