Amba

Monetization

Monetization control plane — plan, drift, export, definitions, adopt, and apply (additive + gated destructive) for your RevenueCat subscription config.

Manage your RevenueCat subscription config (entitlements, products, offerings, packages, paywalls) as Infrastructure-as-Code. plan, drift, export, and definitions only read; adopt writes only Amba's own state; apply pushes your declared config to RevenueCat as a durable, pollable operation (the endpoint returns an operation_id; writes are rate-limit-paced and idempotency-keyed). Apply runs additive ops (create entitlements, offerings, packages, paywall drafts; attach products) automatically and runs destructive ops (detach, archive entitlement/offering/product, remove package) only after you confirm the exact plan_hash. Store-product steps (create a declared product in RevenueCat, and — for App Store products declaring store_metadata.push_to_store — in App Store Connect through RevenueCat's store connection) additionally require an explicit confirmation of the started apply: an up-front confirm never auto-runs a store catalog write. Setting the default offering and publishing a paywall live are not RevenueCat API operations — apply returns them in refused with the exact dashboard step; store-side work no API performs (pricing, agreements/banking/tax, review, SKUs outside the App Store) is returned as a human_floor checklist with exact console locations.

plan/drift/export need a read-scoped secret API key. apply needs a read-write key (project_configuration:*:read_write) — supply it as secret_api_key_write on the RevenueCat integration, or use one full-access key for both. Amba resolves keys server-side and never returns them.

Endpoints

MethodPathDescription
GET/admin/projects/:projectId/monetization/planThree-way diff (declared vs adopted vs live) + store-floor + human-floor checklist + drift. Returns plan_hash.
GET/admin/projects/:projectId/monetization/driftDrift-only view: managed objects changed out-of-band in RevenueCat.
GET/admin/projects/:projectId/monetization/exportLive RevenueCat config projected into the declarative bundle shape.
GET/admin/projects/:projectId/monetization/definitionsThe current declared desired-state definitions.
POST/admin/projects/:projectId/monetization/adoptRecord the live config as the managed baseline (writes Amba state).
POST/admin/projects/:projectId/monetization/applyStart a durable apply (returns an operation_id; destructive ops gated behind confirm).

GET /admin/projects/:projectId/monetization/plan

Runs a read-only fetch of the live RevenueCat config and diffs it against your declared definitions and the last-adopted baseline. Applies nothing.

Response 200

{
  "data": {
    "provider": "revenuecat",
    "rc_project_id": "…",
    "plan_hash": "9f2c…",
    "clean": false,
    "ops": [
      {
        "op": "create",
        "object_type": "entitlement",
        "identifier": "premium",
        "status": "pending",
        "reason": "Declared entitlement \"premium\" does not exist in the provider."
      }
    ],
    "store_floor": [],
    "human_floor": [
      {
        "code": "store_pricing_required",
        "product_key": "monthly",
        "store": "app_store",
        "title": "Set the price for \"com.app.monthly\"",
        "detail": "No API sets store prices — after the product exists, set its price (and any intro/trial offers) in the store console. Declared price: 9.99 USD/month.",
        "location": "App Store Connect → My Apps → your app → Monetization (In-App Purchases / Subscriptions)"
      }
    ],
    "drift": [],
    "validation": []
  }
}

ops[].op is one of create | update | archive | attach | detach | set-current. ops[].status is pending (desired differs from live) or drift. A create on a product is a store-product step: apply registers the product with RevenueCat (and, with store_metadata.push_to_store, creates it in App Store Connect). store_floor lists BLOCKING gaps the apply can neither resolve nor create: products referenced by an entitlement or package but never declared, or declared products whose store has no connected RevenueCat app. human_floor is the store-side checklist no API performs (pricing, agreements/banking/tax, review, SKUs outside the App Store) — informational, with the exact console location per item. drift lists Amba-managed objects whose live state changed out-of-band (store products included). validation lists desired-state coherence problems that must be fixed before an apply (e.g. more than one offering declared is_currentMULTIPLE_CURRENT_OFFERINGS; a to-be-created product missing its typePRODUCT_TYPE_REQUIRED; push declared without its subscription inputs → PRODUCT_PUSH_INPUT_MISSING). plan_hash is a stable hash of the whole plan — pass it straight to apply, which refuses if the provider drifted off it.

