Payment Implementation
Learn how to implement subscriptions and one-time purchases in your a0 app using thea0-purchases library.
The a0 payment system uses the
a0-purchases
library. All apps built with a0 come with the necessary configuration and providers built in.Platform support and testing (important)
- Web: Stripe (test and production). You can test purchases in the browser.
- iOS: Apple App Store. You sync products to App Store Connect and test purchases via TestFlight.
- Android: not supported yet.
Prerequisites
Before implementing payments in your code, ensure you have:Completed Payment Setup
Defined your features and plans (either in the dashboard UI or by editing
.a0/monetization.yaml via the Monetization tab), and created at least one offering. See the Payment Setup guide.Synced (or linked) to providers
Synced your plans to Stripe and/or Apple App Store Connect.If you’re migrating an existing iOS subscription, link your existing StoreKit product IDs in
.a0/monetization.yaml (v2) so your paywall can resolve products immediately (without waiting on a successful first sync).If you’re using the a0 coding agent: it can edit
.a0/monetization.yaml directly (same as build.yaml and general.yaml), then sync to Apple/Stripe. It should not tell you to do it yourself.One-time purchases (lifetime unlocks + consumables)
The same paywall +purchase(packageId) flow supports:
- non-consumable (buy once + restore): great for “lifetime unlock”
- consumable (buy many times): great for “credits”, “tokens”, etc
.a0/monetization.yaml (v2) with pricingTiers[].productType:
type: lifetime+ noproductTypedefaults tonon_consumable(backwards compatible).- Consumables are not restorable on iOS. If you need purchase history, use
customerInfo.nonSubscriptionTransactions. - For consumables (credits/tokens), treat the purchase as a transaction and fulfill/grant the balance server-side. Do not use local storage as your source of truth.
Understanding Offerings Structure
Before building your paywall, it’s important to understand how offerings are structured:offerings.allis an object with offering IDs as keys (not an array) -offerings.currentcontains the offering marked as current in your a0 dashboard - If no offering is set as current,offerings.currentwill benull- Useofferings.currentwhen possible, or access specific offerings via their ID fromofferings.all
packageId vs productId (don’t mix these up)
- packageId =
pkg.identifier(what you pass topurchase(packageId)) - productId =
pkg.product.identifier(the store purchase ID)
- iOS: StoreKit product identifier (SKU)
- Stripe: Stripe price ID
Building a Paywall
Here’s a simple paywall component using theuseA0Purchases hook:
Checking Premium Status
The simplest way to gate content is using theisPremium property:
Checking Specific Entitlements
isPremium returns true if the user has ANY active entitlement. If you have multiple
features/entitlements and need to check specific ones, use getCustomerInfo().Advanced Implementation
Custom Paywall with Package Details
Handling Purchase States
Testing Purchases
Test Credentials
Stripe Test Cards:- Success:
4242 4242 4242 4242 - Decline:
4000 0000 0000 0002 - More test cards in Stripe docs
- Sync products to App Store Connect (production), then test purchases via TestFlight using sandbox tester accounts
- Subscriptions auto-renew every few minutes for testing
- Payments do not work in the a0 app or Expo Go (use TestFlight / native builds)
Testing Flow
Troubleshooting
offerings.all[0] returns undefined
offerings.all[0] returns undefined
Cause:
offerings.all is an object (with offering IDs as keys), not an arraySolution:
Use one of these patterns instead:Paywall shows 'No products available'
Paywall shows 'No products available'
Common causes:
- No offerings created or no current offering set
- The offering has packages, but the packages are not mapped to provider products for the current platform/environment
- Go to a0 dashboard → Payments → Offerings
- Create an offering with your subscription plans
- Set it as the current offering
- iOS (existing products): use
.a0/monetization.yamlv2 linkingbundles[].ios.subscriptionGroupId: numeric App Store Connect subscription group id (digits only)bundles[].pricingTiers[].ios.productId: existing StoreKit product identifier (SKU)
- Stripe (existing prices): use
.a0/monetization.yamlv2 linkingbundles[].pricingTiers[].stripe.priceId: Stripe live price id (production)bundles[].pricingTiers[].stripe.testPriceId: Stripe test price id (development)
- iOS / Stripe (new products): run a provider sync after editing monetization so mappings are created
Purchase fails immediately
Purchase fails immediately
Common causes:
- Not synced to payment provider
- Testing iOS purchases in the a0 app / Expo Go (must use TestFlight / native build)
- Testing with production credentials
- Invalid product identifiers
isPremium not updating after purchase
isPremium not updating after purchase
Entitlements showing as undefined
Entitlements showing as undefined
Cause: Features not properly configured in a0 dashboardSolution:
- Verify feature names match exactly (case-sensitive)
- Ensure plans include the features you’re checking
- Sync plans to payment providers after changes