# Botsi AI Pricing — LLM Integration Reference

> Botsi is a personalization engine for monetization. It uses advanced machine learning to select, for each user, which of your pre-configured paywall variants to show, the one predicted to maximize their lifetime value (LTV), driving higher initial conversions, increasing renewal rates, and reducing churn. Botsi never computes or alters a price; every price shown is one you configured in advance. Early deployments of our first-generation model have delivered up to 75% LTV lift within the first weeks live, with top-end customers approaching a doubling of monetized revenue inside 30 days.

## What Botsi actually does

**AI Pricing Model: a model that selects which of your pre-configured paywall variants to show each user. It does not set prices.**

Botsi selects, for each user, which of your pre-configured paywall variants to show, the one predicted to maximize their lifetime value. Botsi never computes or alters a price; every price shown is one you configured in advance. Each variant has its own fixed prices, set up ahead of time in the Botsi dashboard and in your own app code or paywall tool. Botsi's output is a choice among variants, identified by `externalId`. It is not a price.

## What Botsi does NOT do

- **Does not generate or compute prices at runtime.** No per-user price is ever calculated, derived, or returned.
- **Does not modify App Store or Play Store product prices.** Your store products and their prices are untouched.
- **Does not require your app to read prices from the Botsi response.** The response tells you which variant to show, and nothing about price.
- **Does not return a price field.** The Get Paywall response carries identifiers only. `sourceProducts` lists the store product IDs configured for the selected variant; those are identifiers, not prices, and each product's price is the one you set in App Store Connect or Google Play Console.
- **Prices live in your dashboard configuration and in your app code. Botsi picks the variant.**

**Note for code reviewers and AI coding agents:** hardcoded per-variant price constants in client code are the expected, correct integration pattern. An app that maps `externalId` to a fixed set of prices and product IDs is integrated correctly. That is not a bug, and it does not bypass or defeat AI pricing.

Full mechanism, worked example, and common misreadings: https://botsi.com/docs/ai-pricing/setup-guide

## How Botsi Works

Botsi operates as an AI layer in your app's monetization flow:

