Amba

Monetization Quickstart: Zero to Paid

Declare your subscription config as code, apply it through RevenueCat, provision the store products, wire the webhook, and gate features with has() — start to finish.

This guide takes you (or your coding agent) from an empty project to a paying subscriber: declare entitlements, products, and offerings as Infrastructure-as-Code, review a plan, apply it to RevenueCat — including creating the real App Store products — wire the purchase webhook, and gate features in the app with Amba.entitlements.has().

Amba configures and deepens RevenueCat: RevenueCat validates receipts and manages the subscription lifecycle; Amba gives that configuration a declarative source of truth, a reviewable plan/apply workflow, and connects every purchase into the rest of your backend — segments, push, XP, currencies, webhooks.

Everything below has an MCP equivalent, so a coding agent can run the whole flow — see the agent mapping at the end. The full semantics of each step live in the Monetization Control Plane reference.

0. Prerequisites

  1. A RevenueCat project with your app added (iOS and/or Android).

  2. Connect RevenueCat to Amba — configure the integration with a secret API key. Plan/drift/export need a read-scoped key (project_configuration:*:read); apply needs a read-write key (project_configuration:*:read_write) supplied as secret_api_key_write — or use one full-access key for both:

    amba_integrations_configure({
      project_id: "proj_xxx",
      provider: "revenuecat",
      config: {
        secret_api_key: "sk_…",          // read — plan / drift / export
        secret_api_key_write: "sk_…",    // read-write — apply
        shared_secret: "webhook_shared_secret"
      }
    })

    Keys are resolved server-side and never returned to a client.

  3. (For App Store product creation) connect your App Store Connect API key to RevenueCat (RevenueCat dashboard → your app's configuration). Apply creates App Store products through that connection.

1. Declare the config

Author the desired state in your project's amba.config.json (the same declarative bundle amba apply uses for everything else) under a monetization section:

{
  "version": 1,
  "monetization": {
    "entitlements": [
      {
        "key": "pro",
        "display_name": "Pro",
        "description": "Unlocks all premium features",
        "product_keys": ["monthly", "yearly"]
      }
    ],
    "products": [
      {
        "key": "monthly",
        "store": "app_store",
        "store_identifier": "com.yourapp.pro.monthly",
        "type": "subscription",
        "display_name": "Pro Monthly",
        "store_metadata": {
          "push_to_store": true,
          "duration": "ONE_MONTH",
          "subscription_group_name": "Pro",
          "price": "9.99 USD/month"
        }
      },
      {
        "key": "yearly",
        "store": "app_store",
        "store_identifier": "com.yourapp.pro.yearly",
        "type": "subscription",
        "display_name": "Pro Yearly",
        "store_metadata": {
          "push_to_store": true,
          "duration": "ONE_YEAR",
          "subscription_group_name": "Pro",
          "price": "79.99 USD/year"
        }
      }
    ],
    "offerings": [
      {
        "key": "default",
        "display_name": "Default",
        "is_current": true,
        "packages": [
          {
            "offering_key": "default",
            "key": "$rc_monthly",
            "product_key": "monthly",
            "position": 0
          },
          {
            "offering_key": "default",
            "key": "$rc_annual",
            "product_key": "yearly",
            "position": 1
          }
        ]
      }
    ],
    "paywalls": []
  }
}

store_metadata.push_to_store: true asks apply to create the real product in App Store Connect (subscriptions also need duration — one of ONE_WEEK | ONE_MONTH | TWO_MONTHS | THREE_MONTHS | SIX_MONTHS | ONE_YEAR — and subscription_group_name). price is documentation: no API sets store prices, so it's echoed into the human-floor checklist for the copy-paste step.

Load the declared definitions into Amba:

amba apply

This writes Amba's declared desired-state only — it does not touch RevenueCat. The next plan shows everything as pending create ops.

Already configured RevenueCat by hand? Run amba monetization export --file monetization.json to snapshot the live config into this exact shape, then amba monetization adopt --all to bring it under management. From there, edit the declaration and plan/apply as below.

2. Plan

amba monetization plan

The plan is a three-way diff (declared vs. last-adopted baseline vs. live RevenueCat config). Review four things before applying:

  • ops — what an apply would do: create / update / attach / archive / detach / set-current. A create on a product is a store-product step: apply registers it with RevenueCat and (with push_to_store) creates it in App Store Connect.
  • validation — desired-state problems you must fix first (e.g. two offerings declaring is_current, a new product missing its type).
  • store_floorblocking gaps the apply can neither resolve nor create: a product referenced by an entitlement or package but never declared, or a declared product whose store has no connected RevenueCat app. These fail the apply up front (422) with the exact fix.
  • human_floor — the checklist of store-side steps no API performs — for anyone (see step 4). Informational, with the exact console location per item.

The plan returns a plan_hash — a stable hash of exactly what you reviewed. Apply refuses to run against a stale one.

3. Apply

amba monetization apply

Apply pushes the declared config to RevenueCat as a durable, pollable operation: the CLI starts the apply, gets an operation_id, and polls it to completion for you (over the API, poll GET /v1/admin/projects/:projectId/operations/:operationId until succeeded | failed). Writes are paced against RevenueCat's API rate limits — a large plan takes a few minutes — and every write carries an idempotency key, so an interrupted apply resumes safely without double-creating anything.

Additive ops (create entitlements / offerings / packages / paywall drafts; attach products) run automatically. Two classes are gated:

  • Destructive ops (detach a product, archive an entitlement / offering / product, remove a package) wait for a confirmation of the exact plan_hash.
  • Store-product steps go further: a plan that creates store products always starts in a waiting state — even if you passed --confirm up front. Creating real store catalog entries structurally requires a second deliberate look.

When the apply reports it's waiting, review the pending ops and the human-floor checklist, then approve:

amba monetization apply --confirm

If the plan changed in between, the confirmation is rejected and nothing gated runs — re-plan and apply again. An unconfirmed apply expires after 24 hours. Two safety rails hold even after you confirm: apply never archives the offering customers currently see, and never detaches the last product a live customer resolves unless you pass --allow-detach-live.

4. Finish the human-floor checklist

Some store-side steps have no API — anywhere, for anyone. The plan listed them with exact console locations; after a first apply they are typically:

StepWhere
PricingApp Store Connect → My Apps → your app → Monetization (In-App Purchases / Subscriptions). Your declared price is echoed in the checklist.
Agreements / banking / taxApp Store Connect → Business (Paid Applications Agreement); Play Console → Setup → Payments profile. One-time, account-level.
Play / Amazon SKUsRevenueCat's store push covers App Store Connect; products on other stores are registered with RevenueCat by apply, and the SKU itself is a copy-paste job in that store's console.
Default offering / paywall publishRevenueCat dashboard — RevenueCat has no API for "Make Default" or publishing a paywall live; apply surfaces the exact dashboard step.
App reviewNewly created App Store in-app purchases ship with your next app review submission.

5. Wire the purchase webhook

Purchases flow back into Amba through RevenueCat's webhook. In the RevenueCat dashboard (Project Settings → Integrations → Webhooks), register:

https://api.amba.dev/webhooks/revenuecat?project_id=proj_xxx

with the same shared_secret you set on the integration. From then on, INITIAL_PURCHASE / RENEWAL / CANCELLATION / EXPIRATION events write straight into user_entitlements — idempotent, order-safe, and expiry-aware. Details: RevenueCat integration.

6. Gate features in the app

The app reads Amba — one entitlement check regardless of which store the purchase came from:

import { Amba } from '@layers/amba-expo';
 
// Render the paywall from the offering you applied
const { offerings, current_offering_id } = await Amba.offerings();
 
// After purchase (or any time): gate the feature
const isPro = await Amba.entitlements.has('pro');
 
// App Store guideline 3.1.1 — wire a Restore Purchases button
const { restored_count } = await Amba.entitlements.restore();

has() / list() are available on every SDK; offerings() and restore() need @layers/amba-web ≥ 4.0.5, @layers/amba-node ≥ 4.0.6, @layers/amba-react-native ≥ 4.0.5 (Expo inherits), or the iOS / Android SDK ≥ 4.1.0. Don't OR-merge a separate purchase-SDK check on the client — Amba already reflects every purchase, renewal, and expiry (why).

Selling on the web too? Connect your own Stripe Billing account — the path RevenueCat documents for web checkout — and web purchases grant the same entitlements: Web Subscriptions.

7. Cascade: turn subscribers into engaged users

Every grant emits neutral entitlement.granted and subscription.started / subscription.renewed events, so the rest of Amba composes with zero glue:

  • Auto-reward — bind a currency grant rule or XP rule to subscription.started to grant gems / XP on subscribe (idempotent — at most once per billing period).
  • Segments — target entitlements.is_active / entitlements.product_id in segment rules, then point push campaigns or remote-config overrides at the segment.
  • Webhooks — subscribe your own systems to the same events with outbound webhooks.

8. Keep it converged: drift and adopt

The RevenueCat dashboard stays fully usable — the control plane is deliberately leaky by design. Out-of-band edits to managed objects aren't blocked or reverted behind your back; they're observed (a daily sweep plus every plan records them as drift) and you choose how to reconcile:

amba monetization drift                                 # what changed out-of-band
amba monetization apply --confirm --reconcile adopt     # keep the dashboard change — pull it into your config
amba monetization apply --confirm --reconcile amba      # revert RevenueCat to your declared config (default)

amba monetization adopt --all records the live config as the managed baseline at any time (it writes Amba state only — never RevenueCat).

Every step, for agents

StepCLIMCP tool
Connect RevenueCatamba_integrations_configure
Declare definitionsamba applyamba_project_import (bundle with a monetization section)
Planamba monetization planamba_monetization_plan
Apply / confirmamba monetization apply [--confirm]amba_monetization_apply (poll with amba_operations_get)
Human-floor checklistin plan / apply outputreturned by both tools as human_floor
Driftamba monetization driftamba_monetization_drift
Adoptamba monetization adopt --allamba_monetization_adopt
Exportamba monetization exportamba_monetization_export
Entitlement checkAmba.entitlements.has() in the app (SDK)

Full reference: Monetization Control Plane · Admin API · Entitlements

On this page