In-App Purchase
Purchase products, manage subscriptions, and retrieve product information through the native App Store / Google Play in-app purchase flow.
Access: startiapp.InAppPurchase
For step-by-step guides — see Setup In-App Purchases and Setup Subscriptions.
Methods
purchaseProduct(productId, purchaseType): Promise<InAppPurchaseResponse<InAppPurchaseProductResponse>>
Initiates a native purchase flow for the specified product. The returned promise resolves after the user completes or cancels the transaction.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| productId | string | Yes | The product identifier as configured in App Store Connect or Google Play Console. |
| purchaseType | InApPurchasePurchaseType | Yes | Whether the product is "consumable" or "nonconsumable". |
Returns: Promise<InAppPurchaseResponse<InAppPurchaseProductResponse>> — A response object indicating success (with a transaction identifier) or failure (with an error message).
Example:
const result = await startiapp.InAppPurchase.purchaseProduct(
"com.example.premium",
"nonconsumable"
);
if (result.success) {
console.log("Transaction ID:", result.value.transactionIdentifier);
} else {
console.error("Purchase failed:", result.errorMessage);
}getProduct(productId, purchaseType): Promise<InAppPurchaseResponse<InAppPurchaseGetProductResponse>>
Retrieves product metadata (name, description, localized price) from the store without initiating a purchase.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| productId | string | Yes | The product identifier as configured in App Store Connect or Google Play Console. |
| purchaseType | InApPurchasePurchaseType | Yes | Whether the product is "consumable" or "nonconsumable". |
Returns: Promise<InAppPurchaseResponse<InAppPurchaseGetProductResponse>> — A response object containing product details on success, or an error message on failure.
Example:
const result = await startiapp.InAppPurchase.getProduct(
"com.example.premium",
"nonconsumable"
);
if (result.success) {
console.log(`${result.value.name} - ${result.value.localizedPrice}`);
} else {
console.error("Failed to load product:", result.errorMessage);
}subscribe(productId, options?): Promise<InAppPurchaseResponse<SubscribeResponse>>
Initiates a subscription purchase. Also handles upgrades and downgrades when options.oldProductId is provided.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| productId | string | Yes | The subscription product identifier. |
| options | SubscribeOptions | No | Options for upgrades/downgrades and promotional offers. |
Returns: Promise<InAppPurchaseResponse<SubscribeResponse>> — Transaction details on success.
Example:
// New subscription
const result = await startiapp.InAppPurchase.subscribe("com.example.monthly");
if (result.success) {
console.log("Subscribed! Transaction:", result.value.transactionId);
// IMPORTANT: Validate on your backend first, then finish the transaction
const validated = await fetch("/api/validate-subscription", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(result.value)
}).then(r => r.ok);
if (validated) {
await startiapp.InAppPurchase.finishTransaction(result.value.transactionId);
}
}You must call finishTransaction() after your backend validates the purchase. If you don't, the store will re-deliver the transaction on next app launch and may eventually issue a refund.
Upgrade example:
// Upgrade from monthly to yearly
const result = await startiapp.InAppPurchase.subscribe("com.example.yearly", {
oldProductId: "com.example.monthly",
upgradePolicy: "immediate"
});finishTransaction(transactionId): Promise<InAppPurchaseResponse<boolean>>
Finishes (acknowledges) a subscription transaction after your backend has validated it. This must be called after every successful subscribe() call once your server confirms the purchase.
If not called, the transaction remains "unfinished" — the store will re-deliver it on next app launch (via getActiveSubscriptions() / restorePurchases()), and may eventually refund the user.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| transactionId | string | Yes | The transactionId from the SubscribeResponse. |
Returns: Promise<InAppPurchaseResponse<boolean>> — true if the transaction was acknowledged.
Example:
// After your backend confirms the subscription is valid:
const result = await startiapp.InAppPurchase.finishTransaction(transactionId);
if (result.success) {
console.log("Transaction acknowledged");
}getSubscriptionProducts(productIds): Promise<InAppPurchaseResponse<SubscriptionProduct[]>>
Retrieves subscription product details including pricing, billing period, and available offers.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| productIds | string[] | Yes | Array of subscription product identifiers. |
Returns: Promise<InAppPurchaseResponse<SubscriptionProduct[]>> — Array of subscription product details.
On Android, only the first introductory pricing phase is returned per product. If a subscription has a multi-phase intro (e.g., free trial followed by a discounted period), only the first phase appears in introductoryOffer.
Example:
const result = await startiapp.InAppPurchase.getSubscriptionProducts([
"com.example.monthly",
"com.example.yearly"
]);
if (result.success) {
result.value.forEach(product => {
console.log(`${product.name}: ${product.localizedPrice}/${product.subscriptionPeriod}`);
if (product.introductoryOffer) {
console.log(` Trial: ${product.introductoryOffer.localizedPrice} for ${product.introductoryOffer.period}`);
}
});
}getActiveSubscriptions(options?): Promise<InAppPurchaseResponse<SubscriptionStatus[]>>
Returns the status of all active subscriptions for the current user. Use this to check if the user has an active subscription and its current state.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| options.sync | boolean | No | Force sync with App Store / Google Play before returning. Use for restore purchases functionality. |
Returns: Promise<InAppPurchaseResponse<SubscriptionStatus[]>> — Array of active subscription statuses.
Example:
const result = await startiapp.InAppPurchase.getActiveSubscriptions();
if (result.success) {
const active = result.value.filter(s => s.state === "subscribed");
if (active.length > 0) {
console.log("User has active subscription:", active[0].productId);
console.log("Will renew:", active[0].willAutoRenew);
}
}restorePurchases(): Promise<InAppPurchaseResponse<SubscriptionStatus[]>>
Syncs with the store and returns all subscriptions — including expired ones. Unlike getActiveSubscriptions() which only returns active subscriptions, restorePurchases() returns the full history so you can re-validate on your backend. Required by App Store guidelines — apps with subscriptions must include a "Restore Purchases" button.
Returns: Promise<InAppPurchaseResponse<SubscriptionStatus[]>> — Array of subscription statuses (may include both "subscribed" and "expired").
Example:
const result = await startiapp.InAppPurchase.restorePurchases();
if (result.success) {
console.log("Restored", result.value.length, "subscriptions");
}manageSubscriptions(): Promise<InAppPurchaseResponse<boolean>>
Opens the platform's subscription management screen where the user can cancel, change, or view their subscriptions. On iOS, this opens an in-app management sheet. On Android, this opens the Google Play subscriptions page in an external browser.
Returns: Promise<InAppPurchaseResponse<boolean>> — true if the management screen was opened successfully.
Example:
await startiapp.InAppPurchase.manageSubscriptions();isSubscribed(productId?): Promise<boolean>
Convenience wrapper over getActiveSubscriptions(). Returns true if the user has at least one subscription with state === "subscribed".
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| productId | string | No | If provided, checks for this specific product ID only. |
Returns: Promise<boolean> — true if the user has an active subscription (or the specific product if productId is provided). Returns false on error rather than throwing.
Example:
// Check for any active subscription
if (await startiapp.InAppPurchase.isSubscribed()) {
unlockPremiumFeatures();
}
// Check for a specific product
const hasYearly = await startiapp.InAppPurchase.isSubscribed("com.example.yearly");Events
"unfinishedTransaction"
Emitted automatically on app startup for each subscription transaction that was purchased but not yet finished (acknowledged). This handles cases where the app crashed, lost connectivity, or closed before finishTransaction() was called.
The listener can be registered before or after startiapp.initialize() — unfinished transactions are buffered and delivered when the first listener is added.
Event detail: SubscriptionStatus
Example:
await startiapp.initialize();
startiapp.InAppPurchase.addEventListener("unfinishedTransaction", async (event) => {
const transaction = event.detail;
// Validate on your backend, then finish the transaction
const validated = await fetch("/api/validate-subscription", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(transaction)
}).then(r => r.ok);
if (validated) {
await startiapp.InAppPurchase.finishTransaction(transaction.transactionId);
}
});On Android, Google Play gives you 3 days to acknowledge a purchase before it is automatically refunded.
Types
InApPurchasePurchaseType
Union type specifying the kind of in-app purchase product.
type InApPurchasePurchaseType = "nonconsumable" | "consumable";| Value | Description |
|---|---|
"nonconsumable" | A one-time purchase that does not expire (e.g., unlock a feature). |
"consumable" | A purchase that can be bought multiple times (e.g., in-game currency). |
InAppPurchaseResponse<T>
Discriminated union returned by all purchase methods. Check the success field to determine which shape you have.
type InAppPurchaseResponse<T> =
| { success: true; value: T }
| { success: false; errorMessage: string };InAppPurchaseProductResponse
Payload returned on a successful purchaseProduct call.
type InAppPurchaseProductResponse = {
transactionIdentifier: string;
};InAppPurchaseGetProductResponse
Payload returned on a successful getProduct call.
type InAppPurchaseGetProductResponse = {
name: string;
description: string;
localizedPrice: string;
currencyCode: string;
};SubscribeOptions
Options for the subscribe method. All fields are optional.
type SubscribeOptions = {
oldProductId?: string;
upgradePolicy?: "immediate" | "deferred";
offerId?: string;
resolveOffer?: (productId: string, offerId: string) => Promise<OfferSignature>;
};| Field | Description |
|---|---|
oldProductId | Product ID of the subscription being replaced. Android only — on iOS, StoreKit 2 handles same-group upgrades automatically. On Android, the SDK auto-detects the active subscription unless your app has multiple subscription groups. |
upgradePolicy | "immediate" charges now and switches. "deferred" switches at end of current period. Android only — iOS handles transitions automatically within subscription groups. |
offerId | Promotional offer identifier as configured in App Store Connect or Google Play Console. On iOS, pair with resolveOffer for promotional offers that require a server-signed signature. On Android, this is passed as the offer token. |
resolveOffer | Async callback that returns the offer signature from your backend. Only called when needed (iOS promotional offers). Ignored on Android. |
OfferSignature
Returned by the resolveOffer callback. Your backend must generate these values using your App Store Connect subscription key.
type OfferSignature = {
signature: string;
nonce: string;
timestamp: string;
keyId: string;
};SubscribeResponse
Payload returned on a successful subscribe call. Important: The transaction is not yet finished — you must call finishTransaction() after your backend validates it.
type SubscribeResponse = {
/** Pass this to finishTransaction() after backend validation. */
transactionId: string;
originalTransactionId: string;
productId: string;
purchaseDate: string;
};SubscriptionProduct
Subscription product details returned by getSubscriptionProducts.
type SubscriptionProduct = {
productId: string;
name: string;
description: string;
localizedPrice: string;
currencyCode: string;
/** Human-readable billing period, e.g. "1 month", "1 year", "3 months". null if not available. */
subscriptionPeriod: string | null;
introductoryOffer?: SubscriptionOffer;
promotionalOffers?: SubscriptionOffer[];
};SubscriptionOffer
Details of a subscription offer (introductory or promotional).
type SubscriptionOffer = {
id?: string;
localizedPrice: string;
period: string;
periodCount: number;
paymentMode: "freeTrial" | "payAsYouGo" | "payUpFront";
};paymentMode | Description |
|---|---|
"freeTrial" | Free for the offer period, then regular price. |
"payAsYouGo" | Discounted price charged each period for periodCount periods. |
"payUpFront" | Discounted price charged once up front for the full offer duration. |
SubscriptionStatus
Status of an active or expired subscription.
type SubscriptionStatus = {
productId: string;
transactionId: string;
originalTransactionId: string;
purchaseDate: string;
state: "subscribed" | "expired";
willAutoRenew: boolean;
};state | Description |
|---|---|
"subscribed" | Active and in good standing. Includes users in grace period or billing retry — the SDK handles this automatically. |
"expired" | Subscription has ended, been revoked, or payment failed beyond grace period. |
External Purchase
Manage Apple's External Purchase Custom Link entitlement (StoreKit external purchase API). This module lets you check eligibility, retrieve purchase tokens, display the mandatory Apple disclosure notice, and redirect the user to an external purchase page.
Location
Access the device GPS, track location changes in real time, and set up geofences that trigger events when the user enters or exits a region.