Commerce is an optional service that validates your in-app purchases and subscriptions server-side and gives your app a single, store-agnostic question to ask: does this user have this entitlement right now? It wraps the existing Purchase API (see the Monetization chapter) — purchases still go through the platform store — and adds cloud receipt validation, a normalized subscription state machine across Apple and Google, lifecycle webhooks to your backend, and a revenue console.
Codename One doesn’t process payments and never touches your money — the stores (and, for physical goods, your own payment processor) do. Commerce is infrastructure that sits beside the money flow, exactly like the third-party services it replaces.
Entitlements, not SKUs
The core idea is the entitlement: an abstract access right such as pro or remove_ads. You map one or more store products to an entitlement, and your code only ever checks the entitlement:
if (CommerceManager.getInstance().isEntitled("pro")) {
// unlock pro features
}
isEntitled reads the answer the cloud last cached, so it’s only as fresh as your last refresh(): before one completes the cache is empty and a paying subscriber reads as unentitled. It falls back to asking the store for a subscription named after the entitlement, and since the entitlement-to-SKU mapping lives in the cloud rather than on the device, that only matches when a product happens to share the id. Where the cloud can’t answer — commerce not enabled in the build, an offline start, a degraded account — check the granting SKU yourself, as the post-purchase example below does. Don’t do it when the cloud answered no: a refunded or revoked receipt still looks active on the device, and treating that as a grant unlocks the app after the server denied it.
An entitlement is active when any of its granting products is active. This matters: a promotional grant and a paid subscription, or a sandbox and a production purchase, can both map to pro without one hiding the other. Access is decided by a single authoritative rule — the latest expiry over the active grants, evaluated against a server timestamp — so a tampered device clock can’t extend it. Everything else (will-renew, billing stage, period type, family sharing) is advisory and never removes access on its own.
Client usage
In your Lifecycle.init (or wherever you set up purchases). Installing a receipt store is part of the setup, not an optional extra: Purchase submits pending purchases and reloads its receipt list only when one is installed, so without it refresh() finds no receipts and no entitlement is ever granted. On iOS isSubscriptionSupported() returns false until one is installed, so subscriptions don’t start at all. The monetization chapter builds a store you can use here.
CommerceManager cm = CommerceManager.getInstance();
cm.setAppUserId(myAccountId); // optional; a stable id for the signed-in user
// A receipt store is a prerequisite rather than an extra. Purchase only
// submits pending purchases and reloads its receipt list when one is
// installed, so without it refresh() finds nothing and no entitlement is
// ever granted. On iOS isSubscriptionSupported() is false outright,
// because it is defined as "a receipt store is installed". The
// Monetization chapter builds one.
Purchase.getInAppPurchase().setReceiptStore(myReceiptStore);
Drive purchases through the manager (these delegate to the Purchase API):
cm.subscribe("pro_monthly");
// or cm.purchase("remove_ads");
After a purchase, or on app start, validate the device’s receipts with the cloud and refresh the entitlement cache. refresh() validates the receipts already stored rather than waiting for one in flight, so chain it off receipt synchronization after a purchase — otherwise it runs before the new receipt has been submitted and nothing retries it. It’s a blocking network call, so keep it off the EDT:
// Purchase submits a new receipt to the receipt store asynchronously,
// and refresh() only validates the receipts already stored. Called
// straight after a purchase it would not see the new one, and nothing
// retries it later, so chain it off the synchronization instead.
Purchase.getInAppPurchase().synchronizeReceipts(0, synced -> {
if (!Boolean.TRUE.equals(synced)) {
// The receipt was not submitted or fetched, so it is still
// pending and the stored list is the old one. Refreshing from
// it would read a stale entitlement and leave a paying user
// locked out with no sign anything went wrong.
showPurchasePendingRetry();
return;
}
// This callback arrives on the EDT and refresh() blocks on the
// network, so it needs a thread of its own.
new Thread(() -> {
cm.refresh();
// Back to the EDT to read the entitlement and touch the UI:
// Codename One UI calls are only legal there.
CN.callSerially(() -> {
// Same rule as the entitlement sample earlier in the chapter:
// the store-direct check is for when the cloud had no answer,
// not an override of one it gave.
boolean pro = cm.isEntitled("pro");
if (!pro && (!cm.isCloudEnabled() || cm.isDegraded())) {
pro = Purchase.getInAppPurchase().isSubscribed("pro_monthly");
}
if (pro) {
// unlock the paid features here
}
});
}).start();
});
isEntitled returns the last cloud-validated answer when one is available, and otherwise falls back to the platform’s own receipt (treating the entitlement id as a subscription SKU) so a paying user is never locked out while the network is down.
How the app finds the service
Commerce reuses the standard build_key that every Codename One cloud build already carries, the same token the analytics provider sends — so there is nothing to wire into the build. CommerceManager.isCloudEnabled() is true on any cloud build and false in the simulator or a local build (where every call defers to Purchase). Two optional build hints tune it:
| Build hint | Default | Description |
|---|---|---|
|
| Set to |
| (Codename One cloud) | Base URL of the commerce service. Override only if you self-host. |
Server notifications
For the entitlement state to stay correct after the initial purchase — renewals, cancellations, refunds, billing retries, grace periods, holds — point the stores' server notifications at the service:
Apple — in App Store Connect, set the App Store Server Notifications V2 URL to
https://cloud.codenameone.com/api/v2/commerce/notifications/apple.Google — in the Play Console, set Real-time developer notifications and route the Pub/Sub topic to
https://cloud.codenameone.com/api/v2/commerce/notifications/google.
Notifications are idempotent (deduplicated on Apple’s notificationUUID / Google’s Pub/Sub messageId), so a redelivery never double-counts.
Server-side validation (connect your store credentials)
By default the service validates the store-signed receipt the device presents — enough to gate features, and it works with no setup. For server-authoritative validation — where the service calls Apple’s and Google’s own server APIs to confirm the real subscription state — provide your store credentials in the console Secrets page:
Apple — an App Store Connect API key, as three secrets:
commerce.apple.keyId,commerce.apple.issuerId, andcommerce.apple.privateKey(the contents of the.p8file).Google — a service account with Android Publisher access, as
commerce.google.serviceAccountJson(the full JSON key).
Secrets are stored AES-256-GCM encrypted in the BuildCloud vault; they’re decrypted only server-side at call time and are never shown in the console again. Once set:
Entitlements are confirmed against Apple’s
Get All Subscription Statusesand Google’ssubscriptionsv2.get, so a grant is server-verified rather than device-trusted, andSubscription renewals signalled by store notifications are recorded at their real amount (the renewal price lives only in the store’s server response).
Without credentials the service stays in device-trusted mode — everything still works, you just don’t get the server cross-check or accurate renewal revenue.
Lifecycle webhooks to your backend
Commerce can forward every normalized state change to your own backend. Configure a URL and a signing secret on the Commerce page of the console. Each event is HMAC-SHA256 signed (header CN1-Commerce-Signature) and carries:
A per-customer monotonic
sequence— so your consumer can reject stale events and never has to reorder defensively,The grant’s
state_version— reconcile against this,An always-present
effective_expiration_at, andThe event
idfor idempotent dedup.
Delivery is at-least-once, in sequence order per customer, with retries that back off exponentially. Event types mirror the familiar set: INITIAL_PURCHASE, RENEWAL, CANCELLATION, UNCANCELLATION, EXPIRATION, BILLING_ISSUE, PRODUCT_CHANGE, SUBSCRIPTION_PAUSED, REFUND_REVERSED, TRANSFER.
The console
The Commerce section in the console at https://cloud.codenameone.com/console/index.html is account-level by default, with an app filter to narrow to one app:
Overview — the subscription dashboard: headline KPIs (MRR, ARR, ARPU, active subscriptions, active trials, churn, trial conversion, customers) and trend charts for MRR, active subscriptions and revenue.
Insights — MRR movement (new vs churned), MRR forecast, realized-LTV curve, active subscriptions by store and by product, cohort-retention, and period-over-period MRR.
Customers — each app user and their active entitlements.
Transactions — the recent validated-event ledger.
Webhook — your delivery endpoint and signing secret.
Analytics
Commerce normalizes every active subscription to a monthly figure (MRR) and reconstructs the trends from the subscription history, so the dashboard matches what dedicated subscription-analytics tools provide:
MRR / ARR / ARPU — normalized recurring revenue, annualized, and per active subscription.
Churn and trial conversion rates, refund rate.
Cohort retention — monthly acquisition cohorts and the percentage still active each subsequent month.
Realized LTV — average cumulative revenue per paying customer by months since acquisition.
Forecast — projected MRR for the coming months from the current run-rate and recent growth.
Breakdowns by store and by product, and period-over-period comparison.
Every chart pairs a graph with its underlying data table (one tab-flip away) for export-friendly inspection. All money is shown in your reporting currency; transactions in other currencies are normalized to USD via daily exchange rates.
Privacy
Commerce stores entitlement state and transaction metadata (store, product id, event type, amount, validation state) keyed by your app-user id. It never stores card numbers or payment credentials — those stay with Apple, Google, or your processor. Family shared purchases grant access but are excluded from revenue. The minimum lookup state needed to resolve the next notification is kept; grants are retained as the durable record, while the high-churn ledger and delivered webhook rows are purged on a rolling window.
Setup checklist
Map your store products to entitlements in the console.
In the app, call
CommerceManager.getInstance().subscribe(…)/.purchase(…), then.refresh()off the EDT, and gate features with.isEntitled(…).Point Apple App Store Server Notifications V2 and Google RTDN at the service notification URLs.
(Optional) Set a webhook URL + secret on the Commerce console page to receive lifecycle events on your backend.