Products & Offerings

Configure what you sell and how you display it

Overview

CroissantPay uses three concepts to organize your in-app purchases:

Products

Products are the actual items you sell—subscriptions, one-time purchases, or consumables. Each product maps to a product in the App Store and/or Google Play.

Offerings

Offerings are curated collections of products to display on your paywall. You can have multiple offerings for different scenarios (default, sale, experiment).

Packages

Packages are products within an offering, with a specific type (monthly, annual, lifetime, etc.) that helps you identify them in your UI.

Hierarchy

Offerings
├── "default" (current offering)
│   ├── Package: MONTHLY
│   │   └── Product: Premium Monthly ($9.99/mo)
│   ├── Package: ANNUAL
│   │   └── Product: Premium Annual ($79.99/yr)
│   └── Package: LIFETIME
│       └── Product: Premium Lifetime ($199.99)
│
└── "sale_offering" 
    ├── Package: MONTHLY
    │   └── Product: Premium Monthly ($7.99/mo)  [sale price]
    └── Package: ANNUAL
        └── Product: Premium Annual ($59.99/yr) [sale price]

Product Types

Auto-Renewable Subscriptions

Automatically renew until cancelled. Best for ongoing access to features or content.

  • • Weekly, monthly, quarterly, annual, etc.
  • • Support free trials and introductory pricing
  • • Grace periods for failed payments

Non-Consumable (Lifetime)

One-time purchase that doesn't expire. Best for permanent unlocks.

  • • Permanent access to features
  • • Restored on reinstall/new devices
  • • No renewal or expiration

Consumables

Can be purchased multiple times and used up. Best for virtual currency or credits.

  • • Coins, gems, credits, etc.
  • • Not restored on reinstall
  • • Track balance in your backend

Creating Products

Step 1: Create in App Stores

First, create your products in App Store Connect and Google Play Console. See our iOS Setup and Android Setup guides.

Step 2: Add to CroissantPay

  1. Go to Products in the dashboard
  2. Click "Create Product"
  3. Enter product details:
    • • Identifier (e.g., "premium_monthly")
    • • Display name
    • • Product type
    • • App Store product ID
    • • Google Play product ID
  4. Link entitlements this product grants
Create via API
POST /api/v1/products
{
  "identifier": "premium_monthly",
  "displayName": "Premium Monthly",
  "description": "Full premium access, billed monthly",
  "type": "subscription",
  "storeProductIdApple": "com.myapp.premium_monthly",
  "storeProductIdGoogle": "premium_monthly",
  "entitlementIds": ["premium"]
}

Creating Offerings

Offerings let you control which products appear on your paywall without app updates.

Default Offering

Your app always has a "current" offering that's returned by default. This is typically your main paywall configuration.

Multiple Offerings

Create additional offerings for:

  • Sales: Special pricing during promotions
  • A/B Tests: Different product combinations to test
  • User Segments: Different offerings for different users
  • Win-back: Special offers for churned users
Create offering via API
POST /api/v1/offerings
{
  "identifier": "default",
  "displayName": "Default Offering",
  "isCurrent": true,
  "packages": [
    {
      "productId": "prod_monthly",
      "packageType": "MONTHLY"
    },
    {
      "productId": "prod_annual", 
      "packageType": "ANNUAL"
    }
  ]
}

Package Types

Package types help you identify products in your UI without hardcoding identifiers:

TypeDescription
WEEKLYWeekly subscription
MONTHLYMonthly subscription
TWO_MONTHEvery 2 months
THREE_MONTHQuarterly subscription
SIX_MONTHSemi-annual subscription
ANNUALYearly subscription
LIFETIMEOne-time lifetime purchase
CUSTOMCustom/other package type

Using Offerings in Your App

Paywall.tsx
import { usePurchases } from '@croissantpay/react-native';

function Paywall() {
  const { offerings, purchaseProduct, isLoading } = usePurchases();
  
  if (isLoading) {
    return <LoadingSpinner />;
  }
  
  const currentOffering = offerings?.current;
  
  if (!currentOffering) {
    return <Text>No products available</Text>;
  }
  
  return (
    <View>
      <Text>Choose your plan</Text>
      
      {currentOffering.packages.map(pkg => (
        <TouchableOpacity
          key={pkg.id}
          onPress={() => purchaseProduct(pkg.product)}
        >
          <View style={styles.planCard}>
            <Text style={styles.planName}>
              {pkg.product.title}
            </Text>
            <Text style={styles.price}>
              {pkg.product.priceString}
              {pkg.packageType === 'ANNUAL' && '/year'}
              {pkg.packageType === 'MONTHLY' && '/month'}
            </Text>
            {pkg.packageType === 'ANNUAL' && (
              <Text style={styles.savings}>
                Save 33%
              </Text>
            )}
          </View>
        </TouchableOpacity>
      ))}
    </View>
  );
}

Accessing Specific Packages

Package shortcuts
const currentOffering = offerings?.current;

// Access packages by type
const monthlyPkg = currentOffering?.monthly;
const annualPkg = currentOffering?.annual;
const lifetimePkg = currentOffering?.lifetime;

// Or filter from packages array
const monthly = currentOffering?.packages.find(
  pkg => pkg.packageType === 'MONTHLY'
);

Best Practices

Match product IDs between stores

Use similar identifiers for the same product across platforms to make management easier.

Include annual options

Annual subscriptions typically have higher LTV and lower churn. Always offer an annual option with a discount.

Use offerings for remote control

Instead of hardcoding products, use offerings to control your paywall remotely. This lets you run sales or tests without app updates.

Handle missing products gracefully

Always check if offerings and products exist before rendering. Products might not be available in all regions.

Next Steps