Errors

  • 400 MONETIZATION_NOT_CONFIGURED — no active RevenueCat integration / no key.
  • 502 PROVIDER_READ_FAILED — RevenueCat unreachable or rate-limit exhausted.

GET /admin/projects/:projectId/monetization/drift

{
  "data": {
    "provider": "revenuecat",
    "rc_project_id": "…",
    "has_drift": true,
    "drift": [
      {
        "object_type": "entitlement",
        "identifier": "premium",
        "last_applied_hash": "…",
        "live_hash": "…"
      }
    ]
  }
}

GET /admin/projects/:projectId/monetization/export

{
  "data": {
    "provider": "revenuecat",
    "rc_project_id": "…",
    "monetization": {
      "entitlements": [
        {
          "key": "premium",
          "display_name": "Premium",
          "description": null,
          "product_keys": ["prod_monthly"]
        }
      ],
      "products": [
        {
          "key": "prod_monthly",
          "store": "app_store",
          "store_identifier": "com.app.monthly",
          "type": "subscription",
          "display_name": "Monthly",
          "store_metadata": {}
        }
      ],
      "offerings": [
        {
          "key": "default",
          "display_name": "Default",
          "is_current": true,
          "packages": [
            {
              "offering_key": "default",
              "key": "$rc_monthly",
              "product_key": "prod_monthly",
              "position": 0
            }
          ]
        }
      ],
      "paywalls": [
        { "key": "pw1", "offering_key": "default", "template": "template_1", "display": {} }
      ]
    }
  }
}

GET /admin/projects/:projectId/monetization/definitions

Returns the same monetization shape as export, but from your declared desired-state in Amba (not the live provider).

POST /admin/projects/:projectId/monetization/adopt

Writes the declared definitions AND the adopted baseline (statefile) for the selected objects, so the next plan is a clean no-op. Writes Amba state only — never RevenueCat. Idempotent.

Request

FieldTypeRequiredDescription
adopt_allbooleannoAdopt every live object. Provide this OR objects.
objectsarrayno[{ object_type, identifier }] to adopt a specific selection (max 500).

object_type is one of entitlement | product | offering | paywall. Packages are not independently adoptable — they are adopted together with their parent offering (adopting an offering brings all of its packages under management). A standalone package selector is rejected with 400 PACKAGE_NOT_INDEPENDENTLY_ADOPTABLE.

Response 200

{
  "data": {
    "provider": "revenuecat",
    "rc_project_id": "…",
    "adopted": 4,
    "entitlements": 1,
    "products": 1,
    "offerings": 1,
    "packages": 1,
    "paywalls": 1
  }
}

packages counts the packages brought under management as part of the offerings adopted in this call.

Errors

  • 400 INVALID_BODY — neither adopt_all nor a non-empty objects array, a malformed selector, or more than 500 objects.
  • 400 PACKAGE_NOT_INDEPENDENTLY_ADOPTABLE — a package selector was supplied; adopt its parent offering instead.
  • 409 MONETIZATION_NOT_MIGRATED — the monetization tables aren't present for this project yet.
  • 400 MONETIZATION_NOT_CONFIGURED — no active RevenueCat integration / no key.
  • 502 PROVIDER_READ_FAILED — RevenueCat unreachable or rate-limit exhausted.

POST /admin/projects/:projectId/monetization/apply

Starts a durable apply of your declared config to RevenueCat and returns a pollable operation_id. The endpoint verifies your plan_hash against a fresh plan and the store floor in-request (fast feedback), then runs the apply asynchronously: poll GET /admin/projects/:projectId/operations/:operationId until status is succeeded | failed (failed_reason explains a failure; result carries the final counts). Writes are paced against RevenueCat's API rate limits, every write carries an idempotency key (an interrupted apply resumes without double-creating), and the run survives restarts.

