Skip to content

Search the documentation by title, section, or page text.

Fetch Paywall

POST https://api.botsi.com/v2/paywall

Once 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#

ParameterTypeRequiredDescription
profileIdstringRequiredBotsi profile ID from Create a Profile
placementIdstringRequiredPlacement ID configured in the dashboard
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
      }
    ]
  }
}
FieldWhat it is
data.idThe internal Botsi Paywall ID. Pass it back as paywallId when validating a purchase
data.externalIdThe external paywall identifier, or null when you have not assigned one
data.nameThe Paywall name as configured in Botsi
data.isExperimentWhether this response came from an A/B test. Pass it back when validating a purchase
data.aiPricingModelIdThe model that served the paywall response, or null when the answer did not resolve through one
data.paywallProductsOne entry per product on the Paywall, each carrying every store identity. See below
data.paywallSessionIdA session token for this paywall response. Used when emitting a paywall_shown event

Product shape#

Each entry in data.paywallProducts describes one product attached to the Paywall, followed by one nested block per store.

FieldWhat it is
paywallProductIdNumber identifying this entry within paywallProducts
nameProduct name as configured in Botsi
periodBilling period: weekly, monthly, 2_months, 3_months, 6_months, annual, lifetime, consumable, or non_subscription
appStoreApp Store identity, or null
playStoreGoogle Play identity, or null
stripeStripe identity, or null
web2waveWeb2Wave identity, or null
customCustom store identity, or null

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#

FieldWhat it is
productIdApp Store product ID
offerIdID of the offer attached to this entry, or null when no offer is attached
promotionalOfferIdThe same value as offerId when the attached offer is promotional, null otherwise. Redundant with offerType
offerTypeCategory of the attached offer: introductory, promotional, code, win_back, or unknown. null when no offer is attached

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#

FieldWhat it is
productIdGoogle Play product ID
basePlanIdBase plan ID, or null when the product is the base plan itself
offerIdOffer ID forming part of this variant's identity, or null when the variant is the base plan itself

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#

FieldWhat it is
productIdStripe product ID
priceIdPrice ID for this variant, or null
trialPeriodDaysFree-trial length in days, where 0 means no trial. null when unset

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.

Errors#

CodeMessageCause
404No paywall found for the provided placement ID "main-onboarding" in app ID 4021. The audience does not have a paywall, AB test, or AI pricing model configured.The Placement exists but the resolved audience has nothing configured to serve
500Server Error. with code internal_server_errorServe your baseline variant rather than retrying in front of the user