IAP Integration Guide
Everything you need to integrate in-app purchases from start to finish.
Before You Begin
| Requirement | Detail |
|---|---|
| Unveily Plan | Pro (IAP feature included) |
| Android | Developer account with Google Play billing profile set up |
| iOS | Apple Developer Program account + app registered in App Store Connect |
| App build | Android: Release-signed AAB uploaded at least once |
IAP Activation Gate — coming_soon
IAP only works when both of these conditions are met:
- Your license includes the
iapfeature (the Pro plan). - The server-side
iapEnabledflag is turned on for you.
If either is missing, queryProducts / purchase / restorePurchases do not open the payment sheet and instead return:
{ "status": "coming_soon" }Check this status first in every result callback so you can hide the purchase UI or show a "coming soon" notice.
function handleIapResult(result) {
if (result.status === "coming_soon") {
// IAP not enabled yet — hide the purchase button / show a notice
document.getElementById("iap-section").hidden = true;
return false;
}
return true;
}
// Example: checking the gate inside the purchase result
window.onPurchaseResult = async function(result) {
if (!handleIapResult(result)) return; // stop here when coming_soon
if (!result.success) return;
// ... proceed with server verification
};Android — Google Play Setup
Mission 1 — Prepare Your Google Play App
1-1. Create the app and upload an AAB
- Go to Google Play Console → Create app
- Fill in app name, language, type, then complete creation
- Upload a release-signed AAB to the Internal testing track
- Complete store listing, content rating, and policy sections → Submit for review
Release keystore
Your app/build.gradle.kts must have signingConfigs.release configured.
See the First Build guide for details.
1-2. Set up a payments profile
- Play Console → Setup → Payments profile → Create merchant account
- Enter business info, bank account, and tax details
- Approval takes 1–2 business days
Mission 2 — Create In-App Products (Android)
Subscription product
- Play Console → your app → Monetize → Subscriptions
- Click Create subscription
- Fill in the details:
| Field | Example | Notes |
|---|---|---|
| Product ID | monthly_pro | Used in code — cannot be changed later |
| Name | Pro Monthly | Shown to users |
| Billing period | 1 month | Renewal frequency |
| Price | $9.99 | Set per-country pricing as needed |
- Click Activate → confirm status shows Active
One-time product
- Monetize → In-app products → Create product
- Enter product ID, name, description, price, then activate
Mission 3 — Configure Test Accounts (Android)
Test accounts let you go through the full purchase flow without real charges. The Gmail account must be added as a tester and used to install the app via the internal test link.
- Play Console → your app → Internal testing → Testers tab → add Gmail addresses
- Play Console → Setup → License testing → add the same Gmail addresses
- License testers get accelerated subscription renewal intervals for faster testing
Mission 4 — Set up a Google Play service account (Android real-time verification)
To turn on Android real-time tamper verification (storeApiVerified: true), you need a Google service account. This credential lives on your web server only — it is never sent to or stored by Unveily. Without it, verification requests are processed as storeApiVerified: false (DB record only).
On iOS, the server verifies the Apple signature (JWS), so no credential is needed. This Mission is Android-only.
4-1. Create a Google Cloud service account
- Google Cloud Console → select the project linked to your Play account
- APIs & Services → enable
Google Play Android Developer API - IAM & Admin → Service Accounts → Create service account
- After creation, go to Keys tab → Add Key → JSON → download
4-2. Link to Google Play Console
- Google Play Console → Setup → API access
- Link to your Google Cloud project
- Find the service account → Grant access
- Permissions: View financial data + Manage orders → Save
- Allow up to 24 hours for permissions to propagate
4-3. Place the service account JSON on "your web server"
Keep the downloaded JSON on your web server only. During verification, your web server uses this JSON to generate a ~1-hour short-lived access token and forwards that to Unveily. (The long-lived JSON key itself never leaves your server.)
Unveily does not store your credentials
For security, Unveily does not store your service account. Keep the JSON on your web server and forward only a short-lived token per request. For a sample server-side relay endpoint (generating a short-lived token + calling Unveily), see IAP Bridge — Server verification.
iOS — App Store Setup
Mission 5 — Prepare Your App Store App
5-1. Add the In-App Purchase capability in Xcode
- Open Xcode → select your project → Signing & Capabilities tab
- Click + Capability → add In-App Purchase
5-2. Register in-app products in App Store Connect
- App Store Connect → your app → Monetization → In-App Purchases
- Click + → select product type (Auto-Renewable Subscription, Consumable, or Non-Consumable)
- Fill in the details:
| Field | Example | Notes |
|---|---|---|
| Reference Name | Pro Monthly | Internal management name |
| Product ID | monthly_pro | Used in code — can match Android ID |
| Price | Tier 10 ($9.99) | Select from App Store price tiers |
- Add localizations (name/description) → Save → confirm Ready to Submit status
5-3. Configure a subscription group
Auto-renewable subscriptions must belong to a subscription group.
- Subscription Groups → Create Subscription Group
- Enter a group name (e.g., "Pro Plan")
- Add your subscription product to the group
5-4. Set up sandbox testers
- App Store Connect → Users and Access → Sandbox Testers
- Click + → create a test Apple ID (dedicated test account, not a real Apple ID)
- On a physical device: sign out of Apple ID in Settings → launch the app → sign in with the sandbox account
Mission 6 — Integrate the Unveily SDK (iOS)
No acknowledgePurchase needed on iOS
On iOS (StoreKit 2), the transaction is automatically finished when purchase() completes.
After server verification (via your web server) succeeds, activate your features directly — no acknowledgePurchase call required.
let transactionId = null;
let purchasedProductId = null;
// 1. Load product pricing (App Store Connect product IDs)
function loadProducts() {
window.unveilyBridge.iap.queryProducts(["monthly_pro"], "subs", "onProductsLoaded");
}
window.onProductsLoaded = function(result) {
if (result.error) return;
document.getElementById("price").textContent = result.products[0].price;
};
// 2. Start purchase
function subscribe() {
window.unveilyBridge.iap.purchase("monthly_pro", "subs", "onPurchaseResult");
}
// 3. Purchase complete → verify via your web server (iOS forwards signedTransaction(JWS))
window.onPurchaseResult = async function(result) {
if (!result.success) return;
const res = await fetch("/api/verify-iap", { // your web server calls Unveily
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({
platform: "ios",
productId: result.productId,
productType: "subs",
signedTransaction: result.signedTransaction, // Apple-signed JWS
}),
});
const verify = await res.json();
if (!verify.success) {
alert("Payment verification failed. Please contact support.");
return;
}
// Unlock Pro features (no acknowledgePurchase on iOS)
document.body.classList.add("pro-user");
};
// 5. Restore on app launch (based on Transaction.currentEntitlements)
// Use 'unveilyGlueReady', not 'load' — at page-load time the glue may not have
// injected unveilyBridge yet, which causes a race.
window.addEventListener("unveilyGlueReady", () => {
window.unveilyBridge.iap.restorePurchases("subs", "onRestoreResult");
});
window.onRestoreResult = function(r) {
if ((r.purchases || []).some(p => p.transactionId || p.isAcknowledged))
document.body.classList.add("pro-user");
};Server verification is mandatory
Never activate features without server verification first. Verification is performed via your web server → Unveily (Server verification). Skipping server-side validation exposes your app to receipt fraud that grants Pro access without payment.
Android — Integrate the Unveily SDK
let purchaseToken = null;
let purchasedProductId = null;
// 1. Load product pricing
function loadProducts() {
window.unveilyBridge.iap.queryProducts(["monthly_pro"], "subs", "onProductsLoaded");
}
window.onProductsLoaded = function(result) {
if (result.error) return;
document.getElementById("price").textContent = result.products[0].price;
};
// 2. Start purchase
function subscribe() {
window.unveilyBridge.iap.purchase("monthly_pro", "subs", "onPurchaseResult");
}
// 3. Purchase complete → verify via your web server (Android forwards purchaseToken)
window.onPurchaseResult = async function(result) {
if (!result.success) return;
const res = await fetch("/api/verify-iap", { // your web server calls Unveily
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({
platform: "android",
productId: result.productId,
productType: "subs",
purchaseToken: result.purchaseToken,
}),
});
const verify = await res.json();
if (!verify.success) {
alert("Payment verification failed. Please contact support.");
return;
}
// Unlock Pro features
document.body.classList.add("pro-user");
// Acknowledge (required within 3 days — Android only)
window.unveilyBridge.iap.acknowledgePurchase(result.purchaseToken, "onAckResult");
};
window.onAckResult = function(r) { if (r.success) console.log("Subscription active"); };
// 5. Restore on app launch
// Use 'unveilyGlueReady', not 'load' — at page-load time the glue may not have
// injected unveilyBridge yet, which causes a race.
window.addEventListener("unveilyGlueReady", () => {
window.unveilyBridge.iap.restorePurchases("subs", "onRestoreResult");
});
window.onRestoreResult = function(r) {
if ((r.purchases || []).some(p => p.isAcknowledged))
document.body.classList.add("pro-user");
};Subscription Upgrade / Downgrade
When an existing subscriber switches to a different plan, use the purchase overload to pass the old purchase token and a replacement mode.
window.unveilyBridge.iap.purchase(
"yearly_pro", // the new product ID to switch to
"subs",
{
oldPurchaseToken: currentPurchaseToken, // purchaseToken of the current sub (Android)
replacementMode: 1, // 1=WITH_TIME_PRORATION (default)
},
"onIAPPurchaseResult"
);replacementMode values: 1=WITH_TIME_PRORATION (default), 2=CHARGE_PRORATED_PRICE, 3=WITHOUT_PRORATION, 5=CHARGE_FULL_PRICE, 6=DEFERRED.
iOS ignores the options object
On iOS (StoreKit 2), the third options object ({oldPurchaseToken, replacementMode}) is silently ignored. The App Store handles upgrades/downgrades automatically through subscription groups — calling purchase on another product in the same group applies proration. The options object is Android-only.
Final Check
Android
- Install app via internal test link using a tester account
- Query products → correct price displayed
- Complete purchase → Google Play payment screen appears
- Server verification response shows
success: true - Acknowledge completes successfully
- App restart → subscription restored correctly
iOS
- Install app on a physical device using a sandbox tester account
- Query products → App Store Connect product info displayed
- Complete purchase → App Store payment screen appears
- Server verification response shows
success: truewithtransactionId - Feature unlocked without calling
acknowledgePurchase - App restart → subscription restored correctly
What's Next
- IAP Bridge API — Detailed parameter and response reference
- App Info Bridge — Check the current plan
- License Setup — Configure your license key