Sell Themes or One-Off Content
A complete recipe for selling individual content items as one-off purchases with the per-item unlock model — native restore, no backend — plus the scale path when your catalogue grows.
Sell Themes or One-Off Content
This recipe sells individual content items — themes, chapters, packs — as one-off purchases using the per-item unlock model. It's the fastest path to shipping: native restore, no backend required. When your catalogue outgrows it, jump to Scaling up.
New here? Compare the options first in Choosing a monetization model.
How per-item unlock works
Each content item is one nonconsumable store product. The user buys it, the store remembers it forever, and getOwnedProducts() brings it back on a new device. You don't store anything.
You don't release the app per product
The fear that prompts most people here: "do we really create a product in Apple for every theme — and release the app each time?"
You create a product per item, yes. But you do not release the app each time. After your first in-app purchase ships attached to an app version, every additional product is created in App Store Connect / Google Play Console and submitted for review on its own — no new binary, no app update. Adding a theme is a store-console task, not an app release.
Build it
Create the store products
For each item, create a non-consumable product:
- App Store Connect → your app → In-App Purchases → create a non-consumable.
- Google Play Console → your app → In-app products → create a managed product.
Give each a product ID you can map back to the content item, e.g. theme.aurora, theme.midnight. Use the same ID on both stores so your code has one ID per item. See Apple App Store and Google Play Store for the full store setup.
Show the item with its localized price
Always read the price from the store — never hard-code it — so currencies and regional pricing are correct:
const productId = "theme.aurora";
const product = await startiapp.InAppPurchase.getProduct(productId, "nonconsumable");
if (product.success) {
document.getElementById("theme-name").textContent = product.value.name;
document.getElementById("theme-price").textContent = product.value.localizedPrice;
} else {
console.error("Couldn't load product:", product.errorMessage);
}Buy the item
document.getElementById("buy-btn").addEventListener("click", async () => {
const result = await startiapp.InAppPurchase.purchaseProduct(
productId,
"nonconsumable"
);
if (!result.success) {
console.error("Purchase failed:", result.errorMessage);
return;
}
// Validate on your backend before unlocking — see the warning below.
await fetch("/api/validate-purchase", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
transactionId: result.value.transactionIdentifier,
productId,
}),
});
unlockTheme(productId);
});Validate before you unlock. Don't trust a client-side success alone. Send the transaction to your backend (or a store server-to-server check) and unlock only once it's verified. This is the one piece of server work per-item unlock asks of you, and it's optional for low-stakes content.
Restore on a new device
Because these are non-consumables, the store restores them — no ledger of your own needed. Offer a "Restore purchases" button (the stores require one) and call getOwnedProducts with { sync: true } to force a store sync:
document.getElementById("restore-btn").addEventListener("click", async () => {
const result = await startiapp.InAppPurchase.getOwnedProducts({ sync: true });
if (result.success) {
for (const product of result.value) {
unlockTheme(product.productId);
}
}
});getOwnedProducts returns non-consumables only. Subscriptions come from getActiveSubscriptions, and consumable tokens are never restored by the store — those you track in your own backend.
Scaling up: content tokens
Per-item unlock starts to hurt when you publish items constantly — every drop means a new store product and a review wait. At that point, switch to the content token model:
- Sell one
consumable"token" product (or a few price tiers). - The user buys a token and redeems it for any item — adding new items never touches the store consoles again.
The catch: consumables are never restored by the store, so you must keep your own record of what each user owns, tied to a user account. No accounts, no token — a reinstall would wipe purchases you can't bring back. See Content token for the trade-offs before committing.
See also
Choosing a Monetization Model
Compare per-item unlock, all-access unlock, content token, and all-access subscription — and pick the one that fits your catalogue, pricing, and whether you have user accounts.
App
Control core app behavior including navigation, UI chrome, domain routing, device info, and screen options.