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
| Method | Path | Description |
|---|---|---|
| GET | /admin/projects/:projectId/monetization/plan | Three-way diff (declared vs adopted vs live) + store-floor + human-floor checklist + drift. Returns plan_hash. |
| GET | /admin/projects/:projectId/monetization/drift | Drift-only view: managed objects changed out-of-band in RevenueCat. |
| GET | /admin/projects/:projectId/monetization/export | Live RevenueCat config projected into the declarative bundle shape. |
| GET | /admin/projects/:projectId/monetization/definitions | The current declared desired-state definitions. |
| POST | /admin/projects/:projectId/monetization/adopt | Record the live config as the managed baseline (writes Amba state). |
| POST | /admin/projects/:projectId/monetization/apply | Start 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
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_current → MULTIPLE_CURRENT_OFFERINGS; a to-be-created
product missing its type → PRODUCT_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
GET /admin/projects/:projectId/monetization/export
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
| Field | Type | Required | Description |
|---|---|---|---|
adopt_all | boolean | no | Adopt every live object. Provide this OR objects. |
objects | array | no | [{ 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
packages counts the packages brought under management as part of the offerings
adopted in this call.
Errors
400 INVALID_BODY— neitheradopt_allnor a non-emptyobjectsarray, a malformed selector, or more than 500 objects.400 PACKAGE_NOT_INDEPENDENTLY_ADOPTABLE— apackageselector 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
| Field | Type | Required | Description |
|---|---|---|---|
plan_hash | string | yes | The plan_hash from a fresh plan. Apply refuses if the provider drifted. |
confirm | string | no | Set equal to plan_hash to approve the gated destructive ops (up front, or to approve a waiting apply). |
allow_detach_live | boolean | no | Accept detaching a product a live customer resolves (default false → such a detach is refused). |
reconcile | string | no | Out-of-band drift mode: amba (default, revert provider to your config) or adopt (pull live in). |
Response 202
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_BODY—plan_hashmissing, or a malformedconfirm/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 (itsoperation_idis indetails); poll it, or re-call withconfirmto 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.