Follow one purchase.
Alice buys Premium in your app. Follow her purchase from the store’s payment screen to the moment she can open paid content.
At each step, compare two implementations: openiap-commerce-protocol-example makes the flow visible with a local dashboard and fictional purchases; IAPKit connects the same contract to store services. Follow the behavior first, then open either implementation’s code when you need it.
Apple and Google subscription changes arrive through store notifications. Amazon and Horizon access is rechecked with the store when your backend requests it. Choose your store below to follow the matching path.
Alice chooses Premium.
Alice chooses Premium on your paywall. Your app opens Apple’s purchase screen through its OpenIAP client library. A successful purchase gives your backend the evidence it needs to ask the store for verification.
The app sends the evidence to your backend using Alice’s signed-in session. A pending, canceled, or failed purchase creates no new access.
Where the store result goes
openiap-commerce-protocol-example
The purchase mapper converts Apple, Google, Amazon, and Horizon client results into verification input on your backend. Its demo checks the mapping without making a store purchase.
Read code: toVerifyPurchaseInputIAPKit receives that store evidence through verifyPurchase. The paywall, store purchase screen, and app login stay with your app; IAPKit does not replace them.
Read code: verifyPurchaseHow to check this behavior
Example. runBridgeDemo checks all four store mappings and requires a separate store identity for Amazon and Horizon. Run npm run demo:bridge. Inspect reference
IAPKit. Route tests cover evidence parsing and errors with mocked store calls; they do not perform a mobile purchase. Inspect reference
See the same app switch between the example and IAPKit →
Ready to build? Choose your services and build it →
Implementation reference: requests and receiver setup
Use these details when wiring a backend or checking your AI’s code.
Receive subscription changes#
Connect an existing backend with the ready receiver. It verifies the signature, validates the event, and saves it once in SQLite. You do not need to build a commerce provider or a store adapter.
If your service only consumes events, start here. The purchase and access operations below belong to the app backend and its chosen provider; your service can keep its existing backend.
The example repository includes the receiver and executable checks. AI can reuse it to demonstrate signed delivery, retries, tamper rejection, and preserved inbox entries after reopening storage.
Run the receiver example locally
Clone the repository, install Bun for its runtime, then install dependencies and run the demo. This fixture check requires no credentials or external services.
Use your favorite package manager.
npm installnpm run demo:consumerConnect the receiver to your provider
Set COMMERCE_WEBHOOK_SECRET to your provider’s signing secret, then run npm run consumer. The ready endpoint is http://127.0.0.1:5182/webhooks/commerce. Put it behind your HTTPS reverse proxy, forwarding the exact body bytes to that local address, and register the HTTPS URL with your provider. The receiver accepts the public Host header forwarded by the proxy; its signature check authenticates the sender.
Use one emitter/project and signing key per receiver database. consumer.sqlite is the durable inbox; set COMMERCE_INBOX_PATH for your persistent storage path. To embed the same Fetch-compatible handler in your server, use createReceiver from webhooks.mjs.
The inbox preserves the signed event for your existing processing pipeline. Transaction and price fields are optional; a missing price is unknown. Each lifecycle event is not necessarily a new charge. Keep financial calculations in your business logic.
1. Install the contract#
Use your favorite package manager.
npm install openiap-commerce-protocolThe package gives your backend the contract, schemas, and conformance tools. Obtain a compatible provider endpoint and credentials separately.
To explore first, open the recorded purchase flow, or run the reference dashboard. To implement your own provider, give your AI the build brief.
2. Connect to a provider#
Obtain the base URL, supported store configuration, and Authorization header values from your provider. A provider issues its own credentials; OpenIAP has no registration service. Keep the server credential in your backend. These shell examples assume curl and jq.
export COMMERCE_BASE_URL='https://your-provider.example'
# Set these through your local secret manager or environment.
# COMMERCE_VERIFY_AUTH: complete Authorization header value for verification
# COMMERCE_SERVER_AUTH: complete Authorization header value for the server role
curl --fail-with-body "$COMMERCE_BASE_URL/commerce/v1/capabilities"Inspect profiles, bindings, and the selected store’s implementation support. This flow needs verification, accountLifecycle, entitlements, and rest at compatible major versions. An events-only descriptor does not establish operation support. See capabilities for the full descriptor.
3. Verify, bind, and read access#
Save the following shape as evidence.json. Replace its illustrative JWS with a real StoreKit transaction JWS from the app. For Google, use { "store": "google", "google": { "purchaseToken": "…" } }. Your provider must be configured for that app and store environment.
{
"store": "apple",
"apple": {
"jws": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMDEyMzQ1Njc4OSIsInByb2R1Y3RJZCI6InByZW1pdW0ubW9udGhseSJ9.MEUCIQDkQmFrb2Zha2VzaWduYXR1cmVmb3JleGFtcGxlAiBkb2N1bWVudG9ubHlmYWtlc2lnbmF0dXJldmFsdWU"
}
}curl --fail-with-body "$COMMERCE_BASE_URL/commerce/v1/purchases/verify" \
-H "Authorization: $COMMERCE_VERIFY_AUTH" \
-H 'Content-Type: application/json' \
--data-binary @evidence.jsonA successful response carries isValid. A store rejection is still a successful operation with isValid: false. VERIFICATION_FAILED means no verdict was obtained; retry according to your failure policy. Neither a verdict nor its advisory state identifies the app user or answers their current access.
Your backend authenticates the user and checks that it may associate this purchase with them. Select COMMERCE_USER_ID from that session and ownership policy. A user ID submitted by the app and possession of a receipt are not sufficient authorization.
# Run on your backend; COMMERCE_USER_ID is selected by your authenticated handler.
jq --arg userId "$COMMERCE_USER_ID" '. + {userId: $userId}' \
evidence.json > binding.json
curl --fail-with-body "$COMMERCE_BASE_URL/commerce/v1/purchases/bind" \
-H "Authorization: $COMMERCE_SERVER_AUTH" \
-H 'Content-Type: application/json' \
--data-binary @binding.jsonContinue after bound: true. A bound: falseresult intentionally does not distinguish unknown evidence from a purchase belonging to someone else. Do not transfer the binding or grant access on that result; use your provider’s recovery process. The association can remain bound even when the subscription is expired.
curl --fail-with-body --get "$COMMERCE_BASE_URL/commerce/v1/entitlements" \
-H "Authorization: $COMMERCE_SERVER_AUTH" \
--data-urlencode "userId=$COMMERCE_USER_ID"Gate product-specific features on membership in productIds. An empty list is a complete answer with no current access. An operation error is not an empty list. The subscriptions array contains the tokenless records supporting that answer. For a simple “any active subscription?” check, use subscriptionStatus.active.
4. Handle changes after purchase#
If the provider declares events, register an HTTPS backend destination using its management interface and exchange a webhook secret. Destination registration is provider-specific. Follow the backend architecture to authenticate and persist each delivery before acknowledging it.
Use events to trigger an authoritative entitlement refresh when you cannot safely correlate purchases, especially across multiple subscriptions or product changes. A cancellation disables renewal; it does not automatically remove the remaining paid access. Without events, use bounded server reads at the points your application needs a current answer.
When an authenticated user deletes their account, call eraseUser from your backend. The provider erases its own identity records; arrange erasure separately for copies already delivered to your backend and connected services.
Continue with the operation reference, GraphQL binding, or provider implementation guide.