Stores & billing

Store guideline compliance, StoreKit, Google Play Billing, offers and promo codes, Ask to Buy, family sharing, refunds and web checkout.

How do you help apps stay compliant with App Store and Play Store subscription guidelines?

  • Up-to-date store support. StoreKit 1 and StoreKit 2, Google Play Billing up to v8. We implement store changes ahead of time, so you do not have to track them.
  • Mandatory elements built into the templates. Footer with Terms and Privacy Policy links, a Restore button (required by Apple), an "Already subscribed? Sign in" button, and prices, durations and renewal terms localized by the store itself.
  • Anonymous purchase. Apple does not recommend forcing account creation to subscribe. Our anonymous user handling is designed precisely for that.
  • Store edge cases handled natively: Ask to Buy, Apple promotional offers, offer codes, Google developer-determined offers, deferred purchases, family sharing, grace period.
  • Regulatory tracking. We document and ship store and regulatory changes as they land — recent examples include Apple's 12-month commitment paid monthly, the web checkout path opened after the Epic v. Apple decision, and the Google Play rules update for US users.
  • Preview and Debug Mode let you validate the display and the compliance of your implementation before submission.

StoreKit 1 or StoreKit 2?

StoreKit 2 is the default in SDK 6 and the recommended choice for new integrations. StoreKit 1 remains available for apps that need it.

You must declare your choice explicitly at initialization:

Purchasely
    .apiKey("X-API-KEY")
    .storekitSettings(.storeKit2) // or .storeKit1
    .start { _ in }

📚 StoreKit 1 vs StoreKit 2 · Migrating from StoreKit 1 to StoreKit 2


Which Google Play Billing versions are supported?

Billing v4 through v8. SDK 6 integrates the current Billing version directly, so there is nothing to align on your side.

If you are still on SDK 5.x and prices stopped resolving after moving to Billing 8, you are very likely hitting the billing (non-KTX) vs billing-ktx conflict — the fix is documented in Google Play Billing v8.

On React Native, Flutter and Cordova, keep the Google package on the exact same version as the main package.


Why are prices shown in dollars instead of my local currency in TestFlight or sandbox?

That is store behavior, not Purchasely. Sandbox and TestFlight resolve prices against the store front of the tester's account, and Apple sandbox accounts frequently default to the US store front. Create a sandbox tester in the target country, or verify in production with a real account.

The same applies to Google Play license testers: the currency follows the tester account's country and payment profile.


How are Ask to Buy and deferred purchases handled?

Natively.

  • The SDK emits the IN_APP_DEFERRED UI / SDK event when the user starts a deferred payment (Ask to Buy, PSD2 approval).
  • A native "Waiting for approval" screen is shown, fully localizable through the ply_modal_alert_in_app_deferred_* strings — see Localizing your app.
  • The entitlement is only opened when the purchase is actually approved.

How does family sharing work?

  • Every server event carries an is_family_shared attribute — see Server event attributes.
  • FAMILY_SHARED_REVOKED fires when the subscription owner removes a member's access — see Lifecycle events.
  • A shared subscription attaches automatically to the member the first time they open the app. Nothing to implement on your side.
  • We receive no data about the payer — the stores do not transmit it.

Family-shared subscriptions initially arrive as unknown users if the member has not opened the app yet.


What offer types are supported?

Offer typeStoreDocumentation
Introductory offers / free trialsBothEligibility to introductory offer
Promotional offersApp StoreConfiguration · Implementation
Offer codesApp StoreConfiguring offer codes
Developer-determined offersGoogle PlayConfiguration
Promo codesGoogle PlayConfiguring promo codes
12-month commitment, paid monthlyApp Store12-Month Commitment
Stripe / webWebWeb checkout · Stripe configuration

Eligibility between an introductory offer and a promotional offer is resolved automatically, so a Screen configured with an Offering shows the right price to the right user.

📚 Understanding offer types · Offer mode in the Composer


I override plans or offers at runtime with a Dynamic Offering and the paywall does not change

Dynamic Offerings are resolved server-side, at fetch time. The consequence is an ordering rule:

  • Register the offering before fetching or displaying the Placement. Registering it after the Screen was fetched changes nothing on the Screen already loaded — re-fetch.
  • Offerings persist until you remove them, so a leftover offering from a previous screen or session still applies. Clear them before re-registering.
  • billingPlanType is iOS-only.
