V1 to V2 Migration
This guide walks you through moving your profiles, paywalls, events, and purchase validation to the Botsi V2 API. You can keep your existing users and their Botsi profile IDs.
This guide focuses on replacing V1 API calls and SDK-specific API calls with V2 API calls. It explains the changes to request fields, responses, and authentication, and identifies operations that do not have a V2 replacement.
Already on a Botsi Mobile Native SDK
If you are already using a Botsi Mobile Native SDK, upgrade to the latest 2.x.x version. This will handle migration to V2 Botsi API endpoints. If you upgrade to the latest SDK version, the only other step you will need to take is Section 8: Move notification webhooks to V2.
The migration also includes updating the notification webhook URLs used by your stores and billing services.
You can follow along with the V1 Postman collection and V2 Postman collection.
In this guide#
- Replace SDK-specific API calls and V1 API calls with V2 API calls
- Update your API address and calls
- Keep your existing profiles
- Update custom attributes
- Fetch and display a paywall
- Report a paywall view
- Validate a purchase
- Move notification webhooks to V2
- Handle errors
- Test and switch over
1. Replace SDK-specific API calls and V1 API calls with V2 API calls#
If your integration uses V1 API calls or SDK-specific API calls, move those requests to the V2 calls described below. You can make these calls through your server or directly from your app.
The field comparisons below use the V1 Web API format. When moving from SDK-specific API calls, follow the V2 request examples rather than changing only the request address.
Through your server (recommended)#
Your app → Your server → Botsi APIYour server sends the request to Botsi and returns the information your app needs. This is the most secure option for protecting your app secret key because the key stays on your server.
Directly from your app (when needed)#
Your app → Botsi APIIf using a server is not practical, your app can call the Botsi API directly. Send the public key. It ships in the app binary. Do not put the secret key in a mobile or web client.
Both approaches use the same V2 calls. A call from your server sends the secret key. A call from inside the app sends the public key. POST /v2/transactions accepts only the secret key.
Order of calls#
The usual sequence is:
- Create the user Profile and save their
profileId. - Add any custom attributes to the profile.
- Fetch a paywall and save the response, including
paywallSessionId. - Display the paywall and report one
paywall_shownevent. - Confirm purchases using the Validate endpoints when necessary for App Store and Play Store.
2. Update your API address and calls#
API addresses#
For example, V2 profile creation uses https://api.botsi.com/v2/profiles.
Use the V2 base address for your migrated calls. V2 paths omit the /web-api and /sdk parts of the older addresses.
Authentication#
If you use the V1 Web API, keep the same app secret key for server-to-server calls. If you use SDK-specific API calls with a public key, keep sending that public key on the V2 routes in this guide. You can find the secret key on your app's configuration page in the Botsi dashboard.
Authorization: <APP_SECRET_KEY>
Content-Type: application/jsonFrom inside the app, send the public key in that same header:
Authorization: <APP_PUBLIC_KEY>Send the key exactly as provided, without Bearer or Api-Key before it. In V2, a missing or invalid key returns 401; V1 returns 403.
Which calls change?#
Add the paths below to the matching base address above.
Changing the address is only part of the migration. Update the request fields and how you read the responses as described below.
For the V2 calls in this table, profile creation returns 201, including when it finds an existing profile. The other V2 calls return 200. Update any checks that expect V1's 201 responses for paywalls, events, attributes, or purchase validation.
3. Keep your existing profiles#
Keep the Botsi profileId you already have for each user. You do not need to create replacement profiles for V2.
If you use your own user ID to find a profile, send the same value under the new name, appUserId. For example, a V1 profile created with customerUserId: "user-123" can be found in V2 with appUserId: "user-123", using that app's secret key or public key.
Repeating profile creation with that user ID returns the existing profile without changing its details. If a user has no appUserId, keep their existing profileId; creating again without a user ID can create another profile.
Rename two request fields#
V2, POST /profiles:
{
"appUserId": "user-123",
"country": "US",
"device": "iPhone 15 Pro Max",
"os": "iOS 17.0",
"platform": "ios",
"appVersion": "1.0.0",
"ipAddress": "203.0.113.7"
}Replace the example values with the user's details. The required fields are country, device, os, platform, and appVersion.
You can also send appUserId, locale, appBuild, advertisingId, ipAddress, email, phone, gender, idfa, and username.
- Use a two-letter country code such as
US. - Supported platforms are
android,ios,ipados,tvos,macos,watchos,visionos, andstripe. - Use the new field names. V2 ignores the old
customerUserIdandiprequest fields. - Send operating-system details in
os. Additional fields such asosVersion,currency, andtimezoneare not saved by this call.
Update how you read the response#
Both versions return the profile inside data. Save data.profileId for later calls.
V2 profile creation returns fewer fields than V1. It includes device and app details, but does not return custom attributes, purchase history, access levels, or contact details such as email and phone.
V2 does not offer a profile update call. Sending profile creation again does not update an existing profile or link an anonymous profile to your user ID. If your integration relies on these operations, confirm an alternative with Botsi before migrating them.
4. Update custom attributes#
Custom attributes are extra details you save about a user.
V2 uses profileId to identify the user. Both adding and updating attributes take a list, even when you send only one attribute. Each key and value must be text.
Add or set values#
Use POST /custom-attributes:
{
"profileId": "<PROFILE_ID>",
"custom": [
{ "key": "user_level", "value": "premium" },
{ "key": "referral_source", "value": "facebook" }
]
}This replaces both V1 add calls. If a key already exists, its value is updated. If the same key appears more than once in the request, the last value is used.
V2 returns only the attributes changed by the request. If you keep your own copy of the user's attributes, update those entries without removing the others.
Update existing values#
Use PUT /custom-attributes:
{
"profileId": "<PROFILE_ID>",
"custom": [
{ "key": "user_level", "value": "vip" }
]
}Remove V1's attrId. V2 finds each attribute by its key.
PUT updates existing keys only. If any key is missing, the request returns 404 and nothing changes. Use POST when you want to create a key if it does not exist. Sending an empty list returns 400.
Changing a key in a PUT request does not rename the old attribute. If your integration renames attributes, arrange that change separately before switching.
5. Fetch and display a paywall#
A placement identifies where a paywall appears in your app, such as during onboarding. Keep using the placement ID configured for your app.
Change the path from /paywalls to /paywall. Send only profileId and placementId; V2 no longer needs store.
POST /paywall:
{
"profileId": "<PROFILE_ID>",
"placementId": "onboarding"
}V2 uses the user's saved profile details to choose the paywall. Extra request fields such as store, locale, or country are ignored.
Read products by store#
V1 returns sourceProducts. V2 returns paywallProducts, with separate details for each store:
{
"ok": true,
"data": {
"id": 42,
"externalId": "premium",
"name": "Premium",
"isExperiment": true,
"aiPricingModelId": 32,
"paywallSessionId": "<PAYWALL_SESSION_ID>",
"paywallProducts": [
{
"paywallProductId": 907,
"name": "Premium Monthly",
"period": "monthly",
"appStore": {
"productId": "com.example.premium.monthly",
"offerId": null,
"promotionalOfferId": null,
"offerType": null
},
"playStore": {
"productId": "premium_monthly",
"basePlanId": "monthly-base",
"offerId": null
},
"stripe": null,
"web2wave": null,
"custom": null
}
]
}
}The values above are examples. Use the actual response for the paywall your user sees.
If a store's value is null, that product is not available through that store. Some offer details can also be null.
V2 does not return remoteConfigs, revision, abTestId, abTestName, the Google offerIds list, or the isConsumable flag. Use the product's period and store details to determine its purchase type. If your app needs any of the other removed fields, resolve that dependency with Botsi before switching paywall calls.
Keep the details of the paywall shown#
Save these together for each user and paywall display:
- The
profileIdandplacementIdyou sent. - The returned
data.id, used aspaywallIdin purchase calls. paywallSessionId, which links the view and purchase to the selected paywall.isExperimentandaiPricingModelId, plus the product the user chooses.
aiPricingModelId can be null. isExperiment describes an AI Pricing selection; false does not necessarily mean the user is outside an A/B test.
Fetch the paywall early, before the user needs to see it. If the call fails, show your default paywall instead of making the user wait through repeated attempts.
Changing API calls does not automatically move your existing placements or experiments to V2. Confirm with Botsi that their settings and purchase tracking are ready before making that change.
6. Report a paywall view#
Send this after the paywall appears. Fetching a paywall alone does not count as a view.
V1 sends a list containing the user ID and event details. V2 takes just one event, with exactly two fields:
POST /events:
{
"eventType": "paywall_shown",
"paywallSessionId": "<PAYWALL_SESSION_ID>"
}Remove the surrounding square brackets from your V1 request. Also remove profileId, customerUserId, timestamps, and any separate paywall or experiment fields. V2 gets those details from paywallSessionId; extra fields cause a 400 error.
The response is:
{
"ok": true
}Only paywall_shown is accepted here. Other event types cannot be moved to V2 /events; confirm an alternative with Botsi if your integration uses them.
Use the original session ID#
Save the complete paywallSessionId and send it back unchanged. Its length can vary, so make sure your storage does not shorten it. It may start with v1. or v2. even when you use the V2 API; keep either value exactly as returned.
Report the view promptly. The usual time limit is 24 hours after the session ID was issued. If it expires before a new display, fetch the paywall again. Do not use a new session ID to report an older view.
Send the event once per view. Repeated requests may count the view again, depending on the placement. If a request times out, avoid automatically resending it without checking whether it was received.
7. Validate a purchase#
After a purchase completes, send the store's purchase details and the user's profileId to Botsi. V2 requires profileId; customerUserId cannot replace it in these calls.
Always set environment explicitly:
sandboxfor test purchases.productionfor real purchases.
The V2 Postman collection starts with sandbox, while V1 starts with production. Check this setting when switching. Leaving it out can cause missing revenue in some existing Google purchase flows.
Link the purchase to the paywall#
For placements moved to V2, include the paywallSessionId from the paywall actually shown. Keep it with the purchase if you need to retry later. The 24-hour limit for reporting views does not apply to purchase validation.
For placements still using V1 settings, also keep sending placementId, paywallId, isExperiment, and the model ID when one was returned. These fields are optional in V2 requests, but leaving them out can prevent the purchase from being linked to the correct paywall or experiment.
If your existing A/B tests depend on abTestId, confirm with Botsi how to move their purchase tracking to V2. V2 paywall responses do not include that ID.
Before switching purchases, confirm with Botsi that your placements are ready for the request format below. If your saved Postman purchase requests do not include paywallSessionId, add it.
Apple purchase#
POST /purchases/apple-store/validate:
{
"profileId": "<PROFILE_ID>",
"productId": "com.example.premium.monthly",
"transactionId": "<APPLE_TRANSACTION_ID>",
"originalTransactionId": "<APPLE_ORIGINAL_TRANSACTION_ID>",
"source": "purchasing",
"environment": "sandbox",
"isSubscription": true,
"placementId": "onboarding",
"paywallId": 42,
"isExperiment": true,
"aiPricingModelId": 32,
"paywallSessionId": "<PAYWALL_SESSION_ID>"
}Required fields are profileId, productId, transactionId, and originalTransactionId. Use the transaction IDs from the actual Apple purchase and source: "purchasing" for a new purchase.
V2 does not offer a dedicated purchase restore call. Setting source to restore on a validation request is not a general replacement for restoring purchases. If your integration needs this operation, confirm an alternative with Botsi before migrating it.
Google purchase#
POST /purchases/play-store/validate:
{
"profileId": "<PROFILE_ID>",
"productId": "premium_monthly",
"token": "<GOOGLE_PURCHASE_TOKEN>",
"environment": "sandbox",
"placementId": "onboarding",
"paywallId": 42,
"isExperiment": true,
"aiPricingModelId": 32,
"paywallSessionId": "<PAYWALL_SESSION_ID>",
"subscriptionOfferDetails": {
"basePlanId": "monthly-base",
"pricingPhases": [
{
"priceAmountMicros": 9990000,
"currencyCode": "USD",
"billingPeriod": "P1M",
"recurrenceMode": 1,
"billingCycleCount": 0
}
]
}
}Required fields are profileId, productId, and token. Send the offer details that match the purchase:
- Subscription: use
subscriptionOfferDetailswith the purchased base plan, offer, and pricing periods from Google Play. - Free trial: include the zero-price trial period followed by the paid period. Use the values supplied by Google Play.
- One-time purchase: replace
subscriptionOfferDetailswithoneTimePurchaseOfferDetails, containingpriceAmountMicrosandcurrencyCode.
Choose the offer the user purchased, rather than assuming it is the first offer in the store response. priceAmountMicros uses millionths of a currency unit: 9990000 means USD 9.99 when currencyCode is USD.
Check the returned access details#
Both purchase calls return 200 with the result inside data. V2 returns these fields:
profileId, appUserId, state, totalRevenueUsd,
accessLevels, subscriptions, nonSubscriptionsUpdate your code to read appUserId instead of customerUserId. V2 purchase responses no longer contain the full profile, custom attributes, or contact details.
Check whether the user's access is active and when it expires before enabling paid features. A successful request alone does not prove that new access was granted. The returned purchase details can be incomplete, and nonSubscriptions is currently empty; do not use these fields as a complete purchase history.
In both examples, replace the sample values with your purchase and saved paywall details. Keep paywallId and aiPricingModelId as numbers, and isExperiment as true or false, without quotation marks. Profile, product, and transaction IDs are text. If the paywall returns no model ID, omit aiPricingModelId rather than sending the text "null".
8. Move notification webhooks to V2#
Notification webhooks let your store or billing service send Botsi updates about purchases, renewals, and refunds. Update these destinations separately from your app's API calls.
Choose your V2 notification URL#
All notification URLs use this format:
https://api.botsi.com/v2/notifications/<provider>/<notification-key>Use your app's notification key for every provider. This is different from your app secret key and profileId; it identifies which app should receive the update. See Notification URLs.
Add the following paths to https://api.botsi.com:
Replace the existing destination#
Copy your app's notification URL from Botsi settings. Replace the old Botsi destination in the service that sends or forwards notifications. Keep the provider's required signing secrets or tokens configured, and check a test delivery and the resulting update in Botsi.
- Apple: In App Store Connect, open your app's App Information → App Store Server Notifications settings. Set both the Production Server URL and Sandbox Server URL to your V2 Apple URL. If you forward Apple notifications through your own server, update its destination and forward the original message unchanged. Follow the Apple notification instructions.
- Google Play: In Google Cloud, open Pub/Sub → Subscriptions and edit the push subscription for your Google Play notifications. Replace its endpoint URL with your V2 Google URL. Check that Botsi has the correct package name and service account, with Google Play Orders API access. A successful delivery alone does not confirm that a message with the wrong package name was processed. Follow the Google Play notification instructions.
- Stripe: Update the destination in Developers → Webhooks. Use the V2 Stripe URL for both live and test mode. Keep the required events enabled, and save each endpoint's signing secret in Botsi's matching Production or Sandbox signing-secret field. If you create a replacement endpoint, use its new signing secret. Follow the Stripe setup instructions.
For other services, use the setup link for your provider in the notification URL guide.
Avoid reporting the same purchase twice#
Use one notification route for each transaction. Replace the old destination instead of leaving both versions delivering the same events. Only one of RevenueCat, Superwall, or Adapty can be enabled per app. When using Superwall or Adapty for revenue, do not also send direct Apple or Google notifications to Botsi.
A repeat delivery to the same V2 notification endpoint does not count revenue again. Providers can retry temporary 5xx failures. A 400 can mean the notification key is incorrectly formatted; a 404 means it does not identify an app. Provider-specific errors are covered in the linked setup instructions.
9. Handle errors#
V1 errors use message and status. V2 errors use error and code:
{
"error": "No valid API key provided.",
"code": "unauthorized"
}Use the HTTP status and code to decide what to do. The error message helps explain the problem, but its wording may change.
For a failed paywall fetch, show your default paywall. For a view that may already have been recorded, avoid repeated submissions that could count it twice.
10. Test and switch over#
Try the calls in Postman#
- Import the V2 collection. Set
baseUrltohttps://api.botsi.comandapiVersiontov2. - Set
secretKeyin your private Postman environment. Leave the saved collection key blank. - Set
appUserId,placementId, andstoreEnvironment. Use test purchases and the matching device/platform details. - Run profile creation, optional custom attributes, paywall fetch, one view event, and the purchase request that matches your store and purchase type.
The collection saves profileId and paywall details for later requests. Avoid setting another variable with the same name in your environment, because it can override the saved value. Replace sample purchase IDs and tokens with your actual test values. Add paywallSessionId to purchase requests if it is missing.
The Google subscription, trial, and one-time examples are alternatives. Use the one that matches the purchase.