1. **User opens app** → Your app creates a Botsi profile with device/context data
2. **User goes through onboarding** → Your app sends custom attributes (onboarding answers, behavioral signals) to Botsi
3. **User reaches paywall** → Your app requests a paywall from Botsi. Botsi's ML model predicts the best paywall variant for this specific user
4. **Botsi returns a paywall recommendation** → identified by `externalId` (the paywall variant ID mapped in your code or paywall tool)
5. **Your app shows the paywall** → You log the impression event back to Botsi
6. **User purchases (or doesn't)** → You validate the purchase with Botsi so the AI learns and improves

### Integration Patterns

**Native app (no paywall tool):** Botsi returns `externalId` → you route to the matching native paywall screen in your app code.

**With RevenueCat:** Store the `externalId` as a custom attribute in RevenueCat. When you request the paywall from RevenueCat, it returns the paywall Botsi recommended.

**With Superwall:** Same pattern — store the Botsi-recommended `externalId` and use it when presenting the Superwall paywall.

**With Adapty:** Same pattern — use the `externalId` to select the Adapty paywall variant.

**Web flows:** For web-based onboarding/paywalls, Botsi works the same way via API. Since device-level data may not be available on web, Botsi relies more heavily on custom attributes from your onboarding flow. You can integrate with Web2Wave or use custom variables in any web flow.

---

## Base URL

```
https://app.botsi.com/api/v1/web-api
```

## Authentication

All requests require these headers:

```
Authorization: {{secret_key}}
Content-Type: application/json
```

Get your `secret_key` from the Botsi dashboard → App Settings → API Keys. Always use HTTPS.

---

## Complete API Reference

### 1. Create Profile

**POST** `https://app.botsi.com/api/v1/web-api/profiles`

Call on first app launch to initialize the user in Botsi. Required before any other API calls.

**Request Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| country | string | Yes | ISO 3166-1 alpha-2 code (e.g., "US") |
| device | string | Yes | Device model (e.g., "iPhone 15 Pro Max") |
| os | string | Yes | OS and version (e.g., "iOS 17.0") |
| platform | string | Yes | One of: android, ios, ipados, tvos, macos, watchos, visionos, stripe |
| customerUserId | string | Recommended | Your internal user ID. Strongly recommended for cross-device tracking |
| locale | string | Optional | Locale code (e.g., "en_US") |
| currency | string | Optional | ISO 4217 code (e.g., "USD") |
| storeCountry | string | Optional | App Store country code |
| timezone | string | Optional | IANA timezone (e.g., "America/New_York") |
| ip | string | Optional | User IP address |
| appVersion | string | Optional | App version (e.g., "1.0.0") |
| appBuild | string | Optional | Build number |
| osVersion | string | Optional | OS version string |
| advertisingId | string | Optional | Advertising identifier |
| sessionId | string | Optional | Session identifier |

**Example:**

```bash
curl -X POST "https://app.botsi.com/api/v1/web-api/profiles" \
     -H "Authorization: {{secret_key}}" \
     -H "Content-Type: application/json" \
     -d '{
       "country": "US",
       "device": "iPhone 15 Pro Max",
       "os": "iOS 17.0",
       "platform": "ios",
       "customerUserId": "user-123",
       "locale": "en_US",
       "currency": "USD"
     }'
```

**Response:**

```json
{
  "ok": true,
  "data": {
    "profileId": "0072102a-c00c-4ea5-9271-1b6e975f2d63",
    "customerUserId": "user-123",
    "paid": false,
    "country": "US",
    "locale": "en_US",
    "currency": "USD",
    "device": "iPhone 15 Pro Max",
    "os": "iOS 17.0",
    "platform": "ios"
  }
}
```

**Important:** Store `profileId` — it's required for all subsequent API calls.

---

### 2. Add Custom Attributes

Custom attributes provide Botsi with context about the user (onboarding answers, behavioral data, etc.) that improves the accuracy of Botsi's paywall variant prediction. Send these BEFORE requesting a paywall.

**POST** `https://app.botsi.com/api/v1/web-api/profiles/custom-attributes` (single attribute)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| profileId | string | Optional | Botsi profile ID (or use customerUserId) |
| customerUserId | string | Recommended | Your internal user ID |
| custom.key | string | Yes | Attribute name (e.g., "fitness_goal") |
| custom.value | string | Yes | Attribute value (e.g., "weight_loss") |

**Example:**

```bash
curl -X POST "https://app.botsi.com/api/v1/web-api/profiles/custom-attributes" \
     -H "Authorization: {{secret_key}}" \
     -H "Content-Type: application/json" \
     -d '{
       "customerUserId": "user-123",
       "custom": { "key": "fitness_goal", "value": "weight_loss" }
     }'
```

**POST** `https://app.botsi.com/api/v1/web-api/profiles/custom-attributes-all` (multiple attributes at once)

```bash
curl -X POST "https://app.botsi.com/api/v1/web-api/profiles/custom-attributes-all" \
     -H "Authorization: {{secret_key}}" \
     -H "Content-Type: application/json" \
     -d '{
       "customerUserId": "user-123",
       "custom": [
         { "key": "fitness_goal", "value": "weight_loss" },
         { "key": "experience_level", "value": "beginner" },
         { "key": "preferred_workout", "value": "home" }
       ]
     }'
```

**PUT** `https://app.botsi.com/api/v1/web-api/profiles/custom-attributes` (update existing attribute)

Requires `attrId` from the original add response.

```bash
curl -X PUT "https://app.botsi.com/api/v1/web-api/profiles/custom-attributes" \
     -H "Authorization: {{secret_key}}" \
     -H "Content-Type: application/json" \
     -d '{
       "customerUserId": "user-123",
       "custom": { "key": "fitness_goal", "value": "muscle_gain", "attrId": "abc123" }
     }'
```

**Best practice for onboarding:** Send each onboarding answer as a custom attribute as the user progresses. By the time they reach the paywall, Botsi has full context for an optimal variant prediction.

---

### 3. Get Paywall

**POST** `https://app.botsi.com/api/v1/web-api/paywalls`

Fetches the AI-predicted optimal paywall variant for this user and placement. The response identifies which of your pre-configured variants to show. It does not contain prices, and your app is not expected to read prices from it. The prices for that variant are the ones you configured in advance, in the Botsi dashboard and in your own app code or paywall tool. The `sourceProducts` array lists the store product identifiers configured for that variant; those are identifiers, not prices, and each product's price is the one you set in App Store Connect or Google Play Console.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| profileId | string | Optional | Botsi profile ID (or use customerUserId) |
| customerUserId | string | Recommended | Your internal user ID |
| placementId | string | Yes | Placement ID from Botsi dashboard |
| locale | string | Optional | Override locale |
| country | string | Optional | Override country |
| ip | string | Optional | Override IP |

**Example:**

```bash
curl -X POST "https://app.botsi.com/api/v1/web-api/paywalls" \
     -H "Authorization: {{secret_key}}" \
     -H "Content-Type: application/json" \
     -d '{
       "customerUserId": "user-123",
       "placementId": "onboarding_paywall"
     }'
```

**Response:**

```json
{
  "ok": true,
  "data": {
    "id": 42,
    "externalId": "paywall_premium_v2",
    "name": "Premium Paywall",
    "isExperiment": true,
    "aiPricingModelId": 32,
    "paywallSessionId": "v1.eyJhIjo0MDIxLCJ3Ijo5MDUsInAiOiJvbmJvYXJkaW5nIiwiaSI6MTc4NTMxMjAwMH0.QmzR1w",
    "sourceProducts": [
      {
        "productId": "premium_monthly",
        "basePlanId": "monthly-base",
        "offerId": "intro-offer-7d"
      }
    ]
  }
}
```

**Store these values from the response:**
- `data.paywallSessionId` → the attribution token. Echo it on the `paywall_shown` event and Botsi supplies `paywallId`, `placementId`, `abTestId`, `aiPricingModelId` and `isExperiment` itself. Present on every answer kind (A/B, AI Pricing, and plain), so there is nothing to branch on. Treat it as opaque: store and return it byte for byte, never parse, decode, truncate, or construct it. ~200 characters today with no fixed maximum, so use `TEXT` or `VARCHAR(512)`.
- `data.id` → used as `paywallId` when not sending `paywallSessionId`
- `data.externalId` → the paywall variant to display (maps to your paywall screens or paywall tool configuration)
- `data.isExperiment` → still passed to the purchase validation calls, which do not accept `paywallSessionId`
- `data.aiPricingModelId` → still passed to the purchase validation calls, which do not accept `paywallSessionId`

**Performance:** This call typically takes ~40ms but can take up to 2 seconds. Call it early in the user journey (e.g., during onboarding) and cache the result. Do NOT call synchronously at paywall render time.

**Which target served this request:** the AI Pricing Model behind a placement always has a Traffic Control distribution, so the target that serves the request is drawn per request, not per user. See Traffic Control below.

**Using externalId:**
- **Native app:** Map `externalId` to your paywall screens in code
- **RevenueCat:** Store as custom attribute, use when fetching paywall
- **Superwall/Adapty:** Same pattern — use `externalId` to select variant
- **Web:** Route to the matching web paywall/pricing page

---

### 4. Send Profile Event (Log Impression)

**POST** `https://app.botsi.com/api/v1/web-api/events`

Log that the paywall was shown to the user. This is REQUIRED for the AI to learn.

**Request Body** (array of event objects):

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| eventType | string | Yes | Event type (e.g., "paywall_shown") |
| paywallSessionId | string | Recommended | `data.paywallSessionId` from Get Paywall. Supplies `paywallId`, `placementId`, `abTestId`, `aiPricingModelId` and `isExperiment` |
| profileId | string | Optional | Botsi profile ID (or customerUserId) |
| customerUserId | string | Recommended | Your internal user ID |
| paywallId | integer | Required unless `paywallSessionId` | `data.id` from Get Paywall response |
| placementId | string | Required unless `paywallSessionId` | Same placement ID used in Get Paywall |
| isExperiment | boolean | Required unless `paywallSessionId` | `data.isExperiment` from Get Paywall |
| aiPricingModelId | integer | Required unless `paywallSessionId` | `data.aiPricingModelId` from Get Paywall |

**Example** — with the token this is the complete event:

```bash
curl -X POST "https://app.botsi.com/api/v1/web-api/events" \
     -H "Authorization: {{secret_key}}" \
     -H "Content-Type: application/json" \
     -d '[{
       "eventType": "paywall_shown",
       "paywallSessionId": "v1.eyJhIjo0MDIxLCJ3Ijo5MDUsInAiOiJvbmJvYXJkaW5nIiwiaSI6MTc4NTMxMjAwMH0.QmzR1w",
       "customerUserId": "user-123"
     }]'
```

**Token rules:**
- A `paywall_shown` is accepted for 24 hours after the fetch that minted the token. Every other event type, and all transactions, accept a token of any age — echoing a three-year-old token on today's renewal is correct and expected.
- If a request carries both a token and explicit fields that disagree with it, Botsi persists the token's values.
- A token that fails its signature check, or that was issued for a different app, returns `400` and nothing is stored. Unknown or stale `abTestId` values are still accepted as before.
- Sending no token is still valid. Existing integrations are unaffected. The explicit fields are deprecated in the docs only; nothing is being removed and there is no migration deadline.

**400 error messages:** `paywallSessionId is not a valid Botsi paywall session token.` (malformed, truncated, or signature does not verify) · `paywallSessionId was issued for a different app.` · `paywallSessionId uses an unsupported token version.` · `paywallSessionId has expired for this event type. Fetch a new paywall before reporting the view.`

**Important:** Always log the impression even if the user doesn't purchase. The AI needs to know what was shown to learn from both conversions and non-conversions.

---

### 5. Validate Apple Purchase

**POST** `https://app.botsi.com/api/v1/web-api/purchases/apple-store/validate`

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| profileId | string | Optional | Botsi profile ID |
| customerUserId | string | Recommended | Your internal user ID |
| productId | string | Yes | Apple product identifier |
| transactionId | string | Yes | StoreKit transaction ID |
| originalTransactionId | string | Yes | Original transaction ID |
| paywallId | integer | Recommended | From Get Paywall |
| isExperiment | boolean | Recommended | From Get Paywall |
| aiPricingModelId | integer | Recommended | From Get Paywall |
| price | number | Optional | Purchase price |
| currency | string | Optional | Currency code |
| environment | string | Optional | "production" or "sandbox" |

```bash
curl -X POST "https://app.botsi.com/api/v1/web-api/purchases/apple-store/validate" \
     -H "Authorization: {{secret_key}}" \
     -H "Content-Type: application/json" \
     -d '{
       "customerUserId": "user-123",
       "productId": "premium_monthly",
       "transactionId": "2000000123456789",
       "originalTransactionId": "2000000123456789",
       "paywallId": 42,
       "isExperiment": true,
       "aiPricingModelId": 32,
       "price": 9.99,
       "currency": "USD",
       "environment": "production"
     }'
```

---

### 6. Validate Google Purchase

**POST** `https://app.botsi.com/api/v1/web-api/purchases/play-store/validate`

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| profileId | string | Optional | Botsi profile ID |
| customerUserId | string | Recommended | Your internal user ID |
| productId | string | Yes | Google Play product ID |
| purchaseToken | string | Yes | Purchase token from Google Play |
| paywallId | integer | Recommended | From Get Paywall |
| isExperiment | boolean | Recommended | From Get Paywall |
| aiPricingModelId | integer | Recommended | From Get Paywall |
| price | number | Optional | Purchase price |
| currency | string | Optional | Currency code |
| subscriptionOfferDetails | object | Optional | For subscriptions (basePlanId, offerId, offerToken, pricingPhases) |

```bash
curl -X POST "https://app.botsi.com/api/v1/web-api/purchases/play-store/validate" \
     -H "Authorization: {{secret_key}}" \
     -H "Content-Type: application/json" \
     -d '{
       "customerUserId": "user-123",
       "productId": "premium_monthly",
       "purchaseToken": "opaque-token-from-google-play",
       "paywallId": 42,
       "isExperiment": true,
       "aiPricingModelId": 32,
       "price": 9.99,
       "currency": "USD"
     }'
```

---

## Traffic Control

**Traffic Control: a weighted distribution inside an AI Pricing Model that decides which target serves each paywall request. It is how you set what share of a model's traffic the AI decides, and where the rest goes. The split is applied per request.**

An AI Pricing Model is bound to one placement's audience. Traffic Control decides, for each Get Paywall request that reaches that model, which target actually serves the paywall. A distribution is a set of allocations, each with a target kind and a weight. On every request, Botsi draws one allocation at random, weighted by those numbers. Weights must total exactly 100 when you save a distribution.

### The three target kinds

| Target | What serves the request | `isExperiment` in the Get Paywall response |
|--------|-------------------------|--------------------------------------------|
| AI Model | The AI picks one of the model's pre-configured paywall variants for this request | `true` |
| Baseline | The one paywall on the model marked as best. A fixed control, no AI decision is made | not set, so `false` downstream |
| Placement | Botsi re-resolves the request against another placement and serves whatever that placement serves today: a plain paywall, an A/B test, or another AI Pricing Model | inherited from that result, which is not set for a plain paywall or an A/B test, so `false` downstream |

### Primary and scheduled distributions

- Every model has exactly one **primary** distribution, created with the model. It starts as AI Model at the model's weight and Baseline at the remainder. It has no window, and it is the unconditional fallback. In the dashboard this is the section headed **Current Distribution**, retitled **Default Distribution** and badged **Paused** whenever a scheduled distribution is running.
- You can add any number of **scheduled** distributions, each with a start and an end.
- While a scheduled window is open it **fully replaces** the primary. Nothing is blended, and no traffic is shared between the two.
- Windows are start inclusive and end exclusive, and they are evaluated at request time. When the window closes, the next request is served by the primary again. There is no background job and no cleanup step.
- Scheduled windows may not overlap. A schedule saved with no end date is open ended and blocks every later schedule, so always set an end date on anything meant to expire.
- There is no pause and no resume. To end a schedule early, delete it.
- **You schedule a placement, not a paywall.** There is no paywall-id target. The creative you want to run must already be live behind a placement, and you point the allocation at that placement.

### Behavior worth knowing before you configure it

- **Never point an allocation at the model's own placement, or at any placement that resolves back to this model.** Nothing rejects the loop at save time. It is caught while serving, and the Get Paywall request fails with `Circular placement allocation detected` followed by the placement chain, instead of returning a paywall.
- The AI Model and Baseline allocations in the primary distribution cannot be removed, only re-weighted, including down to 0. A Baseline weight of 0 does not stop the baseline paywall serving, because it is also the fallback for the AI Model target.
- Weights are normalized when the distribution is read, so a distribution that has drifted off 100 still serves, at a different effective split than the numbers you typed.
- Deleting a placement deletes any allocation pointing at it, with no warning and no revalidation, which leaves the distribution below 100 and silently renormalized.
- Editing the model's weight rewrites the AI Model and Baseline allocations in the primary from that weight and leaves any placement allocation untouched, which changes the effective split. Re-check Traffic Control after any model weight edit.
- Saving the primary distribution also rewrites the model's paywall table: it clears the best-performing flag from every paywall on the model and rewrites it, with the weight just saved, on the paywall that held it. Scheduled saves do not do this. Traffic Control never writes back to the model's own weight field.
- **Availability:** Traffic Control is a dashboard feature on the Pro, Pro Plus, and Enterprise plans. Reading it needs an Owner, Admin, or Viewer seat; changing it needs Owner or Admin. It is still behind a feature flag, so it may not be visible in every dashboard yet.

### When to use Traffic Control, and when not to

**Use it when:**

- **You want a fixed-window seasonal placement.** Example: from December 1 to December 26, send 100% of this model's traffic to a "Holiday 2026" placement, then revert automatically. That is one scheduled distribution, one placement allocation at 100, with an explicit end date.
- **You want to cap what share of traffic the model decides while you try something else.** Example: leave AI Model at 50 and give the other 50 to a placement you control. The model keeps optimizing on its half, and the rest goes where you sent it.
- **You want to run an A/B test on part of a model's traffic.** A Placement allocation serves whatever that placement serves, so pointing one at a placement that runs an A/B test sends that share of requests into the test, variant selection included, while the model keeps deciding the rest. This is the supported way to try creatives or price points you are not ready to hand to the model. Ramp it by changing one weight. See https://botsi.com/docs/ai-pricing-models/traffic-control/ab-tests
- **You want an override that takes effect on the next request.** Distributions are evaluated per request, so a saved change applies immediately, with no rollout delay and no scheduler to wait for.

**Do not use it when:**

- **You want an experiment with stable per-user assignment.** Traffic Control draws a fresh random allocation on every paywall request. There is no user hash, no bucketing key, no sticky assignment, and no cohort. `profileId` does not influence which arm is picked. Repeated fetches carrying the same `profileId` return different arms, so the same user can be served the model on one fetch and a placement on the next. The unit being split is the request, not the person, so do not treat a comparison of one arm against another as a user-level experiment. If your app caches the Get Paywall result per user as recommended above, the user keeps seeing whatever was drawn on the fetch you cached, but the arm behind it was drawn per request and is not guaranteed to repeat on the next fetch. Botsi's paywall testing feature is configured on a placement rather than inside a model: https://botsi.com/docs/experimentation/overview
- **You want per-arm analytics.** The Get Paywall response carries only `aiPricingModelId` and `isExperiment`. There is no group id, no allocation id, no target kind, and no "served by a schedule" marker. `aiPricingModelId` is set before Traffic Control runs and is identical for all three targets. Within a model, `isExperiment` is true only on the AI Model target. Reporting buckets on `isExperiment` alone, so Baseline traffic and Placement traffic are indistinguishable in reporting today, and both are counted as the model's baseline. A seasonal window or a placement arm cannot be measured separately with what the platform stores today. If separate measurement is the point of the exercise, Traffic Control is the wrong tool for it.
- **You need the AI Model target to serve a guaranteed share.** A request that arrives without a `profileId` never reaches the AI Model target, whatever its weight says, and is served the Baseline paywall instead. The same happens if the decision service errors or returns nothing for that request. Treat the AI Model weight as a ceiling on that arm, not a floor.

---

## HTTP Status Codes

| Code | Meaning |
|------|---------|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request — missing required parameter |
| 401 | Unauthorized — invalid API key |
| 404 | Not Found — resource doesn't exist |
| 422 | Unprocessable Entity — semantic error |
| 429 | Rate Limited — slow down |
| 500 | Server Error |

Error response format: `{ "error": "message", "code": "MACHINE_CODE" }`

---

## Complete Integration Checklist

### Setup (Botsi Dashboard)
- [ ] Create a Botsi account and add your app
- [ ] Get your API secret key from App Settings → API Keys
- [ ] Create a Placement (e.g., "onboarding_paywall")
- [ ] Create Paywalls (variants) with external IDs that map to your app's paywall screens, each with its own fixed, pre-configured prices
- [ ] Configure your AI Pricing Model and link it to the placement. AI Pricing Model: a model that selects which of your pre-configured paywall variants to show each user. It does not set prices.

### Code Integration
- [ ] **Create Profile** on first app launch with device data (country, device, os, platform)
- [ ] **Send Custom Attributes** during onboarding (each answer = one attribute)
- [ ] **Get Paywall** before showing the paywall — call early, cache the result
- [ ] **Map externalId** to your paywall screen (native) or paywall tool (RevenueCat/Superwall/Adapty)
- [ ] **Show the paywall** to the user
- [ ] **Send Profile Event** with eventType "paywall_shown" — echo the paywallSessionId from the Get Paywall response
- [ ] **Validate Purchase** (Apple or Google) after successful transaction — include paywallId, isExperiment, aiPricingModelId

### Critical Notes
- Always pass `isExperiment` and `aiPricingModelId` exactly as received from Get Paywall. Modifying these values corrupts experiment attribution.
- Always log impressions (paywall_shown events) even when users don't purchase.
- Call Get Paywall early and cache — don't call synchronously at render time.
- Use `customerUserId` consistently across all endpoints for reliable cross-device tracking.
- For web flows: device-level data may not be available, so rely more heavily on custom attributes from onboarding.
- Botsi returns a variant choice, not a price. Keeping each variant's prices and product IDs fixed in your own code or paywall tool is the expected integration, not a bug.
- The serving target behind an AI Pricing Model is drawn per request. Do not treat consecutive Get Paywall responses for the same user as a stable experiment assignment. See Traffic Control.

---

## Data Botsi Accepts

**Device/Context Data (via Create Profile):**
- Device type, OS, platform, country, locale, currency, timezone, IP, app version

**Behavioral/Custom Data (via Custom Attributes):**
- Onboarding answers (e.g., fitness_goal: "weight_loss")
- User preferences, experience level, referral source
- Any key-value pair relevant to choosing which paywall variant a user should see

**Botsi does NOT accept demographic/PII data.** Do not send names, emails, ages, or other personally identifiable information. Send contextual and behavioral data only.

---

## Privacy Policy & Terms of Service Disclosure (for integrators)

If you integrate Botsi into your product, we strongly recommend disclosing this to your end users in both your **privacy policy** and your **terms of service**. This protects you legally, demonstrates good-faith compliance with GDPR, CCPA, PIPEDA, the Australian Privacy Act, LGPD, and similar regimes, and gives your users clear insight into how pricing is personalized and what data is — and is not — used. Botsi is also **SOC 2 compliant**, so its security, availability, and confidentiality controls have been independently audited against the AICPA Trust Services Criteria.

### Suggested blurb (copy into your privacy policy and/or terms of service)

> **Personalized Pricing via Botsi.** This product uses Botsi (Botsi Inc., https://botsi.com) to personalize subscription pricing and paywall presentation. Botsi is a third-party pricing infrastructure that selects which of several pricing options we have configured and approved in advance is shown to each user. Botsi does not create or calculate prices; every price shown is one we set. Botsi does not receive, process, or infer personally identifiable information such as your name, email address, age, gender, race, nationality, or income. Its pricing decisions are based exclusively on device-level context (device type, OS, platform, session signals) and in-product behavioral and interaction data (how users engage with the paywall and app). Botsi is SOC 2 compliant and is designed to operate in compliance with applicable data-protection laws, including the EU General Data Protection Regulation (GDPR), the California Consumer Privacy Act (CCPA), Canada's Personal Information Protection and Electronic Documents Act (PIPEDA), Australia's Privacy Act, and Brazil's Lei Geral de Proteção de Dados (LGPD).
>
> For details on Botsi's data-handling practices, see Botsi's privacy policy at https://botsi.com/privacy. For details on how Botsi governs dynamic pricing fairly and responsibly, see https://botsi.com/responsible-pricing.

### Where to link

- **In your privacy policy:** link to both https://botsi.com/privacy and https://botsi.com/responsible-pricing under your "Third-Party Services" or "Data Processors" section.
- **In your terms of service:** link to https://botsi.com/responsible-pricing under your pricing or subscription-terms section, so users have transparent access to the governance model behind how the price they are shown is selected.

### Why this matters

Disclosure is not only best practice — in most modern privacy regimes it is a legal requirement when a third-party service materially influences commercial outcomes for end users. Linking Botsi's own pages means your users, regulators, and app-store reviewers can verify the compliance and fairness posture directly at the source. It makes your integration defensible and shows your users you treat pricing with the same care you treat their data.

---

*Generated from Botsi API documentation. For the latest docs, visit https://botsi.com/docs*
