Fetch Paywall
POST https://api.botsi.com/v2/paywallOnce a Profile exists, call this at any time to retrieve the Paywall variant the AI Pricing Model selected for that user. The response identifies which of your pre-configured paywalls to display.
Timing#
Do not call this synchronously at render time
The response typically takes around 40 ms, but can take up to 2 seconds depending on prediction speed. Blocking your Paywall's render on it puts that delay in front of the user.
Trigger the request early (immediately after the Profile is created is a good moment) and have the response cached before the user reaches the Paywall.
Request body#
curl -X POST "https://api.botsi.com/v2/paywall" \
-H "Authorization: $BOTSI_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileId": "0072102a-c00c-4ea5-9271-1b6e975f2d63",
"placementId": "ai-placement-id"
}'Response#
{
"ok": true,
"data": {
"id": 42,
"externalId": "paywall_premium_v2",
"name": "Premium Paywall",
"isExperiment": true,
"aiPricingModelId": 32,
"paywallSessionId": "v1.eyJhIjo0MDIxLCJ3Ijo5MDUsInAiOiJvbmJvYXJkaW5nIiwiaSI6MTc4NTMxMjAwMH0.QmzR1w",
"paywallProducts": [
{
"paywallProductId": 907,
"name": "Premium Monthly",
"period": "monthly",
"appStore": {
"productId": "com.example.premium.monthly",
"offerId": "intro-offer-7day",
"promotionalOfferId": null,
"offerType": "introductory"
},
"playStore": {
"productId": "premium_monthly",
"basePlanId": "monthly-base",
"offerId": null
},
"stripe": {
"productId": "prod_QxA1b2C3",
"priceId": "price_1OaBcMonthly",
"trialPeriodDays": 7
},
"web2wave": {
"productId": "w2w_premium_monthly"
},
"custom": {
"productId": "custom_premium_monthly"
}
},
{
"paywallProductId": 908,
"name": "Premium Annual",
"period": "annual",
"appStore": {
"productId": "com.example.premium.annual",
"offerId": null,
"promotionalOfferId": null,
"offerType": null
},
"playStore": {
"productId": "premium_annual",
"basePlanId": "annual-base",
"offerId": null
},
"stripe": {
"productId": "prod_QxA1b2C4",
"priceId": "price_1OaBcAnnual",
"trialPeriodDays": 0
},
"web2wave": null,
"custom": null
}
]
}
}Product shape#
Each entry in data.paywallProducts describes one product attached to the Paywall, followed by one nested block per store.
A block is null when the product has no identifier for that store, and productId inside a block is never null. So if (!product.playStore) is a well-defined "not sold on Google Play" check. Under the previous shape, a product with no Google ID and a genuine mapping bug both arrived as productId: null, with nothing to tell them apart.
For example, Premium Annual above is sold on the App Store, Google Play, and Stripe, so its web2wave and custom blocks come back null. No offer is attached to it either, which is why its three appStore offer fields are null while the block itself remains.
paywallProductId is the ID of the row attaching the product to the Paywall, not the product's own ID. The same product can be attached to one Paywall more than once under different offers, so this is what keeps those entries distinguishable. Key your rendering on it.
appStore#
The three offer fields come from the offer attached to this entry on this Paywall, not from the product itself, which is why the same App Store product can appear twice under two different offers.
Botsi does not model a store-level discount or coupon concept, so there is no discountId. Use offerId together with offerType.
playStore#
Botsi stores "no base plan" and "no offer" as empty strings and returns both as null. A Google Play product with an empty offerId still gets a playStore block, because it is a real product that happens to carry no offer. Only a missing Google Play product ID nulls the block.
stripe#
web2wave and custom#
Both carry a single productId, the product's ID in that store. Earlier versions of this endpoint answered a Web2Wave or custom-store request with the Stripe product ID instead. If your integration works around that, drop the workaround and read web2wave.productId or custom.productId.