{"info":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","description":"<html><head></head><body><p>The Botsi v1 Web API is a legacy API for managing user profiles, choosing paywalls, recording events, and validating or restoring purchases from your server.</p>\n<p>This collection covers selected v1 workflows: Profiles, Paywalls, Events, and Purchases. It includes shared authentication, scripts that save values for later requests, response checks, and saved examples. Profile deletion, direct access grants and revocations, and transaction reporting are outside this collection.</p>\n<h2 id=\"set-up-the-collection\">Set up the collection</h2>\n<ol>\n<li><p>Find your app's secret key on the App configuration page in the <a href=\"https://app.botsi.com\">Botsi dashboard</a>. Set <code>secretKey</code> in a Postman environment. Keep the saved collection value blank.</p>\n</li>\n<li><p>Set <code>customerUserId</code> to your user's ID and <code>placementId</code> to a placement configured for the same app. Review the device, OS, platform, and app version in Create a Profile. Use an Android profile and <code>store: play_store</code> for a Google Play workflow, or an Apple profile and <code>store: app_store</code> for an App Store workflow.</p>\n</li>\n</ol>\n<p>The setup script skips requests with missing variables and writes the variable names to the Postman Console. Use a current Postman version that supports <code>pm.execution.skipRequest()</code>.</p>\n<h2 id=\"base-url\">Base URL</h2>\n<p>Use these settings to connect to the production API:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Setting</th>\n<th>Production value</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>baseUrl</code></td>\n<td><code>https://app.botsi.com/api</code></td>\n</tr>\n<tr>\n<td><code>apiVersion</code></td>\n<td><code>v1</code></td>\n</tr>\n<tr>\n<td>Full request path</td>\n<td><code>https://app.botsi.com/api/v1/web-api/...</code></td>\n</tr>\n</tbody>\n</table>\n</div><p><strong>v1 is not available at</strong> <strong><code>api.botsi.com</code>****.</strong> That host serves v2 and returns <code>404</code> for v1 requests.</p>\n<h2 id=\"authentication\">Authentication</h2>\n<p>All requests use the authentication setting saved on the collection. Send the secret key itself in the <code>Authorization</code> header:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-http\">Authorization: &lt;secretKey&gt;\nContent-Type: application/json\n\n</code></pre>\n<p>Do not add <code>Bearer</code> or <code>Api-Key</code> before the key. A missing key, an unknown key, or a key with a prefix returns <code>403 Forbidden</code>. Your app's public key also returns <code>403</code>; it is used by the mobile SDK, not for these requests.</p>\n<h2 id=\"request-order\">Request order</h2>\n<ol>\n<li><p><strong>Create a Profile</strong> for the user. Use <code>customerUserId</code> to link the profile to your own user record.</p>\n</li>\n<li><p><strong>Add custom attributes</strong> if you have more information about the user.</p>\n</li>\n<li><p><strong>Fetch Paywall</strong> and save the returned <code>paywallSessionId</code>.</p>\n</li>\n<li><p><strong>Send Paywall Shown Event</strong> once when the paywall appears. Include that <code>paywallSessionId</code>. The Explicit Fields request is an alternative for existing integrations. Do not send both for the same view.</p>\n</li>\n<li><p><strong>Validate the purchase</strong> using the one request that matches the store and purchase type. Include the details from the paywall actually shown. The Google subscription, free-trial, and one-time examples are alternatives.</p>\n</li>\n</ol>\n<p>The collection scripts save values for later requests, so you do not need to copy them manually. Keep these generated variables in the collection; environment or runner data values with the same names would override them:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Request</th>\n<th>Variables saved</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Create a Profile</td>\n<td><code>profileId</code></td>\n</tr>\n<tr>\n<td>Add Custom Attribute</td>\n<td><code>customAttributeId</code></td>\n</tr>\n<tr>\n<td>Fetch Paywall</td>\n<td><code>paywallSessionId</code>, <code>paywallId</code>, <code>abTestId</code>, <code>aiPricingModelId</code>, <code>isExperiment</code></td>\n</tr>\n</tbody>\n</table>\n</div><p>In request bodies, keep <code></code>, <code></code>, <code></code>, and <code></code> outside quotation marks. The first three must be numbers; <code>isExperiment</code> must be <code>true</code> or <code>false</code>.</p>\n<h2 id=\"response-status-codes\">Response status codes</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Code</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>200</code></td>\n<td>A <code>PATCH</code>, <code>PUT</code>, or <code>DELETE</code> request succeeded. The response contains the requested data.</td>\n</tr>\n<tr>\n<td><code>201</code></td>\n<td>A <code>POST</code> request succeeded. All v1 POST requests return <code>201</code>, including Fetch Paywall and Send Events. The v1 OpenAPI documentation lists these as <code>200</code>, but the API returns <code>201</code>.</td>\n</tr>\n<tr>\n<td><code>400</code></td>\n<td>Check the request fields and values. Examples include a missing user ID, a product that is not configured for the store, or an invalid <code>paywallSessionId</code>.</td>\n</tr>\n<tr>\n<td><code>403</code></td>\n<td>The secret key is missing or invalid, or Google returned a forbidden response.</td>\n</tr>\n<tr>\n<td><code>404</code></td>\n<td>The profile, placement, paywall, or store transaction was not found.</td>\n</tr>\n<tr>\n<td><code>410</code></td>\n<td>Google reports that the purchase is no longer available to query.</td>\n</tr>\n<tr>\n<td><code>500</code>, <code>502</code>, <code>503</code>, <code>504</code></td>\n<td>The server could not complete the request. See retry guidance below.</td>\n</tr>\n</tbody>\n</table>\n</div><p>For <code>4xx</code> errors, check the request and store response before trying again. For <code>5xx</code> errors, wait before retrying and increase the delay if failures continue. No API rate limit is configured in the reviewed application code. Google can return other <code>4xx</code> statuses; a Google rate-limit response is returned by this implementation as <code>500</code>.</p>\n<p><strong>If Fetch Paywall fails, show your default paywall.</strong> Fetch it early and keep a saved response ready, so the user does not have to wait for retries. Retry failed events carefully because repeated views are counted separately. For Apple validation, a transaction whose purchase date is before the current UTC day is skipped even when the response is <code>201</code>. Use the restore workflow for earlier purchases and check the returned access details.</p>\n<h2 id=\"error-responses\">Error responses</h2>\n<p>An error response contains <code>ok: false</code>, a readable <code>message</code>, and the HTTP status in <code>status</code>:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"ok\": false,\n  \"message\": \"Profile not found with provided customerUserId user-123 appId: 4021\",\n  \"status\": 404\n}\n\n</code></pre>\n<p>Use <code>status</code> to decide how to handle an error. v1 has no separate error <code>code</code>. Log <code>message</code> for troubleshooting, but do not rely on its wording staying the same.</p>\n<p>Most validation errors appear in one comma-separated message. Event errors use <code>Event[].:</code> , with semicolons between fields and commas between rules for the same field. Store error wording can vary. Google API error messages start with <code>Google API error:</code>.</p>\n<h2 id=\"profile-ids-and-access-details\">Profile IDs and access details</h2>\n<p>Use a profile that belongs to the app identified by your secret key. v1 does not consistently check app ownership when a request supplies <code>profileId</code>. This affects profile updates, custom attributes, paywall and event requests, and purchase validation and restore. Where supported, <code>customerUserId</code> looks up the user within your app. If both IDs are sent, <code>profileId</code> generally takes priority; custom attributes can fall back to <code>customerUserId</code> when that profile ID is not found.</p>\n<p>Profile responses include personal information. The <code>accessLevels</code> and <code>subscriptions</code> objects are built from the profile's single linked access record, using the access level ID and store product ID as keys. Both can be empty. <code>nonSubscriptions</code> is always <code>{}</code>. These objects are not a complete purchase history.</p>\n<p><strong>A successful response is not proof that new access was granted.</strong> Purchase processing can be skipped, and some product types do not update profile access. Check the returned access status and expiry before enabling paid features. The collection tests check response structure and request fields; they do not prove that a purchase was saved or new access was granted.</p>\n<h2 id=\"migrating-from-v1-to-v2\">Migrating from V1 to V2</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Detail</th>\n<th>v1: this collection</th>\n<th>v2</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>URL</td>\n<td><code>app.botsi.com/api/v1/web-api/...</code></td>\n<td><code>api.botsi.com/v2/...</code></td>\n</tr>\n<tr>\n<td>Your user ID</td>\n<td><code>customerUserId</code></td>\n<td><code>appUserId</code></td>\n</tr>\n<tr>\n<td>User's IP address in the request</td>\n<td><code>ip</code></td>\n<td><code>ipAddress</code>; the profile response still uses <code>ip</code></td>\n</tr>\n<tr>\n<td>Invalid authentication</td>\n<td><code>403</code></td>\n<td><code>401</code></td>\n</tr>\n<tr>\n<td>Error fields</td>\n<td><code>ok</code>, <code>message</code>, <code>status</code></td>\n<td><code>error</code>, <code>code</code></td>\n</tr>\n<tr>\n<td>Successful POST</td>\n<td><code>201</code></td>\n<td><code>200</code>, or <code>201</code> when creating a profile</td>\n</tr>\n<tr>\n<td>Fetch Paywall</td>\n<td>Requires <code>store</code>; product fields depend on the store</td>\n<td>Requires only <code>profileId</code> and <code>placementId</code>; products are grouped by store</td>\n</tr>\n<tr>\n<td>Events</td>\n<td>An array of supported event types; uses a token or separate fields</td>\n<td>One <code>paywall_shown</code> object; uses a token</td>\n</tr>\n<tr>\n<td>Custom attributes</td>\n<td>Add one, add several, or update by <code>attrId</code></td>\n<td>Add several or update by key</td>\n</tr>\n<tr>\n<td>Purchase validation</td>\n<td>Apple requires <code>source</code>; both stores require <code>isExperiment</code> and <code>aiPricingModelId</code>. Returns the full profile.</td>\n<td>These fields are optional. Returns selected fields without personal information and checks that <code>profileId</code> belongs to the app.</td>\n</tr>\n<tr>\n<td>Other requests</td>\n<td>Profile updates and purchase restores</td>\n<td>Not available</td>\n</tr>\n</tbody>\n</table>\n</div></body></html>","schema":"https://schema.getpostman.com/json/collection/v2.0.0/collection.json","toc":[],"owner":"2219874","collectionId":"261346bd-8303-45f1-9c1e-e1b68f0e656b","publishedId":"2sBYAxRVaz","public":true,"customColor":{"top-bar":"FFFFFF","right-sidebar":"303030","highlight":"FF6C37"},"publishDate":"2026-09-09T15:44:00.000Z"},"item":[{"name":"1. Profiles","item":[{"name":"Create a Profile","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Response includes the current profile and access objects', () => {","        pm.expect(body.data.profileId).to.be.a('string').and.not.empty;","        pm.expect(body.data).to.have.property('customerUserId');","        pm.expect(body.data.accessLevels).to.be.an('object');","        pm.expect(body.data.subscriptions).to.be.an('object');","        pm.expect(body.data.nonSubscriptions).to.deep.equal({});","    });","","    if (body.ok && body.data && typeof body.data.profileId === 'string') {","        pm.collectionVariables.set('profileId', body.data.profileId);","        if (body.data.customerUserId) pm.collectionVariables.set('customerUserId', body.data.customerUserId);","    }","}"],"id":"f5f22aa6-46f2-4df3-b93d-eefbb695a6b0"}}],"id":"d9948265-1a73-4fe8-83c2-bc414bae7ee9","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"customerUserId\": \"user-123\",\n  \"country\": \"US\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"platform\": \"ios\",\n  \"appVersion\": \"1.0.0\",\n  \"appBuild\": \"100\",\n  \"locale\": \"en_US\",\n  \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n  \"ip\": \"203.0.113.7\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles","description":"<p>Create a profile when the user first opens your app. AI Pricing, subscriptions, and paywall tracking all require a profile.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<p><code>country</code>, <code>device</code>, <code>os</code>, <code>platform</code>, and <code>appVersion</code>.</p>\n<p><code>platform</code> accepts <code>android</code>, <code>ios</code>, <code>ipados</code>, <code>tvos</code>, <code>macos</code>, <code>watchos</code>, <code>visionos</code>, <code>stripe</code>, or <code>web2wave</code>.</p>\n<p>Use a two-letter country code for <code>country</code>. Botsi also accepts uppercase three-letter codes it can convert. A three-letter code it cannot convert causes a <code>500</code> error in v1, rather than a <code>400</code> validation error.</p>\n<h2 id=\"optional-fields\">Optional fields</h2>\n<p><code>customerUserId</code>, <code>locale</code>, <code>appBuild</code>, <code>advertisingId</code>, <code>ip</code>, <code>email</code>, <code>phone</code>, <code>gender</code>, <code>idfa</code>, and <code>username</code>.</p>\n<ul>\n<li><code>customerUserId</code> links the profile to your own user record. Later requests can use it to find the profile.</li>\n<li><code>ip</code> must be a valid IPv4 or IPv6 address.</li>\n<li><code>email</code> must be a valid email address.</li>\n</ul>\n<h2 id=\"how-it-works\">How it works</h2>\n<ul>\n<li>Sending the same <code>customerUserId</code> again returns the existing profile without changing its fields or recording another <code>profile_created</code> event.</li>\n<li>Unknown fields are ignored. Use <code>customerUserId</code> and <code>ip</code> in v1. The v2 names <code>appUserId</code> and <code>ipAddress</code> are ignored, which can leave the profile without a <code>customerUserId</code>.</li>\n<li>On Apple platforms, a device code such as <code>iPhone16,2</code> is converted to its product name, <code>iPhone 15 Pro Max</code>. A readable device name is kept as sent.</li>\n</ul>\n<h2 id=\"response\">Response</h2>\n<p>Returns the profile, including personal information. The <code>accessLevels</code> and <code>subscriptions</code> objects come from the profile's single linked access record. Both can be empty, and <code>nonSubscriptions</code> is always <code>{}</code>. They are not a complete purchase history. v2 returns fewer profile fields.</p>\n<p>The collection script saves <code>data.profileId</code> as <code>profileId</code> for later requests.</p>\n<p><strong>Source:</strong> api/src/web-api/profiles/profiles.controller.ts, dto/create-profile.dto.ts, api/src/profiles/profile.mapper.ts</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","profiles"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"9785b28d-1fea-43b2-919e-1a47c40d03ae","name":"201 Created","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"customerUserId\": \"user-123\",\n  \"country\": \"US\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"platform\": \"ios\",\n  \"appVersion\": \"1.0.0\",\n  \"appBuild\": \"100\",\n  \"locale\": \"en_US\",\n  \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n  \"ip\": \"203.0.113.7\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"createdDate\": \"2026-09-09T10:30:00.000Z\",\n    \"linkedProfileIds\": null,\n    \"state\": \"never-subscribed\",\n    \"customerUserId\": \"user-123\",\n    \"migrationProfileId\": null,\n    \"country\": \"US\",\n    \"device\": \"iPhone 15 Pro Max\",\n    \"os\": \"iOS 17.0\",\n    \"platform\": \"ios\",\n    \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n    \"appBuild\": \"100\",\n    \"appVersion\": \"1.0.0\",\n    \"botsiSdkVersion\": null,\n    \"custom\": [],\n    \"email\": null,\n    \"phone\": null,\n    \"gender\": null,\n    \"idfa\": null,\n    \"username\": null,\n    \"locale\": \"en_US\",\n    \"ip\": \"203.0.113.7\",\n    \"createdAt\": null,\n    \"ipCountry\": null,\n    \"appleConsumptionConsent\": null,\n    \"birthday\": null,\n    \"age\": null,\n    \"totalRevenueUsd\": 0,\n    \"updatedDate\": \"2026-09-09T10:30:00.000Z\",\n    \"accessLevels\": {},\n    \"subscriptions\": {},\n    \"nonSubscriptions\": {}\n  }\n}"},{"id":"26c22a4a-43ae-49e7-bafe-533c1a3cff20","name":"403 Forbidden: Missing or invalid secret key","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"customerUserId\": \"user-123\",\n  \"country\": \"US\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"platform\": \"ios\",\n  \"appVersion\": \"1.0.0\",\n  \"appBuild\": \"100\",\n  \"locale\": \"en_US\",\n  \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n  \"ip\": \"203.0.113.7\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles"},"status":"Forbidden","code":403,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Forbidden resource\",\n  \"status\": 403\n}"},{"id":"a3f7f38d-de36-47ff-bbb0-92256bb1f0f2","name":"400 Bad Request: Invalid request fields","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"customerUserId\": \"user-123\",\n  \"country\": \"US\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"platform\": \"iphone\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"platform must be one of the following values: android, ios, ipados, tvos, macos, watchos, visionos, stripe, web2wave, appVersion must be a string\",\n  \"status\": 400\n}"}],"_postman_id":"d9948265-1a73-4fe8-83c2-bc414bae7ee9"},{"name":"Update a Profile","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 200', () => pm.expect(pm.response.code).to.eql(200));","if (pm.response.code === 200) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Response includes the current profile and access objects', () => {","        pm.expect(body.data.profileId).to.be.a('string').and.not.empty;","        pm.expect(body.data).to.have.property('customerUserId');","        pm.expect(body.data.accessLevels).to.be.an('object');","        pm.expect(body.data.subscriptions).to.be.an('object');","        pm.expect(body.data.nonSubscriptions).to.deep.equal({});","    });","","    pm.test('Response belongs to the requested profile', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw));","        if (sent.profileId) pm.expect(body.data.profileId).to.eql(sent.profileId);","        pm.expect(body.data.customerUserId).to.eql(sent.customerUserId);","    });","}"],"id":"2c48280a-aaab-47c2-8edd-0edd31f358cd"}}],"id":"9c2ef445-4088-4ab0-accf-2510e5349adc","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"PATCH","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"customerUserId\": \"user-123\",\n  \"appVersion\": \"1.1.0\",\n  \"os\": \"iOS 17.4\",\n  \"locale\": \"en_GB\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles","description":"<p>Update a user's profile. You can also use this request to read the profile and check the user's access after validating a purchase.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<ul>\n<li><code>customerUserId</code>: a non-empty user ID.</li>\n<li><code>appVersion</code>: the current app version. Include it on every update, even if it has not changed.</li>\n</ul>\n<h2 id=\"optional-fields\">Optional fields</h2>\n<p><code>profileId</code>, <code>country</code>, <code>device</code>, <code>os</code>, <code>platform</code>, <code>advertisingId</code>, <code>appBuild</code>, <code>email</code>, <code>phone</code>, <code>gender</code>, <code>idfa</code>, <code>username</code>, <code>locale</code>, and <code>ip</code>.</p>\n<p>Only the fields you send are explicitly updated. The profile update timestamp also changes.</p>\n<h2 id=\"choosing-the-profile\">Choosing the profile</h2>\n<p>Normally, Botsi finds the profile using <code>customerUserId</code>. If you also send <code>profileId</code>, Botsi uses that ID to find the profile and writes the supplied <code>customerUserId</code> to it. A mismatched pair changes the profile's user ID. Omit <code>profileId</code> unless you intend to do this.</p>\n<p>A <code>customerUserId</code> from another app returns <code>404</code>.</p>\n<h2 id=\"reading-a-profile\">Reading a profile</h2>\n<p>v1 has no GET request for profiles. Sending <code>customerUserId</code> and the current <code>appVersion</code> returns the profile, but still performs an update and refreshes <code>updatedDate</code>. This is not a read-only request.</p>\n<h2 id=\"response\">Response</h2>\n<p>Returns the full profile, including access levels, subscriptions, and custom attributes.</p>\n<p>The <code>accessLevels</code> and <code>subscriptions</code> objects come from the profile's single linked access record. Both can be empty, and <code>nonSubscriptions</code> is always <code>{}</code>. They are not a complete purchase history.</p>\n<p><strong>Profile ownership:</strong> Use a profile from the app identified by your secret key. This v1 request does not check app ownership when using <code>profileId</code>. A <code>customerUserId</code> lookup is limited to your app.</p>\n<p><strong>Source:</strong> api/src/web-api/profiles/profiles.controller.ts, dto/update-profile.dto.ts, api/src/profiles/profiles.service.ts (updateProfileByCustomerUserId)</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","profiles"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"804176ae-29c7-46af-b121-11d1570cfc2b","name":"200 OK","originalRequest":{"method":"PATCH","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"customerUserId\": \"user-123\",\n  \"appVersion\": \"1.1.0\",\n  \"os\": \"iOS 17.4\",\n  \"locale\": \"en_GB\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"createdDate\": \"2026-09-09T10:30:00.000Z\",\n    \"linkedProfileIds\": null,\n    \"state\": \"never-subscribed\",\n    \"customerUserId\": \"user-123\",\n    \"migrationProfileId\": null,\n    \"country\": \"US\",\n    \"device\": \"iPhone 15 Pro Max\",\n    \"os\": \"iOS 17.4\",\n    \"platform\": \"ios\",\n    \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n    \"appBuild\": \"100\",\n    \"appVersion\": \"1.1.0\",\n    \"botsiSdkVersion\": null,\n    \"custom\": [],\n    \"email\": null,\n    \"phone\": null,\n    \"gender\": null,\n    \"idfa\": null,\n    \"username\": null,\n    \"locale\": \"en_GB\",\n    \"ip\": \"203.0.113.7\",\n    \"createdAt\": null,\n    \"ipCountry\": null,\n    \"appleConsumptionConsent\": null,\n    \"birthday\": null,\n    \"age\": null,\n    \"totalRevenueUsd\": 0,\n    \"updatedDate\": \"2026-09-09T10:30:00.000Z\",\n    \"accessLevels\": {},\n    \"subscriptions\": {},\n    \"nonSubscriptions\": {}\n  }\n}"},{"id":"728b9490-1bc1-4331-82e8-2e2e53b5061c","name":"404 Not Found: customerUserId not found","originalRequest":{"method":"PATCH","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"customerUserId\": \"user-123\",\n  \"appVersion\": \"1.1.0\",\n  \"os\": \"iOS 17.4\",\n  \"locale\": \"en_GB\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles"},"status":"Not Found","code":404,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Profile not found with provided customerUserId user-123 appId: 4021\",\n  \"status\": 404\n}"},{"id":"54c462c1-5baa-4c55-b7f2-3fcc54d9c64f","name":"400 Bad Request: Missing appVersion","originalRequest":{"method":"PATCH","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"customerUserId\": \"user-123\",\n  \"locale\": \"en_GB\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"appVersion must be a string\",\n  \"status\": 400\n}"}],"_postman_id":"9c2ef445-4088-4ab0-accf-2510e5349adc"},{"name":"Add Custom Attribute","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Added attribute has an ID and the submitted values', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw)).custom;","        pm.expect(body.data.id).to.be.a('string').and.not.empty;","        pm.expect(body.data.key).to.eql(sent.key);","        pm.expect(body.data.value).to.eql(sent.value);","    });","    if (body.ok && body.data && typeof body.data.id === 'string') {","        pm.collectionVariables.set('customAttributeId', body.data.id);","    }","}"],"id":"49ff6f1a-2535-46c5-978c-39e55d51e671"}}],"id":"5544eb72-f43d-46f1-a719-9410f9ceb00f","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"custom\": {\n    \"key\": \"user_level\",\n    \"value\": \"premium\"\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles/custom-attributes","description":"<p>Add one custom attribute to a profile. Attributes store extra information about the user and can help Botsi's AI Pricing models. To add several at once, use <strong>Add Custom Attributes (bulk)</strong>.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<ul>\n<li><code>profileId</code> or <code>customerUserId</code>.</li>\n<li><code>custom</code>: an object with <code>key</code> and <code>value</code>. Both must be strings.</li>\n</ul>\n<h2 id=\"how-it-works\">How it works</h2>\n<p>If you send both user IDs, Botsi tries <code>profileId</code> first. If that profile is not found, it tries <code>customerUserId</code>. Sending neither returns <code>400</code>.</p>\n<p>Adding the same key twice creates two separate attributes with different IDs. To change an existing value, use <strong>Update Custom Attribute</strong> with the ID returned when the attribute was added.</p>\n<h2 id=\"response\">Response</h2>\n<p>Returns the attribute with its new <code>id</code>. The collection script saves it as <code>customAttributeId</code> for the update request.</p>\n<p><strong>Profile ownership:</strong> Use a profile from the app identified by your secret key. This v1 request does not check app ownership when using <code>profileId</code>. A <code>customerUserId</code> lookup is limited to your app.</p>\n<p><strong>Source:</strong> api/src/web-api/profiles/profiles.controller.ts, dto/create-custom-attribute-body.dto.ts, profiles.docs.ts</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","profiles","custom-attributes"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"6b21b3cf-4458-41ac-8583-d776a20d431e","name":"201 Created","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"custom\": {\n    \"key\": \"user_level\",\n    \"value\": \"premium\"\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles/custom-attributes"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"key\": \"user_level\",\n    \"value\": \"premium\",\n    \"id\": \"a1B2c3D4\"\n  }\n}"},{"id":"51660515-2ebc-491d-bdbe-027a5cfc924f","name":"404 Not Found: Profile not found","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"custom\": {\n    \"key\": \"user_level\",\n    \"value\": \"premium\"\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles/custom-attributes"},"status":"Not Found","code":404,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Profile not found while getting profileId for customer attributes with provided profileId 0072102a-c00c-4ea5-9271-1b6e975f2d63 appId: 4021\",\n  \"status\": 404\n}"},{"id":"6a847e66-718e-4086-a3a9-287b0ea6e6fa","name":"400 Bad Request: Missing user ID","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"custom\": {\n    \"key\": \"user_level\",\n    \"value\": \"premium\"\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles/custom-attributes"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Either profileId or customerUserId must be provided\",\n  \"status\": 400\n}"}],"_postman_id":"5544eb72-f43d-46f1-a719-9410f9ceb00f"},{"name":"Add Custom Attributes (bulk)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('All submitted attributes appear in the returned array', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw)).custom;","        pm.expect(body.data).to.be.an('array');","        for (const attribute of sent) {","            const stored = body.data.find(value => value.key === attribute.key && value.value === attribute.value);","            pm.expect(stored).to.not.equal(undefined);","            pm.expect(stored.id).to.be.a('string').and.not.empty;","        }","    });","}"],"id":"f2e512ea-4fe8-4877-8664-cd9c26d11185"}}],"id":"7ac43093-de8e-454a-861a-2b7a4a6b01a4","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"custom\": [\n    {\n      \"key\": \"subscription_type\",\n      \"value\": \"annual\"\n    },\n    {\n      \"key\": \"referral_source\",\n      \"value\": \"facebook\"\n    }\n  ]\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles/custom-attributes-all","description":"<p>Add several custom attributes to a profile in one request.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<ul>\n<li><code>profileId</code> or <code>customerUserId</code>.</li>\n<li><code>custom</code>: an array with at least one <code>{ \"key\": \"...\", \"value\": \"...\" }</code> object. Both values must be strings.</li>\n</ul>\n<p>An empty <code>custom</code> array returns <code>400</code>.</p>\n<h2 id=\"response\">Response</h2>\n<p>Returns the profile's complete <code>custom</code> array, including attributes added earlier. Each new attribute gets its own ID. Existing attributes with the same key are kept, so duplicate keys are possible.</p>\n<p>The related v2 request, <code>POST /v2/custom-attributes</code>, returns only the attributes added by that request.</p>\n<p><strong>Profile ownership:</strong> Use a profile from the app identified by your secret key. This v1 request does not check app ownership when using <code>profileId</code>. A <code>customerUserId</code> lookup is limited to your app.</p>\n<p><strong>Source:</strong> api/src/web-api/profiles/profiles.controller.ts, dto/create-custom-attributes-body.dto.ts, api/src/profiles/profiles.service.ts (addCustomAttributesToProfile)</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","profiles","custom-attributes-all"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"e3c67cb8-1c40-4409-9d31-54234eb42d2b","name":"201 Created: All custom attributes returned","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"custom\": [\n    {\n      \"key\": \"subscription_type\",\n      \"value\": \"annual\"\n    },\n    {\n      \"key\": \"referral_source\",\n      \"value\": \"facebook\"\n    }\n  ]\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles/custom-attributes-all"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": [\n    {\n      \"key\": \"user_level\",\n      \"value\": \"premium\",\n      \"id\": \"a1B2c3D4\"\n    },\n    {\n      \"key\": \"subscription_type\",\n      \"value\": \"annual\",\n      \"id\": \"e5F6g7H8\"\n    },\n    {\n      \"key\": \"referral_source\",\n      \"value\": \"facebook\",\n      \"id\": \"i9J0k1L2\"\n    }\n  ]\n}"},{"id":"30ae8cb5-a551-4291-9235-0fe6c1181925","name":"400 Bad Request: Empty custom array","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"custom\": []\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles/custom-attributes-all"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"custom must contain at least 1 elements\",\n  \"status\": 400\n}"}],"_postman_id":"7ac43093-de8e-454a-861a-2b7a4a6b01a4"},{"name":"Update Custom Attribute","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 200', () => pm.expect(pm.response.code).to.eql(200));","if (pm.response.code === 200) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Response contains the submitted key, value, and attribute ID', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw));","        pm.expect(body.data).to.deep.equal({ key: sent.custom.key, value: sent.custom.value, id: sent.attrId });","    });","    // The API echoes these values even when the attribute ID was not found.","}"],"id":"d9e57985-917a-4bac-aeda-f82972f1351b"}}],"id":"98edf15f-dc88-4699-a2f7-71a65ab83703","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"attrId\": \"\",\n  \"custom\": {\n    \"key\": \"user_level\",\n    \"value\": \"vip\"\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles/custom-attributes","description":"<p>Change an existing custom attribute's key, value, or both. For example, change a user's plan from <code>premium</code> to <code>vip</code>.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<ul>\n<li><code>profileId</code> or <code>customerUserId</code>.</li>\n<li><code>attrId</code>: the attribute ID returned by an add request.</li>\n<li><code>custom</code>: an object containing both <code>key</code> and <code>value</code>.</li>\n</ul>\n<p>Both the key and value are saved, so this request can also rename an attribute. v1 finds the attribute by ID; v2 uses its key.</p>\n<h2 id=\"response\">Response</h2>\n<p>Returns <code>200</code> with <code>{ \"ok\": true, \"data\": { \"key\": \"...\", \"value\": \"...\", \"id\": \"...\" } }</code>. The <code>id</code> is the supplied <code>attrId</code>. This response is returned even if no attribute matches that ID; in that case, no attribute value changes.</p>\n<p>To confirm the update, read the profile with <strong>Update a Profile</strong> (<code>PATCH /profiles</code>) and check its custom attributes. v2 instead returns <code>404</code> for missing keys.</p>\n<p><strong>Profile ownership:</strong> Use a profile from the app identified by your secret key. This v1 request does not check app ownership when using <code>profileId</code>. A <code>customerUserId</code> lookup is limited to your app.</p>\n<p><strong>Source:</strong> api/src/web-api/profiles/profiles.controller.ts, dto/update-custom-attribute-body.dto.ts, api/src/repositories/profiles/profiles-repository.service.ts (updateCustomAttr)</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","profiles","custom-attributes"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"6a3e1a83-2c87-463b-90ba-c09a7ffcf92f","name":"200 OK","originalRequest":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"attrId\": \"a1B2c3D4\",\n  \"custom\": {\n    \"key\": \"user_level\",\n    \"value\": \"vip\"\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles/custom-attributes"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"key\": \"user_level\",\n    \"value\": \"vip\",\n    \"id\": \"a1B2c3D4\"\n  }\n}"},{"id":"9f7b4913-58e7-41fa-8cee-ad92efd7ee59","name":"400 Bad Request: Missing attrId","originalRequest":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"custom\": {\n    \"key\": \"user_level\",\n    \"value\": \"vip\"\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/profiles/custom-attributes"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"attrId must be a string\",\n  \"status\": 400\n}"}],"_postman_id":"98edf15f-dc88-4699-a2f7-71a65ab83703"}],"id":"ca587bbe-9d63-49e3-a7ca-7ca32f915627","description":"<p>Create and update the profile Botsi keeps for each user. Add custom attributes to store more information about the user. Create a profile before using the other requests in this collection.</p>\n<p>v1 uses <code>customerUserId</code> to identify your user. v2 calls this field <code>appUserId</code>. In v1, you can add one attribute, add several at once, or update an attribute by its ID.</p>\n<p><strong>Source:</strong> api/src/web-api/profiles/</p>\n","_postman_id":"ca587bbe-9d63-49e3-a7ca-7ca32f915627","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}}},{"name":"2. Paywalls","item":[{"name":"Fetch Paywall","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Paywall includes the fields used by later requests', () => {","        pm.expect(body.data.id).to.be.a('number');","        pm.expect(body.data.paywallSessionId).to.be.a('string').and.not.empty;","        pm.expect(body.data.isExperiment).to.be.a('boolean');","        pm.expect(body.data.placementId).to.be.a('string');","        pm.expect(body.data.sourceProducts).to.be.an('array');","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw));","        pm.expect(body.data.placementId).to.eql(sent.placementId);","    });","    pm.test('Paywall does not include both A/B and AI model IDs', () => {","        pm.expect(body.data.abTestId !== undefined && body.data.aiPricingModelId !== undefined).to.eql(false);","    });","    pm.test('Product fields match the v1 response format', () => {","        for (const product of body.data.sourceProducts) {","            pm.expect(product.botsiProductId).to.be.a('number');","            pm.expect(product.isConsumable).to.be.a('boolean');","            if (product.sourceProductId !== undefined && product.sourceProductId !== null) {","                pm.expect(product.sourceProductId).to.be.a('string');","            }","            pm.expect(product.sourcePoductId).to.eql(product.sourceProductId);","        }","    });","    if (body.ok && body.data && typeof body.data.paywallSessionId === 'string') {","        const data = body.data;","        pm.collectionVariables.set('paywallSessionId', data.paywallSessionId);","        pm.collectionVariables.set('paywallId', String(data.id));","        pm.collectionVariables.set('abTestId', String(data.abTestId === undefined ? 0 : data.abTestId));","        pm.collectionVariables.set('aiPricingModelId', String(data.aiPricingModelId === undefined ? 0 : data.aiPricingModelId));","        pm.collectionVariables.set('isExperiment', String(data.isExperiment));","    }","}"],"id":"69d0f3ce-9520-4c26-b015-aac5d349acb0"}}],"id":"a5d86632-a057-45b6-b47b-f11b7fc27182","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"placementId\": \"\",\n  \"profileId\": \"\",\n  \"store\": \"app_store\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/paywalls","description":"<p>Get the paywall to display for a user at a placement. Create the user's profile first. Botsi selects a paywall you configured in the dashboard, including any A/B test or AI Pricing selection.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<ul>\n<li><code>placementId</code>.</li>\n<li><code>store</code>: <code>app_store</code>, <code>play_store</code>, <code>stripe</code>, <code>web2wave</code>, or <code>custom</code>.</li>\n<li><code>profileId</code> or <code>customerUserId</code>. If both are sent, <code>profileId</code> is used. Sending neither returns <code>400</code>.</li>\n</ul>\n<p><code>store</code> is required in v1 and determines which product fields are returned. It does not change the paywall selection. An A/B test with an unchanged test salt and variant configuration selects consistently for the same profile. AI traffic allocation and pricing decisions can change between requests. Save the details of the paywall actually shown and use those details when reporting its view and purchase.</p>\n<h2 id=\"optional-fields\">Optional fields</h2>\n<p><code>languageLocale</code>, <code>deviceTypeModel</code>, <code>osVersion</code>, and <code>attributionSource</code>.</p>\n<p>Each must be a non-empty string when included. These fields are accepted but are not used by the v1 paywall selection code. AI Pricing reads device, OS, locale, country, and custom attributes from the stored profile. Update the profile to change those values.</p>\n<h2 id=\"response-fields\">Response fields</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>id</code></td>\n<td>Paywall ID. Use it as <code>paywallId</code> in requests that send separate paywall fields.</td>\n</tr>\n<tr>\n<td><code>externalId</code>, <code>name</code>, <code>remoteConfigs</code>, <code>revision</code></td>\n<td>Paywall settings from the dashboard.</td>\n</tr>\n<tr>\n<td><code>abTestId</code>, <code>abTestName</code></td>\n<td>Included only for an A/B test selection.</td>\n</tr>\n<tr>\n<td><code>aiPricingModelId</code></td>\n<td>Included only for an AI Pricing selection. A response never includes both this ID and an A/B test ID.</td>\n</tr>\n<tr>\n<td><code>isExperiment</code></td>\n<td>Always included. <code>true</code> when the AI model selected the paywall; <code>false</code> for its default version and for other selection types.</td>\n</tr>\n<tr>\n<td><code>placementId</code></td>\n<td>The placement ID sent in the request.</td>\n</tr>\n<tr>\n<td><code>paywallSessionId</code></td>\n<td>Always included. A protected token that links later events to this selection. Save it and send it as returned.</td>\n</tr>\n<tr>\n<td><code>sourceProducts</code></td>\n<td>An array of products attached to the paywall. Its fields depend on <code>store</code>.</td>\n</tr>\n</tbody>\n</table>\n</div><h2 id=\"product-fields-by-store\">Product fields by store</h2>\n<p>Each product includes <code>botsiProductId</code> and <code>isConsumable</code>. <code>sourceProductId</code> and its misspelled duplicate, <code>sourcePoductId</code>, contain the store product ID when available. These ID fields can be null or absent. The spelling <code>sourcePoductId</code> reflects the API response.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Store</th>\n<th>Additional fields</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>app_store</code></td>\n<td>Uses the App Store product ID. Includes <code>promotionalOfferId</code> and <code>winBackOfferId</code>, which are null when not applicable. <code>appStoreOfferType</code> comes from this paywall's offer and may be absent.</td>\n</tr>\n<tr>\n<td><code>play_store</code></td>\n<td><code>basePlanId</code>, <code>offerIds</code> (the offers this paywall may show), and <code>offerId</code> (this product's offer).</td>\n</tr>\n<tr>\n<td><code>stripe</code>, <code>web2wave</code>, <code>custom</code></td>\n<td>All three use the Stripe product ID, <code>sourcePriceId</code>, and <code>trialPeriodDays</code> in v1. Values depend on the product's Stripe settings; choosing <code>web2wave</code> or <code>custom</code> does not force them to null.</td>\n</tr>\n</tbody>\n</table>\n</div><p>Only entries with no linked product are left out. A product without an ID for the requested store can still appear. Check that the returned ID is usable before offering the product for purchase.</p>\n<h2 id=\"when-to-call\">When to call</h2>\n<p>Fetch the paywall early, such as just after creating the profile, and save the response before the user reaches the paywall. Response time varies with the pricing service and other dependencies. This API does not guarantee a 2-second response limit.</p>\n<p>If the request fails with a <code>5xx</code> error, show your default paywall instead of making the user wait for retries.</p>\n<h2 id=\"saved-variables\">Saved variables</h2>\n<p>The collection script saves <code>paywallSessionId</code>, <code>id</code> as <code>paywallId</code>, <code>abTestId</code>, <code>aiPricingModelId</code>, and <code>isExperiment</code> for event and purchase requests. Missing A/B test and AI model IDs are saved as <code>0</code>.</p>\n<p><strong>Profile ownership:</strong> Use a profile from the app identified by your secret key. This v1 request does not check app ownership when using <code>profileId</code>. A <code>customerUserId</code> lookup is limited to your app.</p>\n<p><strong>Source:</strong> api/src/web-api/paywalls/paywalls.controller.ts, api/src/web-api/ai-pricing/dto/ai-pricing-model.dto.ts, api/src/sdk/paywalls/dto/response/botsi-paywall.dto.ts, api/web-api-ab-testing.md</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","paywalls"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"a8e16e5e-32a0-488b-8395-d28e79cdf0b8","name":"201 Created: AI Pricing paywall (App Store)","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"placementId\": \"ai-placement-id\",\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"store\": \"app_store\",\n  \"languageLocale\": \"en-US\",\n  \"deviceTypeModel\": \"iPhone 15 Pro Max\",\n  \"osVersion\": \"iOS 17.0\",\n  \"attributionSource\": \"organic\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/paywalls"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"aiPricingModelId\": 32,\n    \"id\": 42,\n    \"externalId\": \"paywall_premium_v2\",\n    \"name\": \"Premium Paywall\",\n    \"remoteConfigs\": null,\n    \"revision\": 3,\n    \"sourceProducts\": [\n      {\n        \"botsiProductId\": 1201,\n        \"isConsumable\": false,\n        \"sourceProductId\": \"com.example.premium.monthly\",\n        \"sourcePoductId\": \"com.example.premium.monthly\",\n        \"appStoreOfferType\": \"introductory\",\n        \"promotionalOfferId\": null,\n        \"winBackOfferId\": null\n      }\n    ],\n    \"placementId\": \"ai-placement-id\",\n    \"isExperiment\": true,\n    \"paywallSessionId\": \"v1.eyJhIjo0MDIxLCJ3Ijo0MiwiaSI6MTc4ODk1NTIwMCwicCI6ImFpLXBsYWNlbWVudC1pZCIsIngiOjEsInUiOiIwMDcyMTAyYS1jMDBjLTRlYTUtOTI3MS0xYjZlOTc1ZjJkNjMiLCJtIjozMn0.6m9pLEKJ1BnbQ6AFCDgN-0BXc0o8n3n5yQrPOS-jaPQ\"\n  }\n}"},{"id":"e1e6aa33-dd9f-4693-b3c0-7c9d8bdf2aa1","name":"201 Created: A/B test paywall (Google Play)","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"placementId\": \"onboarding_pricing_framing\",\n  \"customerUserId\": \"user-123\",\n  \"store\": \"play_store\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/paywalls"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"abTestId\": 77,\n    \"abTestName\": \"Annual vs Lifetime Hero Framing\",\n    \"id\": 905,\n    \"externalId\": \"paywall_premium_v2\",\n    \"name\": \"Variant B\",\n    \"remoteConfigs\": null,\n    \"revision\": 3,\n    \"sourceProducts\": [\n      {\n        \"botsiProductId\": 1201,\n        \"sourceProductId\": \"premium_monthly\",\n        \"sourcePoductId\": \"premium_monthly\",\n        \"basePlanId\": \"monthly-base\",\n        \"offerIds\": [\n          \"freetrial-7d\"\n        ],\n        \"offerId\": null,\n        \"isConsumable\": false\n      }\n    ],\n    \"placementId\": \"onboarding_pricing_framing\",\n    \"isExperiment\": false,\n    \"paywallSessionId\": \"v1.eyJhIjo0MDIxLCJ3Ijo5MDUsImkiOjE3ODg5NTUyMDAsInAiOiJvbmJvYXJkaW5nX3ByaWNpbmdfZnJhbWluZyIsIngiOjAsInUiOiIwMDcyMTAyYS1jMDBjLTRlYTUtOTI3MS0xYjZlOTc1ZjJkNjMiLCJ0Ijo3N30.N6_lkU9P1qSHPrYz4Kwhnekj1pQiFPB-dxgqUsrfB64\"\n  }\n}"},{"id":"287182e0-c21c-474b-b11f-6735eea40349","name":"201 Created: Standard paywall (Stripe)","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"placementId\": \"ai-placement-id\",\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"store\": \"stripe\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/paywalls"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"id\": 42,\n    \"externalId\": \"paywall_premium_v2\",\n    \"name\": \"Premium Paywall\",\n    \"remoteConfigs\": null,\n    \"revision\": 3,\n    \"sourceProducts\": [\n      {\n        \"botsiProductId\": 1201,\n        \"sourceProductId\": \"prod_QxA1b2C3\",\n        \"sourcePoductId\": \"prod_QxA1b2C3\",\n        \"sourcePriceId\": \"price_1OaBcMonthly\",\n        \"trialPeriodDays\": 7,\n        \"isConsumable\": false\n      }\n    ],\n    \"placementId\": \"ai-placement-id\",\n    \"isExperiment\": false,\n    \"paywallSessionId\": \"v1.eyJhIjo0MDIxLCJ3Ijo0MiwiaSI6MTc4ODk1NTIwMCwicCI6ImFpLXBsYWNlbWVudC1pZCIsIngiOjAsInUiOiIwMDcyMTAyYS1jMDBjLTRlYTUtOTI3MS0xYjZlOTc1ZjJkNjMifQ.x7XHorYc1uUdM0INp0EUtWFaBjg1kxvpEj7xn_r6UKM\"\n  }\n}"},{"id":"e5e7b84f-0135-4e89-a458-bbd12b9b46e9","name":"404 Not Found: Placement missing or no paywall configured","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"placementId\": \"main-onboarding\",\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"store\": \"app_store\",\n  \"languageLocale\": \"en-US\",\n  \"deviceTypeModel\": \"iPhone 15 Pro Max\",\n  \"osVersion\": \"iOS 17.0\",\n  \"attributionSource\": \"organic\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/paywalls"},"status":"Not Found","code":404,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"No 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.\",\n  \"status\": 404\n}"},{"id":"7702b819-cfc6-40d7-ab2a-7a4db835e654","name":"400 Bad Request: Missing user ID","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"placementId\": \"ai-placement-id\",\n  \"store\": \"app_store\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/paywalls"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"No profileId or customerUserId provided.\",\n  \"status\": 400\n}"},{"id":"1a18760a-d623-4964-99c1-7e85e023b4b3","name":"500 Internal Server Error: Show your default paywall","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"placementId\": \"ai-placement-id\",\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"store\": \"app_store\",\n  \"languageLocale\": \"en-US\",\n  \"deviceTypeModel\": \"iPhone 15 Pro Max\",\n  \"osVersion\": \"iOS 17.0\",\n  \"attributionSource\": \"organic\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/paywalls"},"status":"Internal Server Error","code":500,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Server Error.\",\n  \"status\": 500\n}"},{"id":"9d239bca-11c4-4322-9140-3c0c6352b597","name":"201 Created: Product has no App Store ID","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"placementId\": \"ai-placement-id\",\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"store\": \"app_store\",\n  \"languageLocale\": \"en-US\",\n  \"deviceTypeModel\": \"iPhone 15 Pro Max\",\n  \"osVersion\": \"iOS 17.0\",\n  \"attributionSource\": \"organic\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/paywalls"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"aiPricingModelId\": 32,\n    \"id\": 42,\n    \"externalId\": \"paywall_premium_v2\",\n    \"name\": \"Premium Paywall\",\n    \"remoteConfigs\": null,\n    \"revision\": 3,\n    \"sourceProducts\": [\n      {\n        \"botsiProductId\": 102,\n        \"isConsumable\": false,\n        \"sourceProductId\": null,\n        \"sourcePoductId\": null,\n        \"promotionalOfferId\": null,\n        \"winBackOfferId\": null\n      }\n    ],\n    \"placementId\": \"ai-placement-id\",\n    \"isExperiment\": true,\n    \"paywallSessionId\": \"v1.eyJhIjo0MDIxLCJ3Ijo0MiwiaSI6MTc4ODk1NTIwMCwicCI6ImFpLXBsYWNlbWVudC1pZCIsIngiOjEsInUiOiIwMDcyMTAyYS1jMDBjLTRlYTUtOTI3MS0xYjZlOTc1ZjJkNjMiLCJtIjozMn0.6m9pLEKJ1BnbQ6AFCDgN-0BXc0o8n3n5yQrPOS-jaPQ\"\n  }\n}"}],"_postman_id":"a5d86632-a057-45b6-b47b-f11b7fc27182"}],"id":"32ec7670-7c7d-4a8c-8460-c3ed6949e039","description":"<p>Get the paywall selected for a user at a placement. It may be a standard paywall, an A/B test version, or an AI Pricing selection.</p>\n<p>The response includes <code>paywallSessionId</code>. Send this value when reporting a paywall view so Botsi can link the view to the selected paywall.</p>\n<p><strong>Source:</strong> api/src/web-api/paywalls/, api/web-api-ab-testing.md</p>\n","_postman_id":"32ec7670-7c7d-4a8c-8460-c3ed6949e039","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}}},{"name":"3. Events","item":[{"name":"Send Paywall Shown Event","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Event response contains no data field', () => pm.expect(body).to.not.have.property('data'));","","    pm.test('The view request includes a session token', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw))[0];","        pm.expect(sent.paywallSessionId).to.be.a('string').and.not.empty;","    });","}"],"id":"0ddcae04-77f0-4c74-ba88-34fd637d38d5"}}],"id":"22a2d099-5427-42b7-a86e-a63990bcc222","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"profileId\": \"\",\n    \"eventType\": \"paywall_shown\",\n    \"paywallSessionId\": \"\"\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events","description":"<p>Send this event immediately after the paywall appears. Botsi uses it to count views and compare them with purchases. Fetching a paywall does not count as a view.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<p>Each event needs just three fields when using a token:</p>\n<ul>\n<li><code>profileId</code> or <code>customerUserId</code>.</li>\n<li><code>eventType</code>: <code>paywall_shown</code>.</li>\n<li><code>paywallSessionId</code>: the value returned by <strong>Fetch Paywall</strong>.</li>\n</ul>\n<p>The token supplies <code>paywallId</code>, <code>placementId</code>, <code>abTestId</code>, <code>aiPricingModelId</code>, and <code>isExperiment</code>, so you do not need to send those fields separately.</p>\n<h2 id=\"token-rules\">Token rules</h2>\n<ul>\n<li>For fields present in the token, Botsi saves the token's values and records any mismatch with separately sent values. Separate values for fields absent from the token are not cleared. Send only the token and required event fields to avoid carrying unrelated test details.</li>\n<li>An invalid token, a token whose signature cannot be verified, or a token from another app returns <code>400</code>. No events are saved. A token signed for another app normally returns the generic invalid-token message because the signature check fails first. An outdated <code>abTestId</code> sent as a separate field is instead accepted as sent.</li>\n<li>For <code>paywall_shown</code>, tokens are valid for 24 hours after Fetch Paywall creates them by default. After that, fetch a new paywall before reporting the view. Other event types accept tokens of any age.</li>\n</ul>\n<p><strong>Send one event per view.</strong> Sending the same event twice counts two views.</p>\n<h2 id=\"sending-events\">Sending events</h2>\n<ul>\n<li>Send a JSON array of event objects. An empty array or a single object returns <code>400</code>.</li>\n<li>Every event needs <code>profileId</code> or <code>customerUserId</code>. Use the same identifier field and value throughout the request. The API compares <code>profileId</code> when present, otherwise <code>customerUserId</code>; it does not resolve each event separately before comparing. Different identifier values return <code>400</code>; an unknown <code>customerUserId</code> returns <code>404</code>.</li>\n<li>Each event is checked separately. Field errors use <code>Event[&lt;index&gt;].&lt;field&gt;: &lt;rule&gt;</code>.</li>\n<li><code>timestamp</code> is optional. Use milliseconds since January 1, 1970 (Unix time). If omitted, Botsi uses the current time.</li>\n<li>Botsi gets <code>country</code> and <code>store</code> from the profile, not the event body.</li>\n<li>Events record analytics only. To grant or update a user's access, validate the store purchase using a request in folder 4.</li>\n</ul>\n<p><strong>Profile ownership:</strong> Use a profile from the app identified by your secret key. This v1 request does not check app ownership when using <code>profileId</code>. A <code>customerUserId</code> lookup is limited to your app.</p>\n<p><strong>Source:</strong> api/src/web-api/events/events.controller.ts, events.service.ts, pipes/validate-events-array.pipe.ts, api/src/web-api/paywall-session/paywall-session-resolver.service.ts, api/web-api-events-confluence.md</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","events"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"55e840a1-0753-4ba8-82c5-f629ba2ff34a","name":"201 Created","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"eventType\": \"paywall_shown\",\n    \"paywallSessionId\": \"v1.eyJhIjo0MDIxLCJ3Ijo0MiwiaSI6MTc4ODk1NTIwMCwicCI6ImFpLXBsYWNlbWVudC1pZCIsIngiOjEsInUiOiIwMDcyMTAyYS1jMDBjLTRlYTUtOTI3MS0xYjZlOTc1ZjJkNjMiLCJtIjozMn0.6m9pLEKJ1BnbQ6AFCDgN-0BXc0o8n3n5yQrPOS-jaPQ\"\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true\n}"},{"id":"50bde34a-dd3f-4073-b036-be44119aec7a","name":"400 Bad Request: Invalid token","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"eventType\": \"paywall_shown\",\n    \"paywallSessionId\": \"invalid-token\"\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"paywallSessionId is not a valid Botsi paywall session token.\",\n  \"status\": 400\n}"},{"id":"f8e7208e-228d-44e5-8555-230b2235621a","name":"400 Bad Request: Expired token","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"eventType\": \"paywall_shown\",\n    \"paywallSessionId\": \"v1.eyJhIjo0MDIxLCJ3Ijo0MiwiaSI6MTc4ODc4MjQwMCwicCI6ImFpLXBsYWNlbWVudC1pZCIsIngiOjEsInUiOiIwMDcyMTAyYS1jMDBjLTRlYTUtOTI3MS0xYjZlOTc1ZjJkNjMiLCJtIjozMn0.fO_Hc2hfjYQQAcNNOKbXOaUWmsG71bU12OZgKqiy0BA\"\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"paywallSessionId has expired for this event type. Fetch a new paywall before reporting the view.\",\n  \"status\": 400\n}"},{"id":"fbc720bd-a2a0-4849-aff1-e837242312ce","name":"400 Bad Request: Token belongs to another app","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"eventType\": \"paywall_shown\",\n    \"paywallSessionId\": \"v1.eyJhIjo0MDIyLCJ3Ijo0MiwiaSI6MTc4ODk1NTIwMCwicCI6ImFpLXBsYWNlbWVudC1pZCIsIngiOjEsInUiOiIwMDcyMTAyYS1jMDBjLTRlYTUtOTI3MS0xYjZlOTc1ZjJkNjMiLCJtIjozMn0.9HYZJlDXrsDNiIJ2-e-lCc01jEElKODCezXvTX7_raU\"\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"paywallSessionId is not a valid Botsi paywall session token.\",\n  \"status\": 400\n}"},{"id":"7b314206-dddc-49bd-9ba3-6748d942f45e","name":"400 Bad Request: Body must be an array","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"eventType\": \"paywall_shown\",\n  \"paywallSessionId\": \"v1.eyJhIjo0MDIxLCJ3Ijo0MiwicCI6ImFpLXBsYWNlbWVudC1pZCIsIm0iOjMyLCJ4IjoxLCJ1IjoiMDA3MjEwMmEtYzAwYy00ZWE1LTkyNzEtMWI2ZTk3NWYyZDYzIiwiaSI6MTc4ODk1NTIwMH0.QmzR1w\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Request body must be an array\",\n  \"status\": 400\n}"},{"id":"ca1208d1-b57d-401a-b653-de4d72f5a4ad","name":"404 Not Found: customerUserId not found","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"customerUserId\": \"user-999\",\n    \"eventType\": \"paywall_shown\",\n    \"paywallSessionId\": \"v1.eyJhIjo0MDIxLCJ3Ijo0MiwiaSI6MTc4ODk1NTIwMCwicCI6ImFpLXBsYWNlbWVudC1pZCIsIngiOjEsInUiOiIwMDcyMTAyYS1jMDBjLTRlYTUtOTI3MS0xYjZlOTc1ZjJkNjMiLCJtIjozMn0.6m9pLEKJ1BnbQ6AFCDgN-0BXc0o8n3n5yQrPOS-jaPQ\"\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events"},"status":"Not Found","code":404,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Profile not found while getting profileId with provided customerUserId user-999 appId: 4021\",\n  \"status\": 404\n}"}],"_postman_id":"22a2d099-5427-42b7-a86e-a63990bcc222"},{"name":"Send Paywall Shown Event (Explicit Fields)","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Event response contains no data field', () => pm.expect(body).to.not.have.property('data'));","","    pm.test('Submitted tracking fields have the expected types', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw))[0];","        for (const field of ['paywallId', 'abTestId', 'aiPricingModelId']) {","            if (sent[field] !== undefined) pm.expect(sent[field]).to.be.a('number');","        }","        pm.expect(sent.isExperiment).to.be.a('boolean');","    });","}"],"id":"e3dfb959-3269-4f80-bbb4-9eb22430fa19"}}],"id":"53b50392-113b-4a63-b414-188842473669","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"profileId\": \"\",\n    \"eventType\": \"paywall_shown\",\n    \"placementId\": \"\",\n    \"paywallId\": ,\n    \"abTestId\": ,\n    \"aiPricingModelId\": ,\n    \"isExperiment\": ,\n    \"timestamp\": 1789461786000\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events","description":"<p>Record a paywall view by sending the paywall and test fields separately. This older method is still supported but is deprecated. Use <strong>Send Paywall Shown Event</strong> with <code>paywallSessionId</code> for new integrations.</p>\n<p>Copy the values from Fetch Paywall carefully. Incorrect values are generally saved as sent and can link results to the wrong test.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<p>Without a token, every event type requires:</p>\n<ul>\n<li><code>profileId</code> or <code>customerUserId</code>.</li>\n<li><code>eventType</code>.</li>\n<li><code>aiPricingModelId</code>: a number. Use <code>0</code> if the response had no AI model ID.</li>\n<li><code>isExperiment</code>: <code>true</code> or <code>false</code>. Use <code>false</code> if the response had no AI Pricing selection.</li>\n</ul>\n<h2 id=\"recommended-tracking-fields\">Recommended tracking fields</h2>\n<p>For correct paywall tracking, also send <code>paywallId</code> and <code>placementId</code> on <code>paywall_shown</code>. The API accepts events without these fields, but the view may not be linked correctly. Include <code>abTestId</code> when Fetch Paywall returns an A/B test selection.</p>\n<p>Keep <code></code>, <code></code>, <code></code>, and <code></code> outside quotation marks in the request body. The first three must be numbers; the last must be <code>true</code> or <code>false</code>.</p>\n<h2 id=\"automatic-correction\">Automatic correction</h2>\n<p>For <code>paywall_shown</code>, Botsi changes <code>isExperiment: false</code> to <code>true</code> if its saved paywall selection shows that the AI model was used. No other fields are corrected, and this correction does not change the response.</p>\n<h2 id=\"sending-events\">Sending events</h2>\n<ul>\n<li>Send a JSON array of event objects. An empty array or a single object returns <code>400</code>.</li>\n<li>Every event needs <code>profileId</code> or <code>customerUserId</code>. Use the same identifier field and value throughout the request. The API compares <code>profileId</code> when present, otherwise <code>customerUserId</code>; it does not resolve each event separately before comparing. Different identifier values return <code>400</code>; an unknown <code>customerUserId</code> returns <code>404</code>.</li>\n<li>Each event is checked separately. Field errors use <code>Event[&lt;index&gt;].&lt;field&gt;: &lt;rule&gt;</code>.</li>\n<li><code>timestamp</code> is optional. Use milliseconds since January 1, 1970 (Unix time). If omitted, Botsi uses the current time.</li>\n<li>Botsi gets <code>country</code> and <code>store</code> from the profile, not the event body.</li>\n<li>Events record analytics only. To grant or update a user's access, validate the store purchase using a request in folder 4.</li>\n</ul>\n<p><strong>Profile ownership:</strong> Use a profile from the app identified by your secret key. This v1 request does not check app ownership when using <code>profileId</code>. A <code>customerUserId</code> lookup is limited to your app.</p>\n<p><strong>Source:</strong> api/web-api-ab-testing.md (Appendix: the explicit-field loop), api/src/analytics/types/events/create-profile-event.dto.ts</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","events"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"66b3d02d-b1b7-4bde-a99b-6f6138d9a8d8","name":"201 Created","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"eventType\": \"paywall_shown\",\n    \"placementId\": \"ai-placement-id\",\n    \"paywallId\": 42,\n    \"abTestId\": 0,\n    \"aiPricingModelId\": 32,\n    \"isExperiment\": true,\n    \"timestamp\": 1788955200000\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true\n}"},{"id":"b4e50cf6-7d80-40ac-a16e-d67b1305ae4c","name":"400 Bad Request: Missing paywall details and token","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"eventType\": \"paywall_shown\",\n    \"placementId\": \"ai-placement-id\",\n    \"paywallId\": 42\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Event[0].aiPricingModelId: aiPricingModelId must be a number conforming to the specified constraints, aiPricingModelId should not be null or undefined; Event[0].isExperiment: isExperiment must be a boolean value, isExperiment should not be null or undefined\",\n  \"status\": 400\n}"},{"id":"c9d1c2e1-306c-481f-96bb-85f2ce4eb09e","name":"400 Bad Request: Unsupported eventType","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"eventType\": \"paywall_viewed\",\n    \"aiPricingModelId\": 0,\n    \"isExperiment\": false\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Event[0].eventType: eventType must be one of the following values: profile_created, paywall_shown, trial_started, trial_renewal_cancelled, trial_renewal_reactivated, trial_expired, trial_converted, subscription_paid, subscription_expired, subscription_renewal_cancelled, subscription_renewal, subscription_renewal_reactivated, billing_issue_detected, grace_period, subscription_refunded, non_subscription_paid, non_subscription_refunded, refund_declined, consumption_request, subscription_paused, subscription_deferred, did_change_renewal_pref, monitoring\",\n  \"status\": 400\n}"},{"id":"3ced2de2-357d-45be-bd38-2a6181936c24","name":"400 Bad Request: Events must identify the same user","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"eventType\": \"paywall_shown\",\n    \"placementId\": \"ai-placement-id\",\n    \"paywallId\": 42,\n    \"aiPricingModelId\": 0,\n    \"isExperiment\": false\n  },\n  {\n    \"customerUserId\": \"user-456\",\n    \"eventType\": \"paywall_shown\",\n    \"placementId\": \"ai-placement-id\",\n    \"paywallId\": 42,\n    \"aiPricingModelId\": 0,\n    \"isExperiment\": false\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/events"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"All events must have the same profileId or customerUserId\",\n  \"status\": 400\n}"}],"_postman_id":"53b50392-113b-4a63-b414-188842473669"}],"id":"73c53e43-5f81-478c-af7c-23300d5d3d59","description":"<p>Record paywall views so Botsi can compare views with purchases and improve AI Pricing selections.</p>\n<p>v1 accepts an array of events for one user in each request. v2 accepts one <code>paywall_shown</code> object.</p>\n<p><strong>Source:</strong> api/src/web-api/events/, api/web-api-events-confluence.md</p>\n","_postman_id":"73c53e43-5f81-478c-af7c-23300d5d3d59","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}}},{"name":"4. Validate Purchases","item":[{"name":"Validate Apple Store Purchase","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Response includes the current profile and access objects', () => {","        pm.expect(body.data.profileId).to.be.a('string').and.not.empty;","        pm.expect(body.data).to.have.property('customerUserId');","        pm.expect(body.data.accessLevels).to.be.an('object');","        pm.expect(body.data.subscriptions).to.be.an('object');","        pm.expect(body.data.nonSubscriptions).to.deep.equal({});","    });","","    pm.test('Submitted purchase tracking fields have the expected types', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw));","        pm.expect(sent.aiPricingModelId).to.be.a('number');","        pm.expect(sent.isExperiment).to.be.a('boolean');","        for (const field of ['paywallId', 'abTestId']) {","            if (sent[field] !== undefined) pm.expect(sent[field]).to.be.a('number');","        }","    });","    // These checks do not prove that the purchase was processed or access was granted.","","    pm.test('Apple purchase source is supported', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw));","        pm.expect(sent.source).to.be.oneOf(['purchasing', 'restore', 'observing']);","    });","}"],"id":"0b40eee8-8369-41a8-b891-8a06b37442ce"}}],"id":"364c4122-2857-4c9c-9db1-4598b95c453f","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"productId\": \"\",\n  \"transactionId\": \"\",\n  \"originalTransactionId\": \"\",\n  \"source\": \"purchasing\",\n  \"environment\": \"production\",\n  \"isSubscription\": true,\n  \"placementId\": \"\",\n  \"paywallId\": ,\n  \"abTestId\": ,\n  \"isExperiment\": ,\n  \"aiPricingModelId\": \n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/apple-store/validate","description":"<p>Validate a new App Store purchase promptly after completion. When the transaction is processed, Botsi records the purchase and updates access for the period confirmed by Apple. Some requests return successfully without processing the purchase; see Purchase handling below.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<p><code>profileId</code> or <code>customerUserId</code>, plus <code>productId</code>, <code>transactionId</code>, <code>originalTransactionId</code>, <code>source</code>, <code>isExperiment</code>, and <code>aiPricingModelId</code>.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Type</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>productId</code></td>\n<td>string</td>\n<td>The product ID from the purchase. It must match the App Store Product ID configured in Botsi.</td>\n</tr>\n<tr>\n<td><code>transactionId</code></td>\n<td>string</td>\n<td>The transaction ID from StoreKit.</td>\n</tr>\n<tr>\n<td><code>originalTransactionId</code></td>\n<td>string</td>\n<td>The original transaction ID from StoreKit.</td>\n</tr>\n<tr>\n<td><code>source</code></td>\n<td>string</td>\n<td><code>purchasing</code>, <code>restore</code>, or <code>observing</code>. Use <code>purchasing</code> for a new purchase.</td>\n</tr>\n<tr>\n<td><code>isExperiment</code></td>\n<td>boolean</td>\n<td>The value returned by Fetch Paywall.</td>\n</tr>\n<tr>\n<td><code>aiPricingModelId</code></td>\n<td>number</td>\n<td>The model ID returned by Fetch Paywall, or <code>0</code>.</td>\n</tr>\n</tbody>\n</table>\n</div><h2 id=\"optional-fields\">Optional fields</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Type</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>placementId</code></td>\n<td>string</td>\n<td>The placement used to fetch the paywall.</td>\n</tr>\n<tr>\n<td><code>paywallId</code></td>\n<td>number</td>\n<td><code>data.id</code> from Fetch Paywall.</td>\n</tr>\n<tr>\n<td><code>abTestId</code></td>\n<td>number</td>\n<td><code>data.abTestId</code> from an A/B test selection.</td>\n</tr>\n<tr>\n<td><code>isSubscription</code></td>\n<td>boolean</td>\n<td>Whether the purchase is a subscription.</td>\n</tr>\n<tr>\n<td><code>environment</code></td>\n<td>string</td>\n<td>Set <code>production</code> for production purchases. If omitted, Botsi tries both Apple purchase environments.</td>\n</tr>\n</tbody>\n</table>\n</div><p>Other optional fields: <code>sourceProductId</code>, <code>originalPrice</code>, <code>discountPrice</code>, <code>priceLocale</code>, <code>storeCountry</code>, <code>promotionalOfferId</code>, and <code>offer</code>.</p>\n<p>If you include <code>offer</code>, its fields are:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Accepted values</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>periodUnit</code></td>\n<td><code>day</code>, <code>week</code>, <code>month</code>, <code>year</code>, <code>unknown</code></td>\n</tr>\n<tr>\n<td><code>numberOfUnits</code></td>\n<td>The number of period units.</td>\n</tr>\n<tr>\n<td><code>type</code></td>\n<td><code>pay_as_you_go</code>, <code>pay_up_front</code>, <code>free_trial</code>, <code>unknown</code></td>\n</tr>\n<tr>\n<td><code>category</code></td>\n<td><code>introductory</code>, <code>promotional</code>, <code>code</code>, <code>win_back</code>, <code>unknown</code></td>\n</tr>\n</tbody>\n</table>\n</div><h2 id=\"purchase-handling\">Purchase handling</h2>\n<ul>\n<li><code>source: restore</code> skips purchase processing and returns <code>201</code> with the current profile after settings checks. Request logging still occurs. Use <strong>Restore Apple Store Purchases</strong> to restore earlier purchases.</li>\n<li>Processing is also skipped when the transaction is already recorded, a processing lock is held, or Apple's transaction purchase date is not the current UTC date. These paths still return <code>201</code> with the current profile.</li>\n<li><code>source: purchasing</code> determines how Botsi looks for an already recorded transaction. Revalidating a recorded transaction does not create another event.</li>\n<li>Do not treat <code>201</code> alone as proof of new access. Check the returned access status and expiry. For earlier purchases, use the restore workflow.</li>\n</ul>\n<h2 id=\"finding-the-storekit-ids\">Finding the StoreKit IDs</h2>\n<ul>\n<li><strong>StoreKit:</strong> get the transaction IDs from <code>SKPaymentTransaction</code>. The product ID is <code>SKPaymentTransaction.payment.productIdentifier</code>.</li>\n<li><strong>StoreKit 2:</strong> get all three IDs from the <code>Transaction</code> object.</li>\n</ul>\n<h2 id=\"linking-the-purchase-to-a-paywall\">Linking the purchase to a paywall</h2>\n<p>Purchase validation does not accept <code>paywallSessionId</code>. Copy <code>paywallId</code>, <code>isExperiment</code>, and <code>aiPricingModelId</code> from Fetch Paywall, and include <code>abTestId</code> for an A/B test selection.</p>\n<p>v1 requires <code>isExperiment</code> and <code>aiPricingModelId</code>. If there was no AI Pricing selection, send <code>false</code> and <code>0</code>, respectively. These fields are optional in v2.</p>\n<p>Keep <code></code>, <code></code>, <code></code>, and <code></code> outside quotation marks in the request body. The first three must be numbers; the last must be <code>true</code> or <code>false</code>.</p>\n<h2 id=\"response\">Response</h2>\n<p>Returns the current profile, including personal information. A successful response can contain unchanged access details. The <code>accessLevels</code> and <code>subscriptions</code> objects come from the profile's single linked access record. Both can be empty, and <code>nonSubscriptions</code> is always <code>{}</code>. They are not a complete purchase history. Check access status and expiry before enabling paid features. v2 returns fewer fields.</p>\n<p><strong>Prefer <code>customerUserId</code> when available.</strong> That lookup is limited to your app. Unlike v2, v1 does not check that a supplied <code>profileId</code> belongs to your app.</p>\n<p><strong>Source:</strong> api/src/web-api/purchases/purchases.controller.ts, dto/validate-token.dto.ts, api/src/sdk/purchases/dto/validate-token.dto.ts, apple-purchase.service.ts</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","purchases","apple-store","validate"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"6b393e42-d7d3-4447-99b5-0fd78e2e90c3","name":"201 Created: Purchase access granted","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"productId\": \"com.example.premium.monthly\",\n  \"transactionId\": \"2000000871590381\",\n  \"originalTransactionId\": \"2000000261699248\",\n  \"source\": \"purchasing\",\n  \"environment\": \"production\",\n  \"isSubscription\": true,\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/apple-store/validate"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"createdDate\": \"2026-09-09T10:30:00.000Z\",\n    \"linkedProfileIds\": null,\n    \"state\": \"subscribed\",\n    \"customerUserId\": \"user-123\",\n    \"migrationProfileId\": null,\n    \"country\": \"US\",\n    \"device\": \"iPhone 15 Pro Max\",\n    \"os\": \"iOS 17.0\",\n    \"platform\": \"ios\",\n    \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n    \"appBuild\": \"100\",\n    \"appVersion\": \"1.0.0\",\n    \"botsiSdkVersion\": null,\n    \"custom\": [],\n    \"email\": null,\n    \"phone\": null,\n    \"gender\": null,\n    \"idfa\": null,\n    \"username\": null,\n    \"locale\": \"en_US\",\n    \"ip\": \"203.0.113.7\",\n    \"createdAt\": null,\n    \"ipCountry\": null,\n    \"appleConsumptionConsent\": null,\n    \"birthday\": null,\n    \"age\": null,\n    \"totalRevenueUsd\": 9.99,\n    \"updatedDate\": \"2026-09-09T10:30:00.000Z\",\n    \"accessLevels\": {\n      \"premium\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"com.example.premium.monthly\",\n        \"sourceBasePlanId\": null,\n        \"sourcePurchaseToken\": null,\n        \"store\": \"app_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": false,\n        \"isRefund\": false,\n        \"willRenew\": true,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": null,\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": \"2026-10-09\",\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": null,\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"subscriptions\": {\n      \"com.example.premium.monthly\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"com.example.premium.monthly\",\n        \"sourceBasePlanId\": null,\n        \"sourcePurchaseToken\": null,\n        \"store\": \"app_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": false,\n        \"isRefund\": false,\n        \"willRenew\": true,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": null,\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": \"2026-10-09\",\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": null,\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"nonSubscriptions\": {}\n  }\n}"},{"id":"507b5c04-2526-4e96-86fa-9b66251af12a","name":"404 Not Found: Transaction not found by Apple","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"productId\": \"com.example.premium.monthly\",\n  \"transactionId\": \"2000000871590381\",\n  \"originalTransactionId\": \"2000000261699248\",\n  \"source\": \"purchasing\",\n  \"environment\": \"production\",\n  \"isSubscription\": true,\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/apple-store/validate"},"status":"Not Found","code":404,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Transaction not found. originalTransactionId: 2000000261699248\",\n  \"status\": 404\n}"},{"id":"2faf7c3d-8240-4282-9158-91289bb5197d","name":"400 Bad Request: App Store connection not configured","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"productId\": \"com.example.premium.monthly\",\n  \"transactionId\": \"2000000871590381\",\n  \"originalTransactionId\": \"2000000261699248\",\n  \"source\": \"purchasing\",\n  \"environment\": \"production\",\n  \"isSubscription\": true,\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/apple-store/validate"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"No ios settings found\",\n  \"status\": 400\n}"},{"id":"d6545b71-58ca-4666-b479-6306296e388b","name":"400 Bad Request: Missing required v1 fields","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"productId\": \"com.example.premium.monthly\",\n  \"transactionId\": \"2000000871590381\",\n  \"originalTransactionId\": \"2000000261699248\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/apple-store/validate"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"source must be one of the following values: restore, purchasing, observing, isExperiment must be a boolean value, isExperiment should not be null or undefined, aiPricingModelId must be a number conforming to the specified constraints, aiPricingModelId should not be null or undefined\",\n  \"status\": 400\n}"},{"id":"8ed841f2-e81b-4966-b7da-1673baf0080f","name":"201 Created: Earlier purchase skipped; current profile returned","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"productId\": \"com.example.premium.monthly\",\n  \"transactionId\": \"2000000000000001\",\n  \"originalTransactionId\": \"2000000000000001\",\n  \"source\": \"purchasing\",\n  \"environment\": \"production\",\n  \"isSubscription\": true,\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/apple-store/validate"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"createdDate\": \"2026-09-09T10:30:00.000Z\",\n    \"linkedProfileIds\": null,\n    \"state\": \"never-subscribed\",\n    \"customerUserId\": \"user-123\",\n    \"migrationProfileId\": null,\n    \"country\": \"US\",\n    \"device\": \"iPhone 15 Pro Max\",\n    \"os\": \"iOS 17.0\",\n    \"platform\": \"ios\",\n    \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n    \"appBuild\": \"100\",\n    \"appVersion\": \"1.0.0\",\n    \"botsiSdkVersion\": null,\n    \"custom\": [],\n    \"email\": null,\n    \"phone\": null,\n    \"gender\": null,\n    \"idfa\": null,\n    \"username\": null,\n    \"locale\": \"en_US\",\n    \"ip\": \"203.0.113.7\",\n    \"createdAt\": null,\n    \"ipCountry\": null,\n    \"appleConsumptionConsent\": null,\n    \"birthday\": null,\n    \"age\": null,\n    \"totalRevenueUsd\": 0,\n    \"updatedDate\": \"2026-09-09T10:30:00.000Z\",\n    \"accessLevels\": {},\n    \"subscriptions\": {},\n    \"nonSubscriptions\": {}\n  }\n}"}],"_postman_id":"364c4122-2857-4c9c-9db1-4598b95c453f"},{"name":"Validate Google Play Purchase: Subscription","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Response includes the current profile and access objects', () => {","        pm.expect(body.data.profileId).to.be.a('string').and.not.empty;","        pm.expect(body.data).to.have.property('customerUserId');","        pm.expect(body.data.accessLevels).to.be.an('object');","        pm.expect(body.data.subscriptions).to.be.an('object');","        pm.expect(body.data.nonSubscriptions).to.deep.equal({});","    });","","    pm.test('Submitted purchase tracking fields have the expected types', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw));","        pm.expect(sent.aiPricingModelId).to.be.a('number');","        pm.expect(sent.isExperiment).to.be.a('boolean');","        for (const field of ['paywallId', 'abTestId']) {","            if (sent[field] !== undefined) pm.expect(sent[field]).to.be.a('number');","        }","    });","    // These checks do not prove that the purchase was processed or access was granted.","}"],"id":"78837d79-2257-4309-aa65-be6a38c5d8cc"}}],"id":"cd70d68a-ad02-4cc5-b0af-55fc34255786","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"token\": \"\",\n  \"productId\": \"\",\n  \"environment\": \"production\",\n  \"placementId\": \"\",\n  \"paywallId\": ,\n  \"abTestId\": ,\n  \"isExperiment\": ,\n  \"aiPricingModelId\": ,\n  \"subscriptionOfferDetails\": {\n    \"basePlanId\": \"\",\n    \"pricingPhases\": [\n      {\n        \"priceAmountMicros\": 9990000,\n        \"currencyCode\": \"USD\",\n        \"billingPeriod\": \"P1M\",\n        \"recurrenceMode\": 1,\n        \"billingCycleCount\": 0\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/validate","description":"<p>Validate a completed Google Play subscription. When the purchase is processed, Botsi checks the token with Google, records the purchase, and updates subscription access. A request skipped because it is already processing can still return the current profile.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<p><code>profileId</code> or <code>customerUserId</code>, plus <code>productId</code>, <code>token</code>, <code>isExperiment</code>, and <code>aiPricingModelId</code>.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Type</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>token</code></td>\n<td>string</td>\n<td>The purchase token from <code>getPurchaseToken()</code> in the Google Play Billing Library.</td>\n</tr>\n<tr>\n<td><code>productId</code></td>\n<td>string</td>\n<td>The Play Store Product ID configured in Botsi. This is <code>sourceProductId</code> in a Fetch Paywall response for <code>play_store</code>.</td>\n</tr>\n<tr>\n<td><code>isExperiment</code></td>\n<td>boolean</td>\n<td>The value returned by Fetch Paywall.</td>\n</tr>\n<tr>\n<td><code>aiPricingModelId</code></td>\n<td>number</td>\n<td>The model ID returned by Fetch Paywall, or <code>0</code>.</td>\n</tr>\n</tbody>\n</table>\n</div><h2 id=\"optional-fields\">Optional fields</h2>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Type</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>placementId</code></td>\n<td>string</td>\n<td>The placement used to fetch the paywall.</td>\n</tr>\n<tr>\n<td><code>paywallId</code></td>\n<td>number</td>\n<td><code>data.id</code> from Fetch Paywall.</td>\n</tr>\n<tr>\n<td><code>abTestId</code></td>\n<td>number</td>\n<td><code>data.abTestId</code> from an A/B test selection.</td>\n</tr>\n<tr>\n<td><code>isSubscription</code></td>\n<td>boolean</td>\n<td>Whether the purchase is a subscription.</td>\n</tr>\n<tr>\n<td><code>environment</code></td>\n<td>string</td>\n<td>Set <code>production</code> for production purchases. See the environment rule below.</td>\n</tr>\n<tr>\n<td><code>subscriptionOfferDetails</code></td>\n<td>object</td>\n<td>The purchased subscription offer from <code>ProductDetails.getSubscriptionOfferDetails()</code>.</td>\n</tr>\n<tr>\n<td><code>oneTimePurchaseOfferDetails</code></td>\n<td>object</td>\n<td>Required during processing of a one-time purchase; use the One-Time Purchase request for that workflow.</td>\n</tr>\n</tbody>\n</table>\n</div><p>Send the offer object that matches the purchase. The input validator allows both objects or neither; it does not enforce a one-object rule. One-time purchase processing still requires price details. For subscriptions, use the purchased base plan and offer returned by Google Play, rather than assuming the first offer is the right one.</p>\n<h2 id=\"environment\">Environment</h2>\n<p><strong>Set <code>environment: production</code> explicitly for production purchases.</strong> The API has no default for this field. In a production store configuration, omitting it causes the revenue event to be skipped, even if access is updated.</p>\n<h2 id=\"offer-details\">Offer details</h2>\n<p>For <code>subscriptionOfferDetails</code>, include <code>basePlanId</code> (string), <code>offerId</code> (string, if the product has a store offer), and a <code>pricingPhases</code> array.</p>\n<p>Each pricing phase contains:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Type</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>priceAmountMicros</code></td>\n<td>number</td>\n<td>Price in millionths of a currency unit. For example, <code>9990000</code> is USD 9.99.</td>\n</tr>\n<tr>\n<td><code>currencyCode</code></td>\n<td>string</td>\n<td>A three-letter currency code in ISO 4217 format, such as <code>USD</code>.</td>\n</tr>\n<tr>\n<td><code>billingPeriod</code></td>\n<td>string</td>\n<td>A billing period in ISO 8601 format, such as <code>P1M</code> for one month.</td>\n</tr>\n<tr>\n<td><code>recurrenceMode</code></td>\n<td>number</td>\n<td>The Google Play value describing how the phase repeats.</td>\n</tr>\n<tr>\n<td><code>billingCycleCount</code></td>\n<td>number</td>\n<td>The number of billing cycles in the phase.</td>\n</tr>\n</tbody>\n</table>\n</div><p>For <code>oneTimePurchaseOfferDetails</code>, include <code>priceAmountMicros</code> (number) and <code>currencyCode</code> (ISO 4217 string).</p>\n<h2 id=\"linking-the-purchase-to-a-paywall\">Linking the purchase to a paywall</h2>\n<p>Purchase validation does not accept <code>paywallSessionId</code>. Copy <code>paywallId</code>, <code>isExperiment</code>, and <code>aiPricingModelId</code> from Fetch Paywall, and include <code>abTestId</code> for an A/B test selection.</p>\n<p>v1 requires <code>isExperiment</code> and <code>aiPricingModelId</code>. If there was no AI Pricing selection, send <code>false</code> and <code>0</code>, respectively. These fields are optional in v2.</p>\n<p>Keep <code></code>, <code></code>, <code></code>, and <code></code> outside quotation marks in the request body. The first three must be numbers; the last must be <code>true</code> or <code>false</code>.</p>\n<h2 id=\"response\">Response</h2>\n<p>Returns the current profile, including personal information. A successful response can contain unchanged access details. The <code>accessLevels</code> and <code>subscriptions</code> objects come from the profile's single linked access record. Both can be empty, and <code>nonSubscriptions</code> is always <code>{}</code>. They are not a complete purchase history. Check access status and expiry before enabling paid features. v2 returns fewer fields.</p>\n<p><strong>Prefer <code>customerUserId</code> when available.</strong> That lookup is limited to your app. Unlike v2, v1 does not check that a supplied <code>profileId</code> belongs to your app.</p>\n<p><strong>Source:</strong> api/src/web-api/purchases/purchases.controller.ts, dto/validate-token.dto.ts, api/src/sdk/purchases/dto/validate-token.dto.ts, google-purchase.service.ts</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","purchases","play-store","validate"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"a3e6dd32-99d0-4058-9a1b-1dc7268fac08","name":"201 Created: Purchase access granted","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"productId\": \"premium_monthly\",\n  \"environment\": \"production\",\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32,\n  \"subscriptionOfferDetails\": {\n    \"basePlanId\": \"monthly-base\",\n    \"pricingPhases\": [\n      {\n        \"priceAmountMicros\": 9990000,\n        \"currencyCode\": \"USD\",\n        \"billingPeriod\": \"P1M\",\n        \"recurrenceMode\": 1,\n        \"billingCycleCount\": 0\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/validate"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"createdDate\": \"2026-09-09T10:30:00.000Z\",\n    \"linkedProfileIds\": null,\n    \"state\": \"subscribed\",\n    \"customerUserId\": \"user-123\",\n    \"migrationProfileId\": null,\n    \"country\": \"US\",\n    \"device\": \"Pixel 9\",\n    \"os\": \"Android 15\",\n    \"platform\": \"android\",\n    \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n    \"appBuild\": \"100\",\n    \"appVersion\": \"1.0.0\",\n    \"botsiSdkVersion\": null,\n    \"custom\": [],\n    \"email\": null,\n    \"phone\": null,\n    \"gender\": null,\n    \"idfa\": null,\n    \"username\": null,\n    \"locale\": \"en_US\",\n    \"ip\": \"203.0.113.7\",\n    \"createdAt\": null,\n    \"ipCountry\": null,\n    \"appleConsumptionConsent\": null,\n    \"birthday\": null,\n    \"age\": null,\n    \"totalRevenueUsd\": 9.99,\n    \"updatedDate\": \"2026-09-09T10:30:00.000Z\",\n    \"accessLevels\": {\n      \"premium\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"premium_monthly\",\n        \"sourceBasePlanId\": \"monthly-base\",\n        \"sourcePurchaseToken\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n        \"store\": \"play_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": false,\n        \"isRefund\": false,\n        \"willRenew\": true,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": null,\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": \"2026-10-09\",\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": null,\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"subscriptions\": {\n      \"premium_monthly\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"premium_monthly\",\n        \"sourceBasePlanId\": \"monthly-base\",\n        \"sourcePurchaseToken\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n        \"store\": \"play_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": false,\n        \"isRefund\": false,\n        \"willRenew\": true,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": null,\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": \"2026-10-09\",\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": null,\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"nonSubscriptions\": {}\n  }\n}"},{"id":"8f42c6ab-126a-45c1-98b9-4ea64de82105","name":"403 Forbidden: Google rejected the purchase token","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"productId\": \"premium_monthly\",\n  \"environment\": \"production\",\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32,\n  \"subscriptionOfferDetails\": {\n    \"basePlanId\": \"monthly-base\",\n    \"pricingPhases\": [\n      {\n        \"priceAmountMicros\": 9990000,\n        \"currencyCode\": \"USD\",\n        \"billingPeriod\": \"P1M\",\n        \"recurrenceMode\": 1,\n        \"billingCycleCount\": 0\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/validate"},"status":"Forbidden","code":403,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Google API error: The purchase token was not found.\",\n  \"status\": 403\n}"},{"id":"f771ae9d-ba8f-48bb-8772-215d84d58e9f","name":"400 Bad Request: productId not configured for Google Play","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"productId\": \"premium_monthly\",\n  \"environment\": \"production\",\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32,\n  \"subscriptionOfferDetails\": {\n    \"basePlanId\": \"monthly-base\",\n    \"pricingPhases\": [\n      {\n        \"priceAmountMicros\": 9990000,\n        \"currencyCode\": \"USD\",\n        \"billingPeriod\": \"P1M\",\n        \"recurrenceMode\": 1,\n        \"billingCycleCount\": 0\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/validate"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"No product found for app: 4021. productId: premium_monthly\",\n  \"status\": 400\n}"},{"id":"c05f8008-4c78-4b29-a865-65a7b257e70f","name":"400 Bad Request: Google Play connection not configured","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"productId\": \"premium_monthly\",\n  \"environment\": \"production\",\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32,\n  \"subscriptionOfferDetails\": {\n    \"basePlanId\": \"monthly-base\",\n    \"pricingPhases\": [\n      {\n        \"priceAmountMicros\": 9990000,\n        \"currencyCode\": \"USD\",\n        \"billingPeriod\": \"P1M\",\n        \"recurrenceMode\": 1,\n        \"billingCycleCount\": 0\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/validate"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Android settings doesn't have packageName\",\n  \"status\": 400\n}"},{"id":"e8d51496-485b-401d-8271-2a5bd977db86","name":"410 Gone: Purchase no longer available to query","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"productId\": \"premium_monthly\",\n  \"environment\": \"production\",\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32,\n  \"subscriptionOfferDetails\": {\n    \"basePlanId\": \"monthly-base\",\n    \"pricingPhases\": [\n      {\n        \"priceAmountMicros\": 9990000,\n        \"currencyCode\": \"USD\",\n        \"billingPeriod\": \"P1M\",\n        \"recurrenceMode\": 1,\n        \"billingCycleCount\": 0\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/validate"},"status":"Gone","code":410,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Google API error: The subscription purchase is no longer available for query.\",\n  \"status\": 410\n}"}],"_postman_id":"cd70d68a-ad02-4cc5-b0af-55fc34255786"},{"name":"Validate Google Play Purchase: Subscription with Free Trial","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Response includes the current profile and access objects', () => {","        pm.expect(body.data.profileId).to.be.a('string').and.not.empty;","        pm.expect(body.data).to.have.property('customerUserId');","        pm.expect(body.data.accessLevels).to.be.an('object');","        pm.expect(body.data.subscriptions).to.be.an('object');","        pm.expect(body.data.nonSubscriptions).to.deep.equal({});","    });","","    pm.test('Submitted purchase tracking fields have the expected types', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw));","        pm.expect(sent.aiPricingModelId).to.be.a('number');","        pm.expect(sent.isExperiment).to.be.a('boolean');","        for (const field of ['paywallId', 'abTestId']) {","            if (sent[field] !== undefined) pm.expect(sent[field]).to.be.a('number');","        }","    });","    // These checks do not prove that the purchase was processed or access was granted.","","    pm.test('Free trial request includes a zero-price phase', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw));","        pm.expect(sent.subscriptionOfferDetails.pricingPhases.some(phase => phase.priceAmountMicros === 0)).to.eql(true);","    });","}"],"id":"2e4b3b88-d5c7-41d9-952c-16e868ddebe5"}}],"id":"7d4b0b58-648c-4820-a1e0-8423d12e8789","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"token\": \"\",\n  \"productId\": \"\",\n  \"environment\": \"production\",\n  \"placementId\": \"\",\n  \"paywallId\": ,\n  \"abTestId\": ,\n  \"isExperiment\": ,\n  \"aiPricingModelId\": ,\n  \"subscriptionOfferDetails\": {\n    \"basePlanId\": \"\",\n    \"offerId\": \"\",\n    \"pricingPhases\": [\n      {\n        \"priceAmountMicros\": 0,\n        \"currencyCode\": \"USD\",\n        \"billingPeriod\": \"P1W\",\n        \"recurrenceMode\": 2,\n        \"billingCycleCount\": 1\n      },\n      {\n        \"priceAmountMicros\": 9990000,\n        \"currencyCode\": \"USD\",\n        \"billingPeriod\": \"P1M\",\n        \"recurrenceMode\": 1,\n        \"billingCycleCount\": 0\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/validate","description":"<p>Validate a Google Play subscription with a free trial. This uses the same endpoint and required fields as <strong>Validate Google Play Purchase: Subscription</strong>.</p>\n<h2 id=\"free-trial-details\">Free trial details</h2>\n<p>Include a <code>pricingPhases</code> entry with <code>priceAmountMicros: 0</code>, along with its <code>billingPeriod</code>, <code>recurrenceMode</code>, and <code>billingCycleCount</code>. Botsi uses this zero-price phase to identify the free trial. Without it, the purchase is recorded as a paid subscription and revenue may be overstated.</p>\n<p>Place the free trial phase first, followed by the paid phase that starts when the trial ends.</p>\n<p>The example uses:</p>\n<ul>\n<li><code>recurrenceMode: 2</code> and <code>billingCycleCount: 1</code> for the trial phase, which runs once.</li>\n<li><code>recurrenceMode: 1</code> for the paid phase, which continues to repeat.</li>\n</ul>\n<p>Use the values from the <code>ProductDetails</code> object returned to your app by Google Play.</p>\n<p>Set <code>environment</code> to <code>production</code> for production purchases. See <strong>Validate Google Play Purchase: Subscription</strong> for the full field list and environment rules.</p>\n<h2 id=\"linking-the-purchase-to-a-paywall\">Linking the purchase to a paywall</h2>\n<p>Purchase validation does not accept <code>paywallSessionId</code>. Copy <code>paywallId</code>, <code>isExperiment</code>, and <code>aiPricingModelId</code> from Fetch Paywall, and include <code>abTestId</code> for an A/B test selection.</p>\n<p>v1 requires <code>isExperiment</code> and <code>aiPricingModelId</code>. If there was no AI Pricing selection, send <code>false</code> and <code>0</code>, respectively. These fields are optional in v2.</p>\n<p>Keep <code></code>, <code></code>, <code></code>, and <code></code> outside quotation marks in the request body. The first three must be numbers; the last must be <code>true</code> or <code>false</code>.</p>\n<h2 id=\"response\">Response</h2>\n<p>Returns the current profile, including personal information. A successful response can contain unchanged access details. The <code>accessLevels</code> and <code>subscriptions</code> objects come from the profile's single linked access record. Both can be empty, and <code>nonSubscriptions</code> is always <code>{}</code>. They are not a complete purchase history. Check access status and expiry before enabling paid features. v2 returns fewer fields.</p>\n<p><strong>Prefer <code>customerUserId</code> when available.</strong> That lookup is limited to your app. Unlike v2, v1 does not check that a supplied <code>profileId</code> belongs to your app.</p>\n<p><strong>Source:</strong> api/src/sdk/purchases/google-purchase.service.ts</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","purchases","play-store","validate"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"8c1af5b2-97c7-4d52-8ee2-ee5c9433c769","name":"201 Created: Free trial access granted","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"productId\": \"premium_monthly\",\n  \"environment\": \"production\",\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32,\n  \"subscriptionOfferDetails\": {\n    \"basePlanId\": \"monthly-base\",\n    \"offerId\": \"freetrial-7d\",\n    \"pricingPhases\": [\n      {\n        \"priceAmountMicros\": 0,\n        \"currencyCode\": \"USD\",\n        \"billingPeriod\": \"P1W\",\n        \"recurrenceMode\": 2,\n        \"billingCycleCount\": 1\n      },\n      {\n        \"priceAmountMicros\": 9990000,\n        \"currencyCode\": \"USD\",\n        \"billingPeriod\": \"P1M\",\n        \"recurrenceMode\": 1,\n        \"billingCycleCount\": 0\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/validate"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"createdDate\": \"2026-09-09T10:30:00.000Z\",\n    \"linkedProfileIds\": null,\n    \"state\": \"active-trial\",\n    \"customerUserId\": \"user-123\",\n    \"migrationProfileId\": null,\n    \"country\": \"US\",\n    \"device\": \"Pixel 9\",\n    \"os\": \"Android 15\",\n    \"platform\": \"android\",\n    \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n    \"appBuild\": \"100\",\n    \"appVersion\": \"1.0.0\",\n    \"botsiSdkVersion\": null,\n    \"custom\": [],\n    \"email\": null,\n    \"phone\": null,\n    \"gender\": null,\n    \"idfa\": null,\n    \"username\": null,\n    \"locale\": \"en_US\",\n    \"ip\": \"203.0.113.7\",\n    \"createdAt\": null,\n    \"ipCountry\": null,\n    \"appleConsumptionConsent\": null,\n    \"birthday\": null,\n    \"age\": null,\n    \"totalRevenueUsd\": 0,\n    \"updatedDate\": \"2026-09-09T10:30:00.000Z\",\n    \"accessLevels\": {\n      \"premium\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"premium_monthly\",\n        \"sourceBasePlanId\": \"monthly-base\",\n        \"sourcePurchaseToken\": null,\n        \"store\": \"play_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": false,\n        \"isRefund\": false,\n        \"willRenew\": true,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": \"freetrial-7d\",\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": \"2026-09-16\",\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": \"free_trial\",\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"subscriptions\": {\n      \"premium_monthly\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"premium_monthly\",\n        \"sourceBasePlanId\": \"monthly-base\",\n        \"sourcePurchaseToken\": null,\n        \"store\": \"play_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": false,\n        \"isRefund\": false,\n        \"willRenew\": true,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": \"freetrial-7d\",\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": \"2026-09-16\",\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": \"free_trial\",\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"nonSubscriptions\": {}\n  }\n}"}],"_postman_id":"7d4b0b58-648c-4820-a1e0-8423d12e8789"},{"name":"Validate Google Play Purchase: One-Time Purchase","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Response includes the current profile and access objects', () => {","        pm.expect(body.data.profileId).to.be.a('string').and.not.empty;","        pm.expect(body.data).to.have.property('customerUserId');","        pm.expect(body.data.accessLevels).to.be.an('object');","        pm.expect(body.data.subscriptions).to.be.an('object');","        pm.expect(body.data.nonSubscriptions).to.deep.equal({});","    });","","    pm.test('Submitted purchase tracking fields have the expected types', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw));","        pm.expect(sent.aiPricingModelId).to.be.a('number');","        pm.expect(sent.isExperiment).to.be.a('boolean');","        for (const field of ['paywallId', 'abTestId']) {","            if (sent[field] !== undefined) pm.expect(sent[field]).to.be.a('number');","        }","    });","    // These checks do not prove that the purchase was processed or access was granted.","","    pm.test('One-time request includes currency and a nonzero price', () => {","        const sent = JSON.parse(pm.variables.replaceIn(pm.request.body.raw));","        pm.expect(sent.oneTimePurchaseOfferDetails.currencyCode).to.be.a('string').and.not.empty;","        pm.expect(sent.oneTimePurchaseOfferDetails.priceAmountMicros).to.be.a('number').and.not.equal(0);","    });","}"],"id":"d11db2f1-68bc-4edf-b3f7-5930bf4247e4"}}],"id":"09367579-3b75-433c-b36c-b237474e2d03","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"token\": \"\",\n  \"productId\": \"\",\n  \"environment\": \"production\",\n  \"placementId\": \"\",\n  \"paywallId\": ,\n  \"abTestId\": ,\n  \"isExperiment\": ,\n  \"aiPricingModelId\": ,\n  \"oneTimePurchaseOfferDetails\": {\n    \"priceAmountMicros\": 29990000,\n    \"currencyCode\": \"USD\"\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/validate","description":"<p>Validate a one-time Google Play purchase. This uses the same endpoint and common required fields as <strong>Validate Google Play Purchase: Subscription</strong>, plus the price details below.</p>\n<h2 id=\"one-time-purchase-details\">One-time purchase details</h2>\n<p>Include <code>oneTimePurchaseOfferDetails</code> with a non-empty <code>currencyCode</code> and a nonzero <code>priceAmountMicros</code>. If either is missing, or the price is zero, processing returns <code>400</code>. Send this object for a one-time purchase. The input validator does not reject a body merely because it also contains <code>subscriptionOfferDetails</code>.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>priceAmountMicros</code></td>\n<td>Price in millionths of a currency unit. For example, <code>29990000</code> is USD 29.99.</td>\n</tr>\n<tr>\n<td><code>currencyCode</code></td>\n<td>A three-letter currency code in ISO 4217 format, such as <code>USD</code>.</td>\n</tr>\n</tbody>\n</table>\n</div><p>Botsi uses the product's configured period, such as consumable, lifetime, or non-subscription, to decide whether the purchase is a one-time purchase. The offer details object alone does not determine the purchase type.</p>\n<p>Set <code>environment</code> to <code>production</code> for production purchases. See <strong>Validate Google Play Purchase: Subscription</strong> for the full field list and environment rules.</p>\n<h2 id=\"linking-the-purchase-to-a-paywall\">Linking the purchase to a paywall</h2>\n<p>Purchase validation does not accept <code>paywallSessionId</code>. Copy <code>paywallId</code>, <code>isExperiment</code>, and <code>aiPricingModelId</code> from Fetch Paywall, and include <code>abTestId</code> for an A/B test selection.</p>\n<p>v1 requires <code>isExperiment</code> and <code>aiPricingModelId</code>. If there was no AI Pricing selection, send <code>false</code> and <code>0</code>, respectively. These fields are optional in v2.</p>\n<p>Keep <code></code>, <code></code>, <code></code>, and <code></code> outside quotation marks in the request body. The first three must be numbers; the last must be <code>true</code> or <code>false</code>.</p>\n<h2 id=\"response\">Response</h2>\n<p>Returns the current profile, including personal information. A successful response can contain unchanged access details. The <code>accessLevels</code> and <code>subscriptions</code> objects come from the profile's single linked access record. Both can be empty, and <code>nonSubscriptions</code> is always <code>{}</code>. They are not a complete purchase history. Check access status and expiry before enabling paid features. v2 returns fewer fields.</p>\n<p><strong>Prefer <code>customerUserId</code> when available.</strong> That lookup is limited to your app. Unlike v2, v1 does not check that a supplied <code>profileId</code> belongs to your app.</p>\n<h2 id=\"access-behavior\">Access behavior</h2>\n<p>Lifetime products update the profile's access. Other one-time products record the purchase without this access update. A <code>201</code> response does not by itself confirm that paid access was granted.</p>\n<p><strong>Source:</strong> api/src/sdk/purchases/google-purchase.service.ts</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","purchases","play-store","validate"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"d58bdaf8-550a-45e5-855f-0280461bf85f","name":"201 Created","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"productId\": \"lifetime_unlock\",\n  \"environment\": \"production\",\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32,\n  \"oneTimePurchaseOfferDetails\": {\n    \"priceAmountMicros\": 29990000,\n    \"currencyCode\": \"USD\"\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/validate"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"createdDate\": \"2026-09-09T10:30:00.000Z\",\n    \"linkedProfileIds\": null,\n    \"state\": \"subscribed\",\n    \"customerUserId\": \"user-123\",\n    \"migrationProfileId\": null,\n    \"country\": \"US\",\n    \"device\": \"Pixel 9\",\n    \"os\": \"Android 15\",\n    \"platform\": \"android\",\n    \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n    \"appBuild\": \"100\",\n    \"appVersion\": \"1.0.0\",\n    \"botsiSdkVersion\": null,\n    \"custom\": [],\n    \"email\": null,\n    \"phone\": null,\n    \"gender\": null,\n    \"idfa\": null,\n    \"username\": null,\n    \"locale\": \"en_US\",\n    \"ip\": \"203.0.113.7\",\n    \"createdAt\": null,\n    \"ipCountry\": null,\n    \"appleConsumptionConsent\": null,\n    \"birthday\": null,\n    \"age\": null,\n    \"totalRevenueUsd\": 29.99,\n    \"updatedDate\": \"2026-09-09T10:30:00.000Z\",\n    \"accessLevels\": {\n      \"premium\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"lifetime_unlock\",\n        \"sourceBasePlanId\": null,\n        \"sourcePurchaseToken\": null,\n        \"store\": \"play_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": true,\n        \"isRefund\": false,\n        \"willRenew\": null,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": null,\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": null,\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": null,\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"subscriptions\": {\n      \"lifetime_unlock\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"lifetime_unlock\",\n        \"sourceBasePlanId\": null,\n        \"sourcePurchaseToken\": null,\n        \"store\": \"play_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": true,\n        \"isRefund\": false,\n        \"willRenew\": null,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": null,\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": null,\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": null,\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"nonSubscriptions\": {}\n  }\n}"},{"id":"0bcc8840-bd32-45af-9784-fb270429b118","name":"400 Bad Request: Missing one-time purchase price details","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"productId\": \"lifetime_unlock\",\n  \"environment\": \"production\",\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"abTestId\": 0,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/validate"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"No price data were passed for OTP validation request, appId: 4021, productId: 102, transactionId: GPA.1234-5678-9012-34567\",\n  \"status\": 400\n}"}],"_postman_id":"09367579-3b75-433c-b36c-b237474e2d03"},{"name":"Restore Apple Store Purchases","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Response includes the current profile and access objects', () => {","        pm.expect(body.data.profileId).to.be.a('string').and.not.empty;","        pm.expect(body.data).to.have.property('customerUserId');","        pm.expect(body.data.accessLevels).to.be.an('object');","        pm.expect(body.data.subscriptions).to.be.an('object');","        pm.expect(body.data.nonSubscriptions).to.deep.equal({});","    });","}"],"id":"7a3b7a81-ff31-4f0d-8f19-0e69b76bcac5"}}],"id":"79efe35f-dfc1-40f6-a808-0e960f2bc0b8","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"receipt\": \"\",\n  \"storeCountry\": \"US\",\n  \"appVersion\": \"1.0.0\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"locale\": \"en_US\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/apple-store/restore","description":"<p>Restore a user's App Store purchases after a reinstall or a move to a new device. Botsi reads the transactions from the receipt and restores the associated access to the profile.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<ul>\n<li><code>profileId</code> or <code>customerUserId</code>.</li>\n<li><code>receipt</code>: the full app receipt encoded as base64. Set it in <code>appleReceipt</code>. This v1 request parses an app receipt and does not accept a StoreKit 2 transaction ID in this field.</li>\n</ul>\n<h2 id=\"optional-fields\">Optional fields</h2>\n<p><code>storeCountry</code>, <code>appVersion</code>, <code>appBuild</code>, <code>device</code>, <code>locale</code>, <code>os</code>, and <code>botsiSdkVersion</code>.</p>\n<h2 id=\"app-setup\">App setup</h2>\n<p>Your app must have a public key configured, the same key used by the mobile SDK. If it does not, the request returns <code>400</code> with <code>App has no public key.</code> Continue to use the secret key in this request's <code>Authorization</code> header.</p>\n<h2 id=\"response\">Response</h2>\n<p>Returns the current profile after restore processing. Check the access details to confirm the result. The <code>accessLevels</code> and <code>subscriptions</code> objects come from the profile's single linked access record. Both can be empty, and <code>nonSubscriptions</code> is always <code>{}</code>. They are not a complete purchase history.</p>\n<p><strong>Profile ownership:</strong> Use a profile from the app identified by your secret key. This v1 request does not check app ownership when using <code>profileId</code>. A <code>customerUserId</code> lookup is limited to your app.</p>\n<p><strong>Source:</strong> api/src/web-api/purchases/purchases.controller.ts, dto/restore-purchase.dto.ts, api/src/sdk/purchases/apple-purchase.service.ts (restoreApplePurchases)</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","purchases","apple-store","restore"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"915493a1-b708-455e-9b3a-65cf4d8d8e45","name":"201 Created","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"receipt\": \"MIIT6gYJKoZIhvcNAQcCoIIT2zCCE9cCAQExCzAJBgUrDgMCGgUAMIIDjAYJKoZIhvcNAQcBoIIDfQSCA3kxggN1MAoCAQgCAQEEAhYAMAoCARQCAQEEAgwAMAsCAQECAQEEAwIBADALAgEDAgEBBAMMATEwCwIBCwIBAQQDAgEAMAsCAQ8CAQEEAwIBADALAgEQAgEBBAMCAQAwCwIBGQIBAQQDAgEDMAwCAQoCAQEEBBYCNCswDAIBDgIBAQQEAgIAjTANAgENAgEBBAUCAwHUJTANAgETAgEBBAUMAzEuMDAOAgEJAgEBBAYCBFAyNTUwGAIBBAIBAgQQ\",\n  \"storeCountry\": \"US\",\n  \"appVersion\": \"1.0.0\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"locale\": \"en_US\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/apple-store/restore"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"createdDate\": \"2026-09-09T10:30:00.000Z\",\n    \"linkedProfileIds\": null,\n    \"state\": \"subscribed\",\n    \"customerUserId\": \"user-123\",\n    \"migrationProfileId\": null,\n    \"country\": \"US\",\n    \"device\": \"iPhone 15 Pro Max\",\n    \"os\": \"iOS 17.0\",\n    \"platform\": \"ios\",\n    \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n    \"appBuild\": \"100\",\n    \"appVersion\": \"1.0.0\",\n    \"botsiSdkVersion\": null,\n    \"custom\": [],\n    \"email\": null,\n    \"phone\": null,\n    \"gender\": null,\n    \"idfa\": null,\n    \"username\": null,\n    \"locale\": \"en_US\",\n    \"ip\": \"203.0.113.7\",\n    \"createdAt\": null,\n    \"ipCountry\": null,\n    \"appleConsumptionConsent\": null,\n    \"birthday\": null,\n    \"age\": null,\n    \"totalRevenueUsd\": 9.99,\n    \"updatedDate\": \"2026-09-09T10:30:00.000Z\",\n    \"accessLevels\": {\n      \"premium\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"com.example.premium.monthly\",\n        \"sourceBasePlanId\": null,\n        \"sourcePurchaseToken\": null,\n        \"store\": \"app_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": false,\n        \"isRefund\": false,\n        \"willRenew\": true,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": null,\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": \"2026-10-09\",\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": null,\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"subscriptions\": {\n      \"com.example.premium.monthly\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"com.example.premium.monthly\",\n        \"sourceBasePlanId\": null,\n        \"sourcePurchaseToken\": null,\n        \"store\": \"app_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": false,\n        \"isRefund\": false,\n        \"willRenew\": true,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": null,\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": \"2026-10-09\",\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": null,\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"nonSubscriptions\": {}\n  }\n}"},{"id":"76a89e4d-3a2c-4f3a-8355-47f9246b1fc3","name":"400 Bad Request: App has no public key","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"receipt\": \"MIIT6gYJKoZIhvcNAQcCoIIT2zCCE9cCAQExCzAJBgUrDgMCGgUAMIIDjAYJKoZIhvcNAQcBoIIDfQSCA3kxggN1MAoCAQgCAQEEAhYAMAoCARQCAQEEAgwAMAsCAQECAQEEAwIBADALAgEDAgEBBAMMATEwCwIBCwIBAQQDAgEAMAsCAQ8CAQEEAwIBADALAgEQAgEBBAMCAQAwCwIBGQIBAQQDAgEDMAwCAQoCAQEEBBYCNCswDAIBDgIBAQQEAgIAjTANAgENAgEBBAUCAwHUJTANAgETAgEBBAUMAzEuMDAOAgEJAgEBBAYCBFAyNTUwGAIBBAIBAgQQ\",\n  \"storeCountry\": \"US\",\n  \"appVersion\": \"1.0.0\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"locale\": \"en_US\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/apple-store/restore"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"App has no public key.\",\n  \"status\": 400\n}"},{"id":"173bafe8-4717-4690-be3d-fda710798457","name":"400 Bad Request: No transactions in receipt","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"receipt\": \"MIIT6gYJKoZIhvcNAQcCoIIT2zCCE9cCAQExCzAJBgUrDgMCGgUAMIIDjAYJKoZIhvcNAQcBoIIDfQSCA3kxggN1MAoCAQgCAQEEAhYAMAoCARQCAQEEAgwAMAsCAQECAQEEAwIBADALAgEDAgEBBAMMATEwCwIBCwIBAQQDAgEAMAsCAQ8CAQEEAwIBADALAgEQAgEBBAMCAQAwCwIBGQIBAQQDAgEDMAwCAQoCAQEEBBYCNCswDAIBDgIBAQQEAgIAjTANAgENAgEBBAUCAwHUJTANAgETAgEBBAUMAzEuMDAOAgEJAgEBBAYCBFAyNTUwGAIBBAIBAgQQ\",\n  \"storeCountry\": \"US\",\n  \"appVersion\": \"1.0.0\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"locale\": \"en_US\"\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/apple-store/restore"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"Provided receipt does not contain any transactions\",\n  \"status\": 400\n}"}],"_postman_id":"79efe35f-dfc1-40f6-a808-0e960f2bc0b8"},{"name":"Restore Google Play Purchases","event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test('Status is 201', () => pm.expect(pm.response.code).to.eql(201));","if (pm.response.code === 201) {","    const body = pm.response.json();","    pm.test('Response confirms success', () => pm.expect(body.ok).to.eql(true));","","    pm.test('Response includes the current profile and access objects', () => {","        pm.expect(body.data.profileId).to.be.a('string').and.not.empty;","        pm.expect(body.data).to.have.property('customerUserId');","        pm.expect(body.data.accessLevels).to.be.an('object');","        pm.expect(body.data.subscriptions).to.be.an('object');","        pm.expect(body.data.nonSubscriptions).to.deep.equal({});","    });","}"],"id":"fb3ca789-fbfa-43ab-be0b-786224a49aec"}}],"id":"d67cccec-b6c2-4cd7-8562-77d6ac179d71","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"attributes\": {\n    \"profileId\": \"\",\n    \"items\": [\n      {\n        \"productId\": \"\",\n        \"purchaseToken\": \"\",\n        \"productDetails\": {\n          \"subscriptionOfferDetails\": [\n            {\n              \"basePlanId\": \"\",\n              \"pricingPhases\": [\n                {\n                  \"priceAmountMicros\": 9990000,\n                  \"currencyCode\": \"USD\",\n                  \"billingPeriod\": \"P1M\",\n                  \"recurrenceMode\": 1,\n                  \"billingCycleCount\": 0\n                }\n              ]\n            }\n          ]\n        }\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/restore","description":"<p>Restore a user's Google Play purchases to a profile. Botsi checks each item against Google's purchase history.</p>\n<h2 id=\"required-fields\">Required fields</h2>\n<ul>\n<li><code>attributes.profileId</code> or <code>attributes.customerUserId</code>.</li>\n<li><code>attributes.items</code>: an array of objects containing <code>productId</code> and <code>purchaseToken</code>. Send the purchases you want to restore. An empty array is accepted and returns the current profile after app settings checks; it restores nothing.</li>\n</ul>\n<p><strong>Put the user ID inside <code>attributes</code> for this request.</strong></p>\n<h2 id=\"optional-item-fields\">Optional item fields</h2>\n<p>Each item can include <code>productDetails</code>, with <code>subscriptionOfferDetails</code> or <code>oneTimePurchaseOfferDetails</code>.</p>\n<ul>\n<li><code>subscriptionOfferDetails</code> is an array for restore requests. Purchase validation uses an object instead.</li>\n<li>Items whose <code>productDetails</code> include <code>oneTimePurchaseOfferDetails</code> are restored as one-time purchases. Other items are restored as subscriptions.</li>\n</ul>\n<h2 id=\"app-setup\">App setup</h2>\n<p>Your app must have a public key configured. If it does not, the request returns <code>400</code> with <code>App has no public key.</code> Continue to use the secret key in this request's <code>Authorization</code> header.</p>\n<h2 id=\"response\">Response</h2>\n<p>Returns the current profile after restore processing. Check the access details to confirm the result. The <code>accessLevels</code> and <code>subscriptions</code> objects come from the profile's single linked access record. Both can be empty, and <code>nonSubscriptions</code> is always <code>{}</code>. They are not a complete purchase history.</p>\n<p><strong>Profile ownership:</strong> Use a profile from the app identified by your secret key. This v1 request does not check app ownership when using <code>profileId</code>. A <code>customerUserId</code> lookup is limited to your app.</p>\n<p><strong>Source:</strong> api/src/web-api/purchases/purchases.controller.ts, dto/restore-purchase.dto.ts, api/src/sdk/purchases/google-purchase.service.ts (restoreGooglePurchases)</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}},"urlObject":{"path":["v1","web-api","purchases","play-store","restore"],"host":["https://app.botsi.com/api"],"query":[],"variable":[]}},"response":[{"id":"e3da2453-032b-41b6-9413-5d73147f90ed","name":"201 Created","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"attributes\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"items\": [\n      {\n        \"productId\": \"premium_monthly\",\n        \"purchaseToken\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n        \"productDetails\": {\n          \"subscriptionOfferDetails\": [\n            {\n              \"basePlanId\": \"monthly-base\",\n              \"pricingPhases\": [\n                {\n                  \"priceAmountMicros\": 9990000,\n                  \"currencyCode\": \"USD\",\n                  \"billingPeriod\": \"P1M\",\n                  \"recurrenceMode\": 1,\n                  \"billingCycleCount\": 0\n                }\n              ]\n            }\n          ]\n        }\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/restore"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"createdDate\": \"2026-09-09T10:30:00.000Z\",\n    \"linkedProfileIds\": null,\n    \"state\": \"subscribed\",\n    \"customerUserId\": \"user-123\",\n    \"migrationProfileId\": null,\n    \"country\": \"US\",\n    \"device\": \"Pixel 9\",\n    \"os\": \"Android 15\",\n    \"platform\": \"android\",\n    \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n    \"appBuild\": \"100\",\n    \"appVersion\": \"1.0.0\",\n    \"botsiSdkVersion\": null,\n    \"custom\": [],\n    \"email\": null,\n    \"phone\": null,\n    \"gender\": null,\n    \"idfa\": null,\n    \"username\": null,\n    \"locale\": \"en_US\",\n    \"ip\": \"203.0.113.7\",\n    \"createdAt\": null,\n    \"ipCountry\": null,\n    \"appleConsumptionConsent\": null,\n    \"birthday\": null,\n    \"age\": null,\n    \"totalRevenueUsd\": 9.99,\n    \"updatedDate\": \"2026-09-09T10:30:00.000Z\",\n    \"accessLevels\": {\n      \"premium\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"premium_monthly\",\n        \"sourceBasePlanId\": \"monthly-base\",\n        \"sourcePurchaseToken\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n        \"store\": \"play_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": false,\n        \"isRefund\": false,\n        \"willRenew\": true,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": null,\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": \"2026-10-09\",\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": null,\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"subscriptions\": {\n      \"premium_monthly\": {\n        \"id\": 5150,\n        \"createdDate\": \"2026-09-09T10:31:00.000Z\",\n        \"isActive\": true,\n        \"sourceProductId\": \"premium_monthly\",\n        \"sourceBasePlanId\": \"monthly-base\",\n        \"sourcePurchaseToken\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n        \"store\": \"play_store\",\n        \"activatedAt\": null,\n        \"isLifetime\": false,\n        \"isRefund\": false,\n        \"willRenew\": true,\n        \"isInGracePeriod\": false,\n        \"cancellationReason\": null,\n        \"offerId\": null,\n        \"startsAt\": null,\n        \"renewedAt\": \"2026-09-09T10:31:00.000Z\",\n        \"expiresAt\": \"2026-10-09\",\n        \"revokedAt\": null,\n        \"activeIntroductoryOfferType\": null,\n        \"activePromotionalOfferType\": null,\n        \"activePromotionalOfferId\": null,\n        \"unsubscribedAt\": null,\n        \"billingIssueDetectedAt\": null\n      }\n    },\n    \"nonSubscriptions\": {}\n  }\n}"},{"id":"43868aa1-55d2-4ad0-94f3-149342768414","name":"400 Bad Request: App has no public key","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"attributes\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"items\": [\n      {\n        \"productId\": \"premium_monthly\",\n        \"purchaseToken\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n        \"productDetails\": {\n          \"subscriptionOfferDetails\": [\n            {\n              \"basePlanId\": \"monthly-base\",\n              \"pricingPhases\": [\n                {\n                  \"priceAmountMicros\": 9990000,\n                  \"currencyCode\": \"USD\",\n                  \"billingPeriod\": \"P1M\",\n                  \"recurrenceMode\": 1,\n                  \"billingCycleCount\": 0\n                }\n              ]\n            }\n          ]\n        }\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://app.botsi.com/api/v1/web-api/purchases/play-store/restore"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json; charset=utf-8"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": false,\n  \"message\": \"App has no public key.\",\n  \"status\": 400\n}"}],"_postman_id":"d67cccec-b6c2-4cd7-8562-77d6ac179d71"}],"id":"32bcdf99-c75e-4563-b161-04c6b8dc8665","description":"<p>Validate App Store and Google Play purchases, or restore a user's earlier purchases to a profile.</p>\n<p>Purchase validation links a purchase to the paywall that led to it. Neither validation endpoint accepts <code>paywallSessionId</code>. Send the paywall and test details from Fetch Paywall as separate fields: <code>paywallId</code>, <code>abTestId</code>, <code>isExperiment</code>, and <code>aiPricingModelId</code>.</p>\n<p>All validation and restore requests return the full profile, including personal information and purchase access details. v2 returns fewer validation fields without personal information and does not offer restore endpoints.</p>\n<p><strong>Source:</strong> api/src/web-api/purchases/</p>\n","_postman_id":"32bcdf99-c75e-4563-b161-04c6b8dc8665","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","id":"261346bd-8303-45f1-9c1e-e1b68f0e656b","name":"Botsi API v1 (Legacy)","type":"collection"}}}],"auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]}},"event":[{"listen":"prerequest","script":{"type":"text/javascript","exec":["// Use the same variable precedence as the request's Authorization header.","const key = pm.variables.get('secretKey');","const raw = pm.request.body && pm.request.body.raw || '';","const missing = Array.from(new Set(Array.from(raw.matchAll(/\\{\\{([A-Za-z_][A-Za-z0-9_]*)\\}\\}/g), match => match[1])))","    .filter(name => pm.variables.get(name) === undefined || pm.variables.get(name) === null || pm.variables.get(name) === '');","let setupError = '';","if (!key) {","    setupError = 'Set secretKey in your Postman environment before sending requests.';","} else if (/^(Bearer|Api-Key)\\s/i.test(key)) {","    setupError = 'Use the secret key itself, without a Bearer or Api-Key prefix.';","} else if (pm.variables.get('baseUrl') !== 'https://app.botsi.com/api' || pm.variables.get('apiVersion') !== 'v1') {","    setupError = 'Use baseUrl https://app.botsi.com/api and apiVersion v1 for this production collection.';","} else if (missing.length) {","    setupError = 'Set these variables before sending the request: ' + missing.join(', ');","}","if (setupError) {","    console.error(setupError);","    pm.execution.skipRequest();","}"],"id":"9f0c3f32-491c-4d55-bb56-73de0ed570f0"}},{"listen":"test","script":{"type":"text/javascript","exec":["if (pm.response.code >= 400) {","    pm.test('Error contains ok, message, and status', () => {","        const error = pm.response.json();","        pm.expect(error).to.have.property('ok', false);","        pm.expect(error.message).to.be.a('string');","        pm.expect(error.status).to.eql(pm.response.code);","    });","}"],"id":"4957b284-790f-44a3-91b1-04ca10f85de4"}}],"variable":[{"key":"baseUrl","value":"https://app.botsi.com/api","type":"string","description":"Use `https://app.botsi.com/api` for the production API. Combined with `apiVersion` to build request URLs. v1 is not available at `api.botsi.com`."},{"key":"apiVersion","value":"v1","type":"string","description":"The API version used in request URLs: `v1`."},{"key":"secretKey","value":"","type":"string","description":"Your app secret key from the dashboard App configuration page. Sent directly in the `Authorization` header, without `Bearer` or another prefix. Keep the saved value blank and set it in a Postman environment."},{"key":"customerUserId","value":"user-123","type":"string","description":"The user ID from your own system. Links the Botsi profile across devices. Requests using this ID look for the user within your app. v2 calls this field `appUserId`."},{"key":"profileId","value":"","type":"string","description":"Saved automatically by Create a Profile for use in later requests."},{"key":"placementId","value":"","type":"string","description":"The ID of a placement configured in your dashboard. Set this before running the collection."},{"key":"store","value":"app_store","type":"string","description":"Required by Fetch Paywall. Selects the product fields to return: `app_store`, `play_store`, `stripe`, `web2wave`, or `custom`."},{"key":"storeProductId","value":"","type":"string","description":"A product ID configured in Botsi for the selected store and attached to the paywall used by your placement. Used by Validate Apple Store Purchase. Set an App Store product ID before running that request."},{"key":"storeEnvironment","value":"production","type":"string","description":"Set to `production` for the production purchase workflow. Included explicitly in purchase validation requests."},{"key":"paywallSessionId","value":"","type":"string","description":"Saved automatically by Fetch Paywall. By default, valid for `paywall_shown` events for 24 hours after the fetch. Other event types accept it at any age."},{"key":"paywallId","value":"","type":"string","description":"Saved automatically from `data.id` by Fetch Paywall. Keep this variable outside quotation marks in request bodies because its value must be a number."},{"key":"abTestId","value":"","type":"string","description":"Saved automatically from `data.abTestId` by Fetch Paywall, or set to `0` if no A/B test was selected. Keep it outside quotation marks in request bodies because its value must be a number."},{"key":"aiPricingModelId","value":"","type":"string","description":"Saved automatically from `data.aiPricingModelId` by Fetch Paywall, or set to `0` if no AI Pricing model was selected. Keep it outside quotation marks in request bodies because its value must be a number."},{"key":"isExperiment","value":"","type":"string","description":"Saved automatically by Fetch Paywall. Keep it outside quotation marks in request bodies because its value must be `true` or `false`."},{"key":"customAttributeId","value":"","type":"string","description":"Saved automatically by Add Custom Attribute. Used as `attrId` by Update Custom Attribute."},{"key":"appleTransactionId","value":"","type":"string","description":"Transaction ID from the actual App Store purchase. Set before Apple validation."},{"key":"appleOriginalTransactionId","value":"","type":"string","description":"Original transaction ID from the actual App Store purchase. Set before Apple validation."},{"key":"appleReceipt","value":"","type":"string","description":"Full base64 app receipt for Apple restore. Keep the saved value blank and set it in a Postman environment. A transaction ID is not an app receipt."},{"key":"googlePurchaseToken","value":"","type":"string","description":"Token for the Google Play purchase being validated or restored. Keep the saved value blank and set it in a Postman environment."},{"key":"googleSubscriptionProductId","value":"","type":"string","description":"Google Play subscription product ID configured in Botsi for this app."},{"key":"googleOneTimeProductId","value":"","type":"string","description":"Google Play one-time product ID configured in Botsi for this app."},{"key":"googleBasePlanId","value":"","type":"string","description":"Base plan ID for the purchased Google Play subscription. Must match the store and Botsi product configuration."},{"key":"googleOfferId","value":"","type":"string","description":"Offer ID for the purchased Google Play free-trial offer. Set before running the Free Trial request."}]}