❗️

One plan → one billing plan type per Screen

If two offering references resolve to the same plan with different billing plan types in the same Screen, the billing type becomes ambiguous and resolves to unspecified — which reads as "the commitment plan randomly does not apply". Back up-front and monthly-commitment variants with two distinct plans instead.

📚 Dynamic offering


Does Google Play have an equivalent of the 12-month commitment?

Yes — Google Play has its own native installment subscriptions, and it works differently: everything is configured on the base plan in the Google Play Console, there is nothing to set in the SDK and no Android counterpart to Apple's billing plan type. Once the base plan is an installment plan, Purchasely surfaces the commitment information the same way it does for Apple, and the INSTALLMENT_* server events report the individual payments.

So a "yearly plan generating monthly transactions" is expected on both stores — through Apple's 12-month commitment paid monthly, or through a Google Play installment base plan.

📚 12-Month Commitment (Paid Monthly)


The same user has an active subscription on two stores at once — is that a bug?

No. Purchasely does not enforce a single active subscription per user across platforms: App Store, Play Store, Huawei, Amazon and Stripe subscriptions for the same user ID coexist, each with its own independent lifecycle events. Posting a Stripe receipt for a user who already has an App Store subscription creates a second subscription — it does not transfer or cancel the first one, and no transfer event is emitted (transfers only happen when an existing subscription changes owner, typically anonymous → logged-in).

If your product allows only one subscription per user, handle it before the purchase:

  • Block — check the user's current status (SDK, or your backend kept in sync by webhooks) and do not launch the store flow or post the web receipt if another platform is already active.
  • Warn — show a screen telling the user where their active subscription lives and how to cancel it.

There is no way to force-deactivate the other platform's subscription from Purchasely, and it would not stop the store from billing the user anyway.


Can I sell outside the store — web checkout or external purchase links?

Yes. Web checkout has been available since SDK 5.3 and relies on Stripe Payment Links. It is fully no-code, and web transactions are included in your A/B test results and revenue dashboards. Targeting a specific market (US, for example) is done natively with Audiences.

📚 Web checkout · Stripe configuration


My yearly plan is generating monthly transactions — is that normal?

Yes, if the Plan has the 12-month commitment paid monthly billing plan enabled. It is not a separate product: it is an additional billing plan on the same yearly SKU, and Purchasely serves whichever one the user is eligible for.

Billing planThe user is chargedExample
1 Year Upfront (always enabled)The full price, once a year$119.88 today
Monthly with 12-Month CommitmentEvery month, for 12 months$9.99/month, $119.88 in total

What surprises people most:

  • Each monthly payment is an independent transaction granting one month of access, so you see 12 transactions per commitment cycle.
  • If the user cancels during the commitment, billing continues to the end of the 12 periods and access is kept until then.
  • Upgrades take effect immediately and end the commitment; downgrades are deferred to the end of it.
  • The billing grace period does not apply to commitments — a failed monthly payment suspends access immediately and restores it on recovery.
  • Apple requires you to display both the monthly amount and the total commitment before purchase. Combining {{MONTHLY_AMOUNT}} and {{PRICE}} in your offering copy covers that.

Purchasely automatically falls back to 1 Year Upfront for users whose App Store country is not eligible (the US and Singapore are excluded by Apple), and for users on older OS or SDK versions. No app-side handling needed.

📚 12-Month Commitment (Paid Monthly)


How do refunds and grace periods appear?

📚 Lifecycle events · Subscription status dashboard


We are transferring our app to another Apple or Google account — what breaks?

Store account transfers change the underlying identifiers, so this needs to be coordinated with us before you start.

📚 Transferring Apple and Google account — and contact your Purchasely support channel ahead of the transfer window.


Which stores can Purchasely validate receipts for?

Apple App Store, Google Play Store, Huawei AppGallery, Amazon Appstore, and Stripe for web transactions. Declare the stores you need at initialization:

Purchasely {
    context(applicationContext)
    apiKey("X-API-KEY")
    stores(listOf(GoogleStore()))
    runningMode(PLYRunningMode.Full)
}

Starting without any store is also a first-class path in SDK 6: Screens, analytics, Campaigns, deeplinks and user attributes all work, and store-dependent APIs return an explicit PLYError.NoStoreConfigured.


Did this page help you?