# PurchaseKit > In-app purchase infrastructure for Rails apps with Hotwire Native. Handles App Store and Google Play webhooks, normalizes subscription data, and syncs to your Rails app. - Website: https://purchasekit.com - Documentation: https://purchasekit.com/docs --- ## Getting started

PurchaseKit enables in-app purchases for Hotwire Native apps built with Rails.

Want to see it work first? Try the demo and make a test purchase in under 10 minutes.
It normalizes Apple and Google webhooks into a unified format, handles the complexity of subscription management, and provides drop-in bridge components for native purchases. ![Video of an iOS app making an in-app purchase with PurchaseKit](demo.gif) ### How it works 1. Your Rails app renders an HTML paywall 2. Hotwire Native displays it in your iOS/Android app 3. User taps subscribe, triggering StoreKit/Play Billing 4. Apple/Google send webhooks to PurchaseKit 5. PurchaseKit normalizes the data and forwards to your Rails app 6. Your app creates a subscription record and redirects the user ### Quick start 1. **Rails setup** - Choose your integration: - [Setup with Pay](rails/setup) - Automatic `Pay::Subscription` creation (recommended) - [Standalone setup](rails/standalone) - Custom subscription handling via callbacks 2. **Native setup** - Add the package to your Hotwire Native app: - [iOS setup](ios/setup) - Swift package for StoreKit - [Android setup](android/setup) - Kotlin library for Google Play Billing 3. **Store configuration** - Create products and configure webhooks: - [App Store Connect](ios/app-store-connect) - For iOS apps - [Google Play Console](android/google-play-console) - For Android apps 4. **Testing** - Test locally before going live: - [iOS testing](ios/testing) - StoreKit Configuration files - [Android testing](android/testing) - License testers ### Reference - [Webhook payload](webhooks/payload) - Normalized payload format and event types ### Prerequisites - A Rails application with [Hotwire Native](https://hotwire.dev) - An Apple Developer account (for iOS) - A Google Play Developer account (for Android) ### Support If you run into issues, check the testing guides for [iOS](ios/testing) or [Android](android/testing), or reach out at [joe@purchasekit.com](mailto:joe@purchasekit.com). --- ## Try the demo

See a working in-app purchase in under 10 minutes. No PurchaseKit account or Apple Developer account needed.

The demo runs entirely locally using Xcode's StoreKit Configuration. You'll make a test purchase and see how PurchaseKit handles the entire flow. ### Prerequisites - macOS with Xcode 26+ - Ruby 3.2+ with Bundler ### 1. Clone the repo ```bash git clone https://github.com/purchasekit/purchasekit-demo.git cd purchasekit-demo ``` ### 2. Start the Rails server ```bash cd demo/rails/pay bin/setup ``` The server starts on `http://localhost:3000`. Leave this terminal running. ### 3. Run the iOS app 1. Open `demo/ios/PurchaseKitDemo.xcodeproj` in Xcode 2. Select the **PurchaseKitDemo** scheme and an iPhone simulator 3. Press **Cmd+R** to build and run The app connects to your local Rails server automatically. ### 4. Make a test purchase 1. Sign in with `user@example.com` / `password` 2. Tap the **Subscribe** button on the paywall 3. Confirm the purchase in the StoreKit dialog 4. Watch the app redirect to the success page That's it. You just completed an in-app purchase flow! ### What just happened? 1. The Rails app rendered an HTML paywall 2. Hotwire Native displayed it in the iOS app 3. You tapped subscribe, triggering StoreKit 4. The gem's demo mode simulated the webhook locally 5. A `Pay::Subscription` record was created 6. The user was redirected to the success page In production, steps 4-5 happen via PurchaseKit's webhook infrastructure instead of locally. ### Next steps Ready to add purchases to your app? 1. [Create an account](/registration/new) on PurchaseKit 2. Follow the [Getting started guide](/docs) --- ## Setup with Pay

Add in-app purchases to your Rails app with the purchasekit gem.

