Skip to main content
Unveilydocs

IAP Integration Guide

Everything you need to integrate in-app purchases from start to finish.

Before You Begin

RequirementDetail
Unveily PlanPro (IAP feature included)
AndroidDeveloper account with Google Play billing profile set up
iOSApple Developer Program account + app registered in App Store Connect
App buildAndroid: Release-signed AAB uploaded at least once

IAP Activation Gate — coming_soon

IAP only works when both of these conditions are met:

  1. Your license includes the iap feature (the Pro plan).
  2. The server-side iapEnabled flag 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

  1. Go to Google Play ConsoleCreate app
  2. Fill in app name, language, type, then complete creation
  3. Upload a release-signed AAB to the Internal testing track
  4. 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

  1. Play Console → SetupPayments profile → Create merchant account
  2. Enter business info, bank account, and tax details
  3. Approval takes 1–2 business days

Mission 2 — Create In-App Products (Android)

Subscription product

  1. Play Console → your app → MonetizeSubscriptions
  2. Click Create subscription
  3. Fill in the details:
FieldExampleNotes
Product IDmonthly_proUsed in code — cannot be changed later
NamePro MonthlyShown to users
Billing period1 monthRenewal frequency
Price$9.99Set per-country pricing as needed
  1. Click Activate → confirm status shows Active

One-time product

  1. MonetizeIn-app productsCreate product
  2. 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.

  1. Play Console → your app → Internal testingTesters tab → add Gmail addresses
  2. Play Console → SetupLicense 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

  1. Google Cloud Console → select the project linked to your Play account
  2. APIs & Services → enable Google Play Android Developer API
  3. IAM & AdminService AccountsCreate service account
  4. After creation, go to Keys tab → Add KeyJSON → download
  1. Google Play ConsoleSetupAPI access
  2. Link to your Google Cloud project
  3. Find the service account → Grant access
  4. Permissions: View financial data + Manage orders → Save
  5. 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

  1. Open Xcode → select your project → Signing & Capabilities tab
  2. Click + Capability → add In-App Purchase

5-2. Register in-app products in App Store Connect

  1. App Store Connect → your app → MonetizationIn-App Purchases
  2. Click + → select product type (Auto-Renewable Subscription, Consumable, or Non-Consumable)
  3. Fill in the details:
FieldExampleNotes
Reference NamePro MonthlyInternal management name
Product IDmonthly_proUsed in code — can match Android ID
PriceTier 10 ($9.99)Select from App Store price tiers
  1. 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.

  1. Subscription GroupsCreate Subscription Group
  2. Enter a group name (e.g., "Pro Plan")
  3. Add your subscription product to the group

5-4. Set up sandbox testers

  1. App Store Connect → Users and AccessSandbox Testers
  2. Click + → create a test Apple ID (dedicated test account, not a real Apple ID)
  3. 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: true with transactionId
  • Feature unlocked without calling acknowledgePurchase
  • App restart → subscription restored correctly

What's Next

On this page