Additive ops (create entitlements, offerings, packages, paywall drafts; attach products to new packages/entitlements; update display metadata — including a product's display name) apply automatically. Destructive ops (detach a product, archive an entitlement/offering/product, remove a package) are gated: they run only after a confirmation of the exact plan_hash. Pass confirm up front to approve immediately, or apply without it — the response then has confirm_required: true and the apply waits (up to 24 hours) for you to re-call this endpoint with confirm, which approves the pending apply (same operation_id, response status: "confirm_sent"). Store-product steps go further: a plan that creates store products ALWAYS starts in the waiting state — confirm_required: true even when confirm was passed up front — and only runs after the explicit re-call. Creating real store catalog entries structurally requires a second deliberate approval. The confirming request's allow_detach_live and reconcile are the ones honored. A confirmation that no longer matches the plan is rejected and nothing destructive runs. set-current and paywall publish are not RevenueCat API operations and are always returned in refused with the dashboard step.

Before any write, the apply recomputes the plan against live RevenueCat and confirms your plan_hash still matches (re-plan if it drifted — including drift during the confirmation wait). On a confirmed destructive apply it enforces the ordering invariant before any write — it will not archive the live current offering (no API to move the default pointer) and will not detach a product a live customer resolves unless allow_detach_live is set. Confirmed store-product creates run in their topological slot (before anything that references them); gated teardown runs last, in safe order (detach → remove package → archive entitlement → archive product → archive offering). The apply reads back every write and stops on the first failure without rolling back (a re-attempted App Store push that already landed resolves cleanly). One apply runs at a time per project.

Request

FieldTypeRequiredDescription
plan_hashstringyesThe plan_hash from a fresh plan. Apply refuses if the provider drifted.
confirmstringnoSet equal to plan_hash to approve the gated destructive ops (up front, or to approve a waiting apply).
allow_detach_livebooleannoAccept detaching a product a live customer resolves (default false → such a detach is refused).
reconcilestringnoOut-of-band drift mode: amba (default, revert provider to your config) or adopt (pull live in).

Response 202

{
  "data": {
    "provider": "revenuecat",
    "operation_id": "5f0e…",
    "status": "started",
    "plan_hash": "9f2c…",
    "confirm_required": false,
    "gated": [],
    "refused": []
  }
}

status is started (a new apply is running — poll the operation), confirm_sent (your confirm was delivered to the waiting apply — keep polling the same operation), or succeeded (the plan was already clean; the operation is returned terminal with 200). confirm_required: true means the plan has gated ops and the apply is waiting for your confirmation — always the case when the plan contains store-product steps, even with an up-front confirm. gated lists the ops a confirmation would execute (entries with store: true are store catalog writes); refused lists ops with no RevenueCat API (set-current, paywall publish); human_floor is the store-side checklist accompanying any pending store-product steps. The settled operation's result carries applied, skipped_existing, gated_applied, failed, and (when a sandbox RevenueCat project is configured) a sandbox_stage report. Each apply run is also recorded for audit.

Errors

  • 400 INVALID_BODYplan_hash missing, or a malformed confirm / allow_detach_live / reconcile.
  • 409 MONETIZATION_PLAN_STALE — the provider drifted since the plan; re-plan and apply again.
  • 409 MONETIZATION_APPLY_IN_PROGRESS — an apply is already running for this project (its operation_id is in details); poll it, or re-call with confirm to approve a pending confirmation.
  • 422 MONETIZATION_STORE_FLOOR — a referenced product can neither be resolved nor created (dangling reference, or no RevenueCat app connected for the store); fix the declaration or connect the app first.
  • 409 MONETIZATION_NOT_MIGRATED — the monetization tables aren't present for this project yet.
  • 400 MONETIZATION_NOT_CONFIGURED — no active RevenueCat integration / no key.
  • 502 PROVIDER_READ_FAILED — RevenueCat unreachable while planning.
  • 502 APPLY_START_FAILED — the apply could not be started; try again.

A pre-write refusal discovered during the durable run (plan drift during the confirmation wait, an ordering-invariant violation, or a failed sandbox stage) settles the operation failed with a failed_reason explaining the exact condition — nothing was written.