This guide is for apps using the [Pay gem](https://github.com/pay-rails/pay). If you're not using Pay, see [Standalone setup](rails/standalone) instead. ### Installation Add the gem to your Gemfile: ```ruby gem "purchasekit" ``` Run bundle install: ```bash bundle install ``` The gem auto-detects Pay and enables integration automatically. ### Configuration Create an initializer at `config/initializers/purchasekit.rb`: ```ruby PurchaseKit.configure do |config| config.api_key = Rails.application.credentials.dig(:purchasekit, :api_key) config.app_id = Rails.application.credentials.dig(:purchasekit, :app_id) config.webhook_secret = Rails.application.credentials.dig(:purchasekit, :webhook_secret) end ``` Add your credentials: ```bash bin/rails credentials:edit ``` ```yaml purchasekit: api_key: sk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX app_id: app_XXXXXXXX webhook_secret: whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ``` You'll find these values in the PurchaseKit dashboard under **Account → Developer**. ### Demo mode For local development, you can use demo mode without PurchaseKit credentials: ```ruby PurchaseKit.configure do |config| config.demo_mode = Rails.application.credentials.dig(:purchasekit, :api_key).blank? # ... credentials end ``` Demo mode simulates the purchase flow locally. Disable it in production by setting credentials. ### Mount the engine Add the engine to your routes: ```ruby{3} ## config/routes.rb Rails.application.routes.draw do mount PurchaseKit::Engine, at: "/purchasekit" # ... end ``` This adds: - `/purchasekit/webhooks` - Receives webhooks from PurchaseKit - `/purchasekit/purchases` - Creates purchase intents for the native app ### JavaScript setup Import the Turbo Stream actions for real-time redirects after purchase: ```javascript // app/javascript/application.js import "purchasekit/turbo_actions" ``` Register the gem's Stimulus controllers: ```javascript{6} // app/javascript/controllers/index.js import { application } from "controllers/application" import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" eagerLoadControllersFrom("controllers", application) eagerLoadControllersFrom("purchasekit", application) ``` ### Set up Pay customer Ensure your user model has a PurchaseKit payment processor: ```ruby ## In your controller or concern current_user.set_payment_processor(:purchasekit) ``` ### Next step [Build your paywall](rails/paywall) with the gem's view helpers. --- ## Standalone setup

Use the purchasekit gem without Pay for custom subscription handling.

This guide is for apps that don't use the [Pay gem](https://github.com/pay-rails/pay). If you use Pay, see [Setup with Pay](rails/setup) instead. ### Installation Add the gem to your Gemfile: ```ruby gem "purchasekit" ``` Run bundle install: ```bash bundle install ``` ### Configuration Create an initializer at `config/initializers/purchasekit.rb`: ```ruby PurchaseKit.configure do |config| config.api_key = Rails.application.credentials.dig(:purchasekit, :api_key) config.app_id = Rails.application.credentials.dig(:purchasekit, :app_id) config.webhook_secret = Rails.application.credentials.dig(:purchasekit, :webhook_secret) end ``` Add your credentials: ```bash bin/rails credentials:edit ``` ```yaml purchasekit: api_key: sk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX app_id: app_XXXXXXXX webhook_secret: whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ``` You'll find these values in the PurchaseKit dashboard under **Account → Developer**. ### Demo mode For local development, you can use demo mode without PurchaseKit credentials: ```ruby PurchaseKit.configure do |config| config.demo_mode = Rails.application.credentials.dig(:purchasekit, :api_key).blank? # ... credentials end ``` Demo mode simulates the purchase flow locally. Disable it in production by setting credentials. ### Mount the engine Add the engine to your routes: ```ruby{3} ## config/routes.rb Rails.application.routes.draw do mount PurchaseKit::Engine, at: "/purchasekit" # ... end ``` This adds: - `/purchasekit/webhooks` - Receives webhooks from PurchaseKit - `/purchasekit/purchases` - Creates purchase intents for the native app ### JavaScript setup Import the Turbo Stream actions for real-time redirects after purchase: ```javascript // app/javascript/application.js import "purchasekit/turbo_actions" ``` Register the gem's Stimulus controllers: ```javascript{6} // app/javascript/controllers/index.js import { application } from "controllers/application" import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" eagerLoadControllersFrom("controllers", application) eagerLoadControllersFrom("purchasekit", application) ``` ### Event callbacks Register callbacks to handle subscription events. These fire when webhooks are received: ```ruby ## config/initializers/purchasekit.rb PurchaseKit.configure do |config| config.api_key = Rails.application.credentials.dig(:purchasekit, :api_key) config.app_id = Rails.application.credentials.dig(:purchasekit, :app_id) config.webhook_secret = Rails.application.credentials.dig(:purchasekit, :webhook_secret) config.on(:subscription_created) do |event| user = User.find(event.customer_id) user.subscriptions.create!( processor_id: event.subscription_id, store: event.store, store_product_id: event.store_product_id, status: event.status, current_period_end: event.current_period_end ) end config.on(:subscription_canceled) do |event| subscription = Subscription.find_by(processor_id: event.subscription_id) subscription&.update!(status: "canceled", ends_at: event.ends_at) end config.on(:subscription_expired) do |event| subscription = Subscription.find_by(processor_id: event.subscription_id) subscription&.update!(status: "expired") end end ``` ### Build your paywall Fetch products in your controller: ```ruby class PaywallsController < ApplicationController def show @annual = PurchaseKit::Product.find("prod_XXXXXXXX") @monthly = PurchaseKit::Product.find("prod_YYYYYYYY") end end ``` Render the paywall using the helper. Subscribe to Turbo Stream for real-time redirects: ```erb <%= turbo_stream_from "purchasekit_customer_#{current_user.id}" %> <%= purchasekit_paywall customer_id: current_user.id, success_path: dashboard_path do |paywall| %> <%= paywall.plan_option product: @annual, selected: true do %> Annual - <%= paywall.price %>/year <% end %> <%= paywall.plan_option product: @monthly do %> Monthly - <%= paywall.price %>/month <% end %> <%= paywall.submit "Subscribe" %> <% end %> <%= button_to "Restore purchases", restore_purchases_path %> ``` #### Paywall helper options | Option | Description | |--------|-------------| | `customer_id:` | Required. Your user/customer identifier | | `success_path:` | Where to redirect after purchase (defaults to `root_path`) | #### Builder methods | Method | Description | |--------|-------------| | `plan_option(product:, selected:)` | Radio button and label for a plan | | `price` | Localized price (must be inside `plan_option` block) | | `submit(text)` | Submit button (disabled until prices load) | #### Restore purchases Add a restore link that checks your server for an active subscription: ```ruby ## config/routes.rb post "restore_purchases", to: "subscriptions#restore" ## app/controllers/subscriptions_controller.rb def restore if current_user.subscribed? redirect_to dashboard_path, notice: "Your subscription is active." else redirect_to paywall_path, alert: "No active subscription found." end end ``` ### Available events | Event | Description | |-------|-------------| | `:subscription_created` | New subscription started | | `:subscription_updated` | Subscription renewed or plan changed | | `:subscription_canceled` | User canceled (still active until `ends_at`) | | `:subscription_expired` | Subscription ended | ### Event payload All events provide these methods: | Method | Description | |--------|-------------| | `event.event_id` | Unique event identifier (for idempotency) | | `event.customer_id` | Your user ID (passed when creating purchase intent) | | `event.subscription_id` | Store's subscription ID | | `event.store` | `"apple"` or `"google"` | | `event.store_product_id` | e.g., `"com.example.pro.annual"` | | `event.status` | `"active"`, `"canceled"`, `"expired"` | | `event.current_period_start` | Start of current billing period | | `event.current_period_end` | End of current billing period | | `event.ends_at` | When subscription will end (if canceled) | | `event.success_path` | Redirect path (for `subscription_created`) | ### Idempotency Webhooks may be delivered more than once. Write idempotent callbacks using `find_or_create_by` or check `event.event_id` to avoid duplicate side effects. ### Next step Add the native package to your Hotwire Native app: - [iOS setup](ios/setup) - Swift package for StoreKit - [Android setup](android/setup) - Kotlin library for Google Play Billing --- ## Paywall

Build an HTML paywall that triggers native in-app purchases.

### Create a paywall Fetch products in your controller: ```ruby class PaywallsController < ApplicationController def show @annual = PurchaseKit::Product.find("prod_XXXXXXXX") @monthly = PurchaseKit::Product.find("prod_YYYYYYYY") end end ``` You'll find product IDs in the PurchaseKit dashboard under **Apps → [Your App] → Products**. #### Minimal example ```erb <%= turbo_stream_from current_user.payment_processor %> <%= purchasekit_paywall customer_id: current_user.payment_processor.id, success_path: dashboard_path do |paywall| %> <%= paywall.plan_option product: @annual, selected: true do %> Annual - <%= paywall.price %>/year <% end %> <%= paywall.plan_option product: @monthly do %> Monthly - <%= paywall.price %>/month <% end %> <%= paywall.submit "Subscribe" %> <% end %> <%= button_to "Restore purchases", restore_purchases_path %> ``` #### Styled example (Bootstrap) ```erb <%= turbo_stream_from current_user.payment_processor %> <%= purchasekit_paywall customer_id: current_user.payment_processor.id, success_path: dashboard_path do |paywall| %>
<%= paywall.plan_option product: @annual, selected: true, class: "btn btn-outline-primary text-start" do %> Annual <%= paywall.price %>/year <% end %> <%= paywall.plan_option product: @monthly, class: "btn btn-outline-primary text-start" do %> Monthly <%= paywall.price %>/month <% end %>
<%= paywall.submit "Subscribe", class: "btn btn-primary w-100" %>
<%= button_to "Restore purchases", restore_purchases_path, class: "btn btn-link link-secondary small p-0" %>
<% end %> ``` ### Paywall helper options | Option | Description | |--------|-------------| | `customer_id:` | Required. Your `Pay::Customer` ID (use `current_user.payment_processor.id`) | | `success_path:` | Where to redirect after purchase (defaults to `root_path`) | ### Builder methods | Method | Description | |--------|-------------| | `plan_option(product:, selected:)` | Radio button and label for a plan | | `price` | Localized price (must be inside `plan_option` block) | | `submit(text)` | Submit button (disabled until prices load) | ### Restore purchases Add a restore link that checks your server for an active subscription: ```ruby ## config/routes.rb post "restore_purchases", to: "subscriptions#restore" ## app/controllers/subscriptions_controller.rb def restore if current_user.subscribed? redirect_to dashboard_path, notice: "Your subscription is active." else redirect_to paywall_path, alert: "No active subscription found." end end ``` ### Webhook events The gem handles these events automatically: | Event | Action | |-------|--------| | `subscription.created` | Creates `Pay::Subscription`, redirects user | | `subscription.updated` | Updates status and period dates | | `subscription.canceled` | Marks subscription as canceled | | `subscription.expired` | Marks subscription as expired | ### Next step Add the native package to your Hotwire Native app: - [iOS setup](ios/setup) - Swift package for StoreKit - [Android setup](android/setup) - Kotlin library for Google Play Billing --- ## iOS setup

Add the PurchaseKit Swift package to your Hotwire Native iOS app.

### Installation Add the package via Swift Package Manager in Xcode: 1. Open your Xcode project 2. Go to **File → Add Package Dependencies** 3. Enter the package URL: ``` https://github.com/purchasekit/purchasekit-ios ``` 4. Select the latest version and click **Add Package** ![Adding the PurchaseKit package in Xcode](xcode-add-package.png) ### Register the bridge component In your app's setup code, register the `PaywallComponent` with Hotwire Native: ```swift{8-10} import HotwireNative import PurchaseKit import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Hotwire.registerBridgeComponents([ PaywallComponent.self ]) return true } } ``` That's it! The component automatically: - Listens for price requests from your web paywall - Handles StoreKit purchases when the user subscribes - Finishes transactions (fulfillment happens via webhooks) ### Environment detection PurchaseKit automatically detects whether the app is running in sandbox or production: | Build type | Environment | |------------|-------------| | Simulator | sandbox | | Development build | sandbox | | TestFlight | sandbox | | App Store | production | This ensures sandbox purchases don't accidentally get treated as production (and vice versa). ### Requirements - iOS 16.0+ - Xcode 15.0+ - Hotwire Native iOS 1.2.0+ ### Next step [Create your subscriptions](ios/subscriptions) in App Store Connect. --- ## Creating subscriptions in App Store Connect

Create the products users will purchase. PurchaseKit uses these product IDs to match purchases to your app.

### Create a subscription group Subscription groups let users upgrade or downgrade between plans without being charged twice. 1. In App Store Connect, go to your app 2. Select **Subscriptions** in the sidebar 3. Click the **+** button next to **Subscription Groups** 4. Enter a reference name (e.g., "Pro Plans") ![Creating a new subscription group](asc-create-group.png) ### Add a subscription 1. Within your subscription group, click **Create** next to **Subscriptions** 2. Enter a **Reference Name** (internal only, e.g., "Pro Annual") 3. Enter a **Product ID** - this is what you'll use in PurchaseKit: - Use reverse domain notation: `com.yourapp.pro.annual` - Be consistent: `com.yourapp.pro.monthly`, `com.yourapp.pro.annual` ![Creating a new subscription with product ID](asc-create-subscription.png) 4. Click **Create** 5. Configure subscription details: - **Subscription Duration** (1 week, 1 month, 1 year, etc.) - **Availability** → select which countries can purchase - **Subscription Prices** → click **Add Subscription Price** ### Add the product to PurchaseKit 1. In the PurchaseKit dashboard, go to your app 2. Click **Add a product** 3. Enter the Apple Product ID (e.g., `com.yourapp.pro.annual`) 4. Save the product The product ID in PurchaseKit must exactly match the Product ID in App Store Connect. ### Next step Configure [App Store Connect](ios/app-store-connect) to send webhooks to PurchaseKit. --- ## App Store Connect webhooks

Connect App Store Connect to PurchaseKit so you receive real-time subscription updates (renewals, cancellations, refunds).

### Bundle identifier The bundle identifier connects your iOS app to App Store Connect and PurchaseKit. #### Find your bundle ID in Xcode 1. Open your Xcode project 2. Select your app target 3. Go to the **Signing & Capabilities** tab 4. Find **Bundle Identifier** ![The bundle identifier in Xcode's Signing & Capabilities tab](xcode-bundle-id.png) #### Verify in App Store Connect 1. In App Store Connect, select your app 2. Go to **App Information** 3. Confirm the **Bundle ID** matches your Xcode project #### Configure in PurchaseKit 1. In the PurchaseKit dashboard, go to your app settings 2. Enter your **Apple Bundle ID** 3. Save changes PurchaseKit uses the bundle ID to match incoming Apple webhooks to the correct app. ### Server Notifications Apple sends server notifications when subscriptions are created, renewed, or canceled. PurchaseKit receives these webhooks and forwards them to your Rails app. 1. Open [App Store Connect](https://appstoreconnect.apple.com) 2. Select your app 3. Navigate to **App Information** in the sidebar 4. Scroll to **App Store Server Notifications** ![The App Store Server Notifications section in App Information](asc-webhook-section.png) 5. For **Production Server URL**, enter: ``` https://purchasekit.com/webhooks/apple ``` 6. For **Sandbox Server URL**, enter the same URL (PurchaseKit handles both environments) 7. Click **Save** ### Next step [Test locally](ios/testing) with StoreKit Configuration files. --- ## Testing on iOS

Test your PurchaseKit integration locally without receiving real Apple webhooks.

### StoreKit Configuration files Xcode's StoreKit Configuration files let you test purchases in the simulator without connecting to App Store Connect. PurchaseKit automatically detects these purchases and completes them locally. #### Create a StoreKit Configuration file 1. In Xcode, go to **File → New → File** 2. Search for "StoreKit" and select **StoreKit Configuration File** 3. Name it (e.g., `StoreKit.storekit`) and save it in your project ![Creating a new StoreKit Configuration file](xcode-storekit-new.png) #### Sync products from App Store Connect 1. Open your `.storekit` file 2. Check **Sync this file with an app in App Store Connect** 3. Select your app from the dropdown This automatically imports your subscriptions and keeps them in sync. #### Enable the configuration 1. In Xcode, go to **Product → Scheme → Edit Scheme** 2. Select **Run** in the sidebar 3. Go to the **Options** tab 4. Set **StoreKit Configuration** to your `.storekit` file ![Enabling the StoreKit Configuration in your scheme](xcode-storekit-enable.png) #### Clear test purchases To reset and test again: 1. In Xcode, go to **Debug → StoreKit → Manage Transactions** 2. Select and delete previous transactions 3. Or use **Debug → StoreKit → Clear Purchase History** ### Testing webhooks locally To test real Apple sandbox webhooks locally: 1. Expose your local Rails app with [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/): ```bash cloudflared tunnel --url http://localhost:3000 ``` 2. In the PurchaseKit dashboard, set your app's **Sandbox Webhook URL** to your tunnel URL: ``` https://example-tunnel.trycloudflare.com/purchasekit/webhooks ``` 3. Make a sandbox purchase on a real device (not simulator) 4. Watch the webhook arrive in your Rails logs #### Sandbox subscription durations Apple accelerates sandbox subscriptions for faster testing: | Production duration | Sandbox duration | |---------------------|------------------| | 1 week | 3 minutes | | 1 month | 5 minutes | | 2 months | 10 minutes | | 3 months | 15 minutes | | 6 months | 30 minutes | | 1 year | 1 hour | #### Test webhook script The example Rails app includes a script to test webhook handling: ```bash bin/test_webhook created # Test subscription.created bin/test_webhook updated # Test subscription.updated bin/test_webhook canceled # Test subscription.canceled bin/test_webhook expired # Test subscription.expired ``` --- ## Android setup

Add the PurchaseKit library to your Android app and register the bridge component.

### Installation #### Add JitPack repository Add JitPack to your project's `settings.gradle.kts`: ```kotlin{5} dependencyResolutionManagement { repositories { google() mavenCentral() maven { url = uri("https://jitpack.io") } } } ``` #### Add the dependency Add PurchaseKit to your app's `build.gradle.kts`: ```kotlin dependencies { implementation("com.github.purchasekit:purchasekit-android:0.1.0") } ``` ### Register the bridge component In your Application class, register the `PaywallComponent`: ```kotlin{3,12-14} import android.app.Application import dev.hotwire.core.bridge.BridgeComponentFactory import dev.hotwire.core.bridge.KotlinXJsonConverter import dev.hotwire.core.config.Hotwire import dev.hotwire.navigation.config.registerBridgeComponents import dev.purchasekit.android.PaywallComponent class MyApplication : Application() { override fun onCreate() { super.onCreate() Hotwire.registerBridgeComponents( BridgeComponentFactory("paywall", ::PaywallComponent) ) Hotwire.config.jsonConverter = KotlinXJsonConverter() } } ``` The component name `"paywall"` must match the name used in your web app's JavaScript. ### How it works When your web paywall loads, it sends a `prices` message to the native app. PurchaseKit fetches localized prices from Google Play and returns them to the web. When the user taps Subscribe, your web app sends a `purchase` message with: - `googleStoreProductId` - The product ID from Google Play Console - `correlationId` - A UUID that links this purchase to your PurchaseKit Purchase Intent PurchaseKit launches the Google Play purchase flow and returns the result. ### Testing #### License testers Add your test accounts to Play Console → Setup → License testing. License testers can make purchases without being charged. #### Internal testing For full end-to-end testing: 1. Create an internal testing track in Play Console 2. Upload your APK or AAB 3. Add testers and share the opt-in link 4. Testers install via the Play Store ### Next step [Create your subscriptions](android/subscriptions) in Google Play Console. --- ## Creating subscriptions in Google Play Console

Create the products users will purchase. PurchaseKit uses these product IDs to match purchases to your app.

### Create a subscription 1. In [Google Play Console](https://play.google.com/console), select your app 2. Go to **Monetize** → **Subscriptions** 3. Click **Create subscription** 4. Enter a **Product ID** - this is what you'll use in PurchaseKit: - Use reverse domain notation: `com.yourapp.pro.annual` - Be consistent: `com.yourapp.pro.monthly`, `com.yourapp.pro.annual` 5. Enter a name and description 6. Click **Create** ### Add a base plan Each subscription needs at least one base plan: 1. Within your subscription, click **Add base plan** 2. Enter a **Base plan ID** (e.g., `annual`) 3. Set the **Billing period** (weekly, monthly, yearly, etc.) 4. Click **Set price** and configure pricing for each country 5. Click **Activate** ### Add the product to PurchaseKit 1. In the PurchaseKit dashboard, go to your app 2. Click **Add a product** 3. Enter the Google Product ID (e.g., `com.yourapp.pro.annual`) 4. Save the product The product ID in PurchaseKit must exactly match the Product ID in Google Play Console. ### Next step Configure [Google Play Console](android/google-play-console) to send webhooks to PurchaseKit. --- ## Google Play Console webhooks

Connect Google Play to PurchaseKit so you receive real-time subscription updates (renewals, cancellations, refunds).

### Package name The package name connects your Android app to Google Play Console and PurchaseKit. #### Find your package name 1. Open your Android Studio project 2. Open `app/build.gradle.kts` (or `build.gradle`) 3. Find the `applicationId` in the `android` block: ```kotlin android { namespace = "com.yourapp.android" defaultConfig { applicationId = "com.yourapp.android" } } ``` #### Configure in PurchaseKit 1. In the PurchaseKit dashboard, go to your app settings 2. Enter your **Google Package Name** 3. Save changes PurchaseKit uses the package name to match incoming Google webhooks to the correct app. ### Automated setup (recommended) Run our script in Google Cloud Shell to configure the Pub/Sub topic and service account. #### Open Google Cloud Shell 1. Go to [Google Cloud Console](https://console.cloud.google.com) 2. Select your project from the dropdown at the top 3. Click the **Cloud Shell** icon (terminal icon) in the top-right toolbar 4. Wait for the shell to initialize #### Create and run the script 1. In Cloud Shell, click **Open Editor** (pencil icon) to open the editor 2. Click **File → New File** 3. Copy the script from [purchasekit.com/scripts/google-setup.sh](/scripts/google-setup.sh) and paste it 4. Change line 18: replace `your_project_id` with your Google Cloud project ID 5. Click **File → Save As** and name it `setup.sh` 6. Click **Open Terminal** to return to the terminal 7. Run the script: ``` bash setup.sh ``` ![Google Cloud Shell Editor](google-cloud-shell-editor.png) After the script completes: 1. [Add the service account to Play Console](#link-to-google-play-console) 2. [Enable real-time notifications](#enable-real-time-notifications) in your app 3. [Upload the JSON key](https://purchasekit.com/account/developer) to PurchaseKit --- ### Manual setup If you prefer to configure everything manually, follow the steps below. #### Google Cloud project 1. Go to [Google Cloud Console](https://console.cloud.google.com) 2. Click the project dropdown at the top 3. Click **New Project** 4. Enter a name (e.g., "My App Purchases") 5. Click **Create** #### Enable APIs 1. In Google Cloud Console, go to **APIs & Services** → **Library** 2. Search for and enable: - **Cloud Pub/Sub API** - **Google Play Developer API** #### Create a Pub/Sub topic 1. Go to **Pub/Sub** → **Topics** 2. Click **Create Topic** 3. Enter a topic ID (e.g., `purchasekit-play-notifications`) 4. Uncheck "Add a default subscription" 5. Click **Create** #### Create a push subscription 1. Click on your newly created topic 2. Click **Create Subscription** 3. Enter a subscription ID (e.g., `purchasekit-push`) 4. Set **Delivery type** to **Push** 5. Enter the PurchaseKit webhook URL: ``` https://purchasekit.com/webhooks/google ``` 6. Click **Create** #### Grant Pub/Sub permissions Google Play needs permission to publish to your topic: 1. Go to **Pub/Sub** → **Topics** 2. Click on your topic 3. Click the **Permissions** tab 4. Click **Grant Access** 5. For **New principals**, enter: ``` google-play-developer-notifications@system.gserviceaccount.com ``` 6. For **Role**, select **Pub/Sub Publisher** 7. Click **Save** #### Create a service account PurchaseKit needs a service account to fetch subscription details from Google Play. 1. In Google Cloud Console, go to **IAM & Admin** → **Service Accounts** 2. Click **Create Service Account** 3. Enter a name (e.g., "PurchaseKit") 4. Click **Create and Continue** 5. Skip the optional steps and click **Done** #### Generate a key 1. Click on your new service account 2. Go to the **Keys** tab 3. Click **Add Key** → **Create new key** 4. Select **JSON** 5. Click **Create** 6. Save the downloaded file securely #### Link to Google Play Console 1. Open [Google Play Console](https://play.google.com/console) 2. Go to **Users and permissions** (in the left sidebar, under Setup) 3. Click **Invite new users** 4. Enter the service account email (looks like `name@project.iam.gserviceaccount.com`) 5. Set permissions: - **Account permissions**: View app information and download bulk reports - **Financial data**: View financial data, orders, and cancellation survey responses 6. Click **Invite user** 7. Click **Apply** on the access level page **Note:** Permissions can take up to 24 hours to propagate. #### Upload to PurchaseKit 1. In the PurchaseKit dashboard, go to **Account** → **Developer** 2. Upload the JSON key file you downloaded 3. Click **Upload credentials** #### Enable real-time notifications 1. In Google Play Console, select your app 2. Go to **Monetize** → **Monetization setup** 3. Scroll to **Real-time developer notifications** 4. Enter your Cloud Pub/Sub topic name: ``` projects/YOUR_PROJECT_ID/topics/purchasekit-play-notifications ``` (Replace `YOUR_PROJECT_ID` with your Google Cloud project ID) 5. Click **Save changes** #### Test the connection 1. Click **Send test notification** 2. Check the PurchaseKit dashboard for the incoming webhook 3. If no webhook appears, verify: - Pub/Sub subscription URL is correct - Pub/Sub Publisher permission is granted - Service account permissions have propagated (wait 24 hours) ### Next step Return to [Getting started](/docs) to complete your PurchaseKit integration, or see [Android setup](android/setup) if you haven't added the PurchaseKit library yet. --- ## Testing on Android

Test your PurchaseKit integration with license testers and internal testing tracks.

### License testers License testers can make purchases without being charged. This is the easiest way to test during development. #### Add license testers 1. Open [Google Play Console](https://play.google.com/console) 2. Go to **Setup → License testing** 3. Add the email addresses of your test accounts 4. Click **Save changes** License testers must use the same Google account on their test device. ### Internal testing track For full end-to-end testing with real Play Store downloads: 1. In Play Console, go to **Testing → Internal testing** 2. Click **Create new release** 3. Upload your APK or AAB 4. Click **Save** and then **Review release** 5. Click **Start rollout to Internal testing** #### Add testers 1. Go to **Testing → Internal testing → Testers** 2. Create a new email list or use an existing one 3. Add tester email addresses 4. Copy the **Join on the web** link and share with testers Testers must accept the invite and install from the Play Store to test purchases. ### Testing webhooks locally To test real Google Play webhooks locally: 1. Expose your local Rails app with [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/): ```bash cloudflared tunnel --url http://localhost:3000 ``` 2. In the PurchaseKit dashboard, set your app's **Sandbox Webhook URL** to your tunnel URL: ``` https://example-tunnel.trycloudflare.com/purchasekit/webhooks ``` 3. Make a test purchase with a license tester account 4. Watch the webhook arrive in your Rails logs ### Test vs production purchases Google Play doesn't have a separate sandbox environment like Apple. Instead: - **License testers** make test purchases that don't charge their payment method - **Non-testers** make real purchases that are charged PurchaseKit detects test purchases via Google's `testPurchase` flag and marks them as sandbox environment. --- ## Purchase intents

Track purchases from "user tapped subscribe" to "subscription created."

### View purchase intents In the PurchaseKit dashboard: 1. Go to your app 2. Select a product 3. Click **Purchase intents** You'll see all intents with their status: - **Pending** - Waiting for store webhook - **Completed** - Subscription created successfully ### Manually complete an intent For sandbox testing, you can manually complete a pending intent: 1. Find the pending intent in the dashboard 2. Click **Complete** This simulates receiving a webhook from the app store and triggers your Rails app's webhook handler. The user will be redirected to their success path. > **Note:** Manual completion is only available for sandbox intents. Production intents must come from real Apple or Google webhooks. ### Intent fields | Field | Description | |-------|-------------| | `identifier` | Public ID (e.g., `pi_QCCWGR8F`) | | `uuid` | Correlation ID sent to the store (Apple's `appAccountToken` or Google's `obfuscatedAccountId`) | | `customer_id` | Your `Pay::Customer` ID | | `success_path` | Redirect destination after purchase | | `status` | `pending` or `completed` | | `environment` | `sandbox`, `production`, or `xcode` |