{"info":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","description":"<html><head></head><body><p>Every endpoint documented in the Botsi API reference, in the order the AI Pricing flow calls them.</p>\n<h2 id=\"setup\">Setup</h2>\n<ol>\n<li>Set the <code>secretKey</code> variable to your app’s secret key, from the App configuration page in the dashboard at <a href=\"https://app.botsi.com\">https://app.botsi.com</a>.</li>\n<li>Set <code>placementId</code> to a Placement configured in your dashboard.</li>\n<li>Run the requests top to bottom, or use the Collection Runner.</li>\n</ol>\n<p>Keep the key out of source control. Prefer setting <code>secretKey</code> in a Postman <strong>environment</strong> or as an initial-value-blank secret variable rather than saving it into the collection.</p>\n<h2 id=\"authentication\">Authentication</h2>\n<p>Every request sends the secret key in the <code>Authorization</code> header, configured once at the collection level and inherited by each request.</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code>Authorization: \nContent-Type: application/json\n</code></pre><p>The header value is the key itself. <strong>There is no <code>Bearer</code> and no <code>Api-Key</code> prefix.</strong> Adding a prefix is the most common cause of an authentication failure that looks like a wrong key.</p>\n<p>Never ship this key in a mobile binary or browser bundle: it authorizes every endpoint here, including reading Profiles and validating purchases.</p>\n<h2 id=\"the-call-sequence\">The call sequence</h2>\n<ol>\n<li><strong>Create a Profile</strong> for each user.</li>\n<li><strong>Publish custom attributes</strong> on that Profile, if you have additional user metadata.</li>\n<li><strong>Fetch the Paywall</strong> and persist the <code>paywallSessionId</code> it returns.</li>\n<li><strong>Send a <code>paywall_shown</code> event</strong> with that <code>paywallSessionId</code>.</li>\n<li><strong>Validate purchases</strong> if using IAP with App Store or Google Play.</li>\n</ol>\n<p>Requests are chained through collection variables. Create a Profile writes <code>profileId</code>; Fetch Paywall writes <code>paywallSessionId</code>, <code>paywallId</code>, <code>aiPricingModelId</code>, and <code>isExperiment</code>. Running the collection in order therefore needs no manual copying.</p>\n<p><code>paywallId</code>, <code>aiPricingModelId</code>, and <code>isExperiment</code> are interpolated <strong>unquoted</strong> in the purchase-validation bodies, because the API expects two numbers and a boolean. Keep them unquoted if you edit those bodies.</p>\n<h2 id=\"status-codes\">Status codes</h2>\n<p><code>2xx</code> means success, <code>4xx</code> means something in your request needs changing, and <code>5xx</code> means the failure was on Botsi’s side.</p>\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>The request succeeded. The body carries the requested data</td>\n</tr>\n<tr>\n<td><code>201</code></td>\n<td>The request succeeded and created a resource, such as a Profile</td>\n</tr>\n<tr>\n<td><code>204</code></td>\n<td>The request succeeded with nothing to return</td>\n</tr>\n<tr>\n<td><code>400</code></td>\n<td>A required parameter is missing: no <code>profileId</code>, or an empty <code>custom</code> array</td>\n</tr>\n<tr>\n<td><code>401</code></td>\n<td>No valid API key in the <code>Authorization</code> header</td>\n</tr>\n<tr>\n<td><code>403</code></td>\n<td>The key is valid but lacks permission for that resource</td>\n</tr>\n<tr>\n<td><code>404</code></td>\n<td>The resource does not exist</td>\n</tr>\n<tr>\n<td><code>422</code></td>\n<td>Well-formed but semantically wrong: a string where a number was expected</td>\n</tr>\n<tr>\n<td><code>429</code></td>\n<td>Rate limited. Back off and retry</td>\n</tr>\n<tr>\n<td><code>500</code> <code>502</code> <code>503</code> <code>504</code></td>\n<td>Server-side failure. Retry with backoff</td>\n</tr>\n</tbody>\n</table>\n</div><p><code>429</code> and <code>5xx</code> are the only codes worth retrying without changing the request first.</p>\n<h2 id=\"error-format\">Error format</h2>\n<p>Errors return a JSON object with a human-readable message and a machine-readable code:</p>\n<pre class=\"click-to-expand-wrapper is-snippet-wrapper\"><code class=\"language-json\">{\n  \"error\": \"Internal error\",\n  \"code\": \"paywall_fetch_failed\"\n}\n</code></pre>\n<p>Branch on <code>code</code>. The <code>error</code> string is for humans reading logs and may change.</p>\n<h2 id=\"one-exception-to-retrying\">One exception to retrying</h2>\n<p>A failed <strong>Fetch Paywall</strong> should fall back, not block. Retrying a <code>5xx</code> with backoff is correct for events and validation, where the data matters and arriving late is fine. In front of a user waiting on a Paywall, serve your baseline variant instead.</p>\n<h2 id=\"fidelity-notes\">Fidelity notes</h2>\n<p>Request bodies and response examples are taken verbatim from the API reference. Two places required judgement, each flagged in the relevant description:</p>\n<ul>\n<li><strong>401 and 400 bodies</strong> are shown in the documented <code>{ error, code }</code> envelope, but the docs do not print the specific strings for those cases except on Send Paywall Shown Event, whose four messages are verbatim.</li>\n<li><strong><code>recurrenceMode</code> values</strong> on Google pricing phases follow the Google Play Billing Library convention; the Botsi docs require the field but do not enumerate its values.</li>\n</ul>\n</body></html>","schema":"https://schema.getpostman.com/json/collection/v2.0.0/collection.json","toc":[],"owner":"2219874","collectionId":"e0698370-1beb-443b-968a-1ac87f4be07e","publishedId":"2sBYAvwB8S","public":true,"customColor":{"top-bar":"FFFFFF","right-sidebar":"303030","highlight":"FF6C37"},"publishDate":"2026-09-04T15:22:33.000Z"},"item":[{"name":"1. Profiles","item":[{"name":"Create a Profile","event":[{"listen":"test","script":{"type":"text/javascript","packages":{},"exec":["pm.test('Status is 200 OK or 201 Created', () => {","    pm.expect(pm.response.code).to.be.oneOf([200, 201]);","});","","const body = pm.response.json();","","pm.test('Response uses the documented { ok, data } envelope', () => {","    pm.expect(body).to.have.property('ok', true);","});","","pm.test('Response carries a profileId', () => {","    pm.expect(body.data).to.have.property('profileId').that.is.a('string');","});","","// Chain the new Profile into the rest of the collection.","pm.collectionVariables.set('profileId', body.data.profileId);","if (body.data.appUserId) {","    pm.collectionVariables.set('appUserId', body.data.appUserId);","}"],"id":"f7d577ec-e5fe-4af9-9d53-2353ea932fb1"}}],"id":"ff7d9df9-1bde-427a-9954-4d42556af769","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"country\": \"US\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"platform\": \"ios\",\n  \"appVersion\": \"1.0.0\",\n  \"appUserId\": \"user-123\",\n  \"locale\": \"en_US\",\n  \"appBuild\": \"100\",\n  \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n  \"ipAddress\": \"203.0.113.7\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/profiles","description":"<p>Call this on first app launch. It creates the Profile every other operation depends on — AI Pricing, subscriptions, and Paywall tracking all need one to exist.</p>\n<p><strong>Required:</strong> <code>country</code>, <code>device</code>, <code>os</code>, <code>platform</code>, <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>, and <code>stripe</code>.</p>\n<p><code>appUserId</code> is documented as <em>Recommended</em> rather than required: it ties the Botsi Profile to the user record in your own system and lets the same person be recognised on a second device.</p>\n<p><strong>Optional and stored:</strong> <code>appUserId</code>, <code>locale</code>, <code>appBuild</code>, <code>advertisingId</code>, <code>ipAddress</code>, <code>email</code>, <code>phone</code>, <code>gender</code>, <code>idfa</code>, <code>username</code>.</p>\n<p><strong>Accepted but discarded</strong> — the API strips any field it does not model, so these are silently ignored rather than rejected: <code>currency</code>, <code>billingCountry</code>, <code>timezone</code>, <code>osVersion</code>, <code>sessionId</code>, <code>pushToken</code>. They are not stored and never appear in the response.</p>\n<p><code>country</code> takes a 2-letter alpha-2 code, or an uppercase 3-letter alpha-3 code that Botsi can map. An unmappable 3-letter value is rejected with <code>400</code>.</p>\n<p>The test script stores <code>data.profileId</code> into the <code>profileId</code> collection variable, so every later request in this collection resolves against the Profile you just created.</p>\n<p><em>Source: content/api-reference/create-profile.md</em></p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}},"urlObject":{"path":["v2","profiles"],"host":["https://api.botsi.com"],"query":[],"variable":[]}},"response":[{"id":"579544fe-9577-454f-a46b-677fc37d5517","name":"201 Created","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"country\": \"US\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"platform\": \"ios\",\n  \"appVersion\": \"1.0.0\",\n  \"appUserId\": \"user-123\",\n  \"locale\": \"en_US\",\n  \"appBuild\": \"100\",\n  \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n  \"ipAddress\": \"203.0.113.7\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/profiles"},"status":"Created","code":201,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n    \"appUserId\": \"user-123\",\n    \"country\": \"US\",\n    \"locale\": \"en_US\",\n    \"ip\": \"203.0.113.7\",\n    \"device\": \"iPhone 15 Pro Max\",\n    \"os\": \"iOS 17.0\",\n    \"platform\": \"ios\",\n    \"appVersion\": \"1.0.0\",\n    \"appBuild\": \"100\",\n    \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n    \"createdAt\": \"2026-08-27T10:30:00.000Z\",\n    \"updatedAt\": \"2026-08-27T10:30:00.000Z\"\n  }\n}"},{"id":"21f0dcb2-ec3c-43af-a290-3ce7a4528e73","name":"401 Unauthorized — missing or prefixed key","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"country\": \"US\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"platform\": \"ios\",\n  \"appVersion\": \"1.0.0\",\n  \"appUserId\": \"user-123\",\n  \"locale\": \"en_US\",\n  \"appBuild\": \"100\",\n  \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n  \"ipAddress\": \"203.0.113.7\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/profiles"},"status":"Unauthorized","code":401,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"error\": \"No valid API key provided.\",\n  \"code\": \"unauthorized\"\n}"},{"id":"69980851-3c4e-4b44-91d4-55e1041d0a7a","name":"400 Bad Request — unmappable country","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"country\": \"US\",\n  \"device\": \"iPhone 15 Pro Max\",\n  \"os\": \"iOS 17.0\",\n  \"platform\": \"ios\",\n  \"appVersion\": \"1.0.0\",\n  \"appUserId\": \"user-123\",\n  \"locale\": \"en_US\",\n  \"appBuild\": \"100\",\n  \"advertisingId\": \"6D92078A-8246-4BA4-AE5B-76104861E7DC\",\n  \"ipAddress\": \"203.0.113.7\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/profiles"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"error\": \"country must be a 2-letter ISO 3166-1 alpha-2 code or an uppercase 3-letter alpha-3 code\",\n  \"code\": \"bad_request\"\n}"}],"_postman_id":"ff7d9df9-1bde-427a-9954-4d42556af769"},{"name":"Add Custom Attributes","event":[{"listen":"test","script":{"type":"text/javascript","packages":{},"exec":["pm.test('Status is 200 OK or 201 Created', () => {","    pm.expect(pm.response.code).to.be.oneOf([200, 201]);","});","","const body = pm.response.json();","","pm.test('Response uses the documented { ok, data } envelope', () => {","    pm.expect(body).to.have.property('ok', true);","});","","pm.test('Every submitted attribute comes back with an id', () => {","    const sent = JSON.parse(pm.request.body.raw).custom.length;","    pm.expect(body.data).to.be.an('array').with.lengthOf(sent);","    body.data.forEach((attribute) => pm.expect(attribute).to.have.property('id'));","});"],"id":"c57ace06-60e2-460a-a725-47aebc943820"}}],"id":"64336719-9506-4158-8e6b-43fe195d2d65","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\": \"user_level\",\n      \"value\": \"premium\"\n    },\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://api.botsi.com/v2/custom-attributes","description":"<p>Custom attributes enrich a Profile with metadata. Provide as much as possible for better results from the Botsi AI Pricing Models.</p>\n<p><strong>Required:</strong> <code>profileId</code>, and a <code>custom</code> array holding at least one <code>{ key, value }</code> object. Both <code>key</code> and <code>value</code> are strings.</p>\n<p>An empty <code>custom</code> array is rejected with <code>400</code>.</p>\n<p><em>Source: content/api-reference/add-profile-attributes.md</em></p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}},"urlObject":{"path":["v2","custom-attributes"],"host":["https://api.botsi.com"],"query":[],"variable":[]}},"response":[{"id":"b4d05833-f526-4450-bc3d-c4e324b761d9","name":"200 OK","originalRequest":{"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\": \"user_level\",\n      \"value\": \"premium\"\n    },\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://api.botsi.com/v2/custom-attributes"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": [\n    {\n      \"id\": \"abc123\",\n      \"key\": \"user_level\",\n      \"value\": \"premium\"\n    },\n    {\n      \"id\": \"def456\",\n      \"key\": \"subscription_type\",\n      \"value\": \"annual\"\n    },\n    {\n      \"id\": \"ghi789\",\n      \"key\": \"referral_source\",\n      \"value\": \"facebook\"\n    }\n  ]\n}"},{"id":"812897bf-aca2-4b7d-8c4a-8eed2cb2b55f","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\": \"\",\n  \"custom\": [\n    {\n      \"key\": \"user_level\",\n      \"value\": \"premium\"\n    },\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://api.botsi.com/v2/custom-attributes"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"error\": \"custom must contain at least 1 elements\",\n  \"code\": \"bad_request\"\n}"},{"id":"35825a23-e5b3-47bb-a06d-3fb75ff2fc6c","name":"404 Not Found — unknown profile","originalRequest":{"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\": \"user_level\",\n      \"value\": \"premium\"\n    },\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://api.botsi.com/v2/custom-attributes"},"status":"Not Found","code":404,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"error\": \"No profile found by id: 0072102a-c00c-4ea5-9271-1b6e975f2d63\",\n  \"code\": \"not_found\"\n}"}],"_postman_id":"64336719-9506-4158-8e6b-43fe195d2d65"},{"name":"Update Custom Attributes","event":[{"listen":"test","script":{"type":"text/javascript","packages":{},"exec":["pm.test('Status is 200 OK', () => pm.response.to.have.status(200));","","const body = pm.response.json();","","pm.test('Response uses the documented { ok, data } envelope', () => {","    pm.expect(body).to.have.property('ok', true);","});","","pm.test('Updated values are echoed back', () => {","    const sent = JSON.parse(pm.request.body.raw).custom;","    sent.forEach((attribute) => {","        const returned = body.data.find((item) => item.key === attribute.key);","        pm.expect(returned, `attribute ${attribute.key}`).to.not.be.undefined;","        pm.expect(returned.value).to.eql(attribute.value);","    });","});"],"id":"c5ca2de0-e943-4a81-bf8b-36afdc390a52"}}],"id":"34cbb9bb-bad0-4e64-bd22-d7e261616902","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  \"custom\": [\n    {\n      \"key\": \"user_level\",\n      \"value\": \"vip\"\n    },\n    {\n      \"key\": \"subscription_type\",\n      \"value\": \"annual\"\n    }\n  ]\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/custom-attributes","description":"<p>Update existing attribute values, as when a user upgrades from <code>premium</code> to <code>vip</code>.</p>\n<p>Identify the user with <code>profileId</code>. Each attribute is targeted by <code>custom[].key</code>.</p>\n<p><strong>Required:</strong> <code>profileId</code>, and a <code>custom</code> array holding at least one <code>{ key, value }</code> object.</p>\n<p>An empty <code>custom</code> array is rejected with <code>400</code>.</p>\n<p><em>Source: content/api-reference/update-profile-attributes.md</em></p>\n<p>The update is <strong>atomic</strong>: if any key in <code>custom</code> is missing on the Profile, nothing is written and the <code>404</code> names the missing keys. Use POST to create attributes.</p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}},"urlObject":{"path":["v2","custom-attributes"],"host":["https://api.botsi.com"],"query":[],"variable":[]}},"response":[{"id":"351ba572-448a-449f-b259-e0397d026298","name":"200 OK","originalRequest":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"custom\": [\n    {\n      \"key\": \"user_level\",\n      \"value\": \"vip\"\n    },\n    {\n      \"key\": \"subscription_type\",\n      \"value\": \"annual\"\n    }\n  ]\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/custom-attributes"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": [\n    {\n      \"id\": \"abc123\",\n      \"key\": \"user_level\",\n      \"value\": \"vip\"\n    },\n    {\n      \"id\": \"def456\",\n      \"key\": \"subscription_type\",\n      \"value\": \"annual\"\n    }\n  ]\n}"},{"id":"1fbf265b-1062-42a5-98ef-b1c5181219af","name":"404 Not Found — key missing on the profile","originalRequest":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"custom\": [\n    {\n      \"key\": \"user_level\",\n      \"value\": \"vip\"\n    },\n    {\n      \"key\": \"subscription_type\",\n      \"value\": \"annual\"\n    }\n  ]\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/custom-attributes"},"status":"Not Found","code":404,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"error\": \"Custom attribute key(s) not found on profile 0072102a-c00c-4ea5-9271-1b6e975f2d63: user_level. PUT only updates existing attributes \\u2014 create them with POST /v2/custom-attributes.\",\n  \"code\": \"not_found\"\n}"}],"_postman_id":"34cbb9bb-bad0-4e64-bd22-d7e261616902"}],"id":"5099d256-27c6-4be5-9618-026c6f64ba7b","description":"<p>Create the record Botsi keeps for one of your users, and enrich it with metadata. Every other operation depends on a Profile existing.</p>\n<p><em>Source: content/api-reference/create-profile.md, add-profile-attributes.md, update-profile-attributes.md</em></p>\n","_postman_id":"5099d256-27c6-4be5-9618-026c6f64ba7b","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}}},{"name":"2. Paywalls","item":[{"name":"Fetch Paywall","event":[{"listen":"test","script":{"type":"text/javascript","packages":{},"exec":["pm.test('Status is 200 OK', () => pm.response.to.have.status(200));","","const body = pm.response.json();","","pm.test('Response uses the documented { ok, data } envelope', () => {","    pm.expect(body).to.have.property('ok', true);","});","","pm.test('Response carries the fields the later calls need', () => {","    const data = body.data;","    pm.expect(data).to.have.property('id').that.is.a('number');","    pm.expect(data).to.have.property('paywallSessionId').that.is.a('string');","    pm.expect(data).to.have.property('aiPricingModelId');","    pm.expect(data).to.have.property('isExperiment');","    pm.expect(data).to.have.property('paywallProducts').that.is.an('array');","});","","pm.test('Each product carries a keyed row id and one block per store', () => {","    const products = body.data.paywallProducts;","    pm.expect(products).to.be.an('array').that.is.not.empty;","","    for (const product of products) {","        pm.expect(product).to.have.property('paywallProductId').that.is.a('number');","        pm.expect(product).to.have.property('name').that.is.a('string');","        pm.expect(product).to.have.property('period').that.is.a('string');","","        // All five blocks are always present as a key: an object, or null when","        // the product has no identity on that store (BTSI-191).","        for (const store of ['appStore', 'playStore', 'stripe', 'web2wave', 'custom']) {","            pm.expect(product, `${store} key is always present`).to.have.property(store);","","            if (product[store] !== null) {","                // productId inside a block is non-nullable: its presence is what","                // makes the block exist at all.","                pm.expect(product[store].productId, `${store}.productId`).to.be.a('string').and.not.empty;","            }","        }","    }","","    // The flat productId and offer fields moved inside the store blocks, and","    // Botsi has never modelled a store-level discount.","    pm.expect(products[0]).to.not.have.property('productId');","    pm.expect(products[0]).to.not.have.property('discountId');","});","","// Attribution chain. paywallId, aiPricingModelId and isExperiment are written","// as bare literals so they interpolate unquoted into the validation bodies,","// where the API expects a number and a boolean rather than strings.","pm.collectionVariables.set('paywallSessionId', body.data.paywallSessionId);","pm.collectionVariables.set('paywallId', String(body.data.id));","pm.collectionVariables.set('aiPricingModelId', String(body.data.aiPricingModelId));","pm.collectionVariables.set('isExperiment', String(body.data.isExperiment));","","// The token is only valid for a paywall_shown event for 24 hours.","console.log('paywallSessionId expires', new Date(Date.now() + 86400000).toISOString());"],"id":"f48ec667-db92-460e-a153-5f4bce79bf36"}}],"id":"19963bad-1843-4676-b54a-c3bd22efbfa8","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  \"placementId\": \"ai-placement-id\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/paywall","description":"<p>Once a Profile exists, call this at any time to retrieve the Paywall variant the AI Pricing Model selected for that user. The response identifies which of your pre-configured paywalls to display.</p>\n<p><strong>Required:</strong> <code>profileId</code>, <code>placementId</code>. Those two fields are the whole request.</p>\n<p>Any other field is stripped before validation rather than rejected, so a client still sending <code>store</code>, <code>locale</code>, <code>country</code>, <code>ip</code> or <code>storeCountry</code> keeps working — those values are ignored, and resolution uses the Profile data Botsi already holds.</p>\n<p>One call answers for <strong>every</strong> store, so a multi-store integrator fetches a Placement's Paywall once rather than once per store. That also means one A/B or AI-pricing decision and one <code>paywallSessionId</code> per impression.</p>\n<h3 id=\"product-shape\">Product shape</h3>\n<p>Each entry in <code>data.paywallProducts</code> describes one product attached to the Paywall, followed by one nested block per store:</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>paywallProductId</code></td>\n<td>Number identifying this entry within <code>paywallProducts</code></td>\n</tr>\n<tr>\n<td><code>name</code></td>\n<td>Product name as configured in Botsi</td>\n</tr>\n<tr>\n<td><code>period</code></td>\n<td><code>weekly</code>, <code>monthly</code>, <code>2_months</code>, <code>3_months</code>, <code>6_months</code>, <code>annual</code>, <code>lifetime</code>, <code>consumable</code>, or <code>non_subscription</code></td>\n</tr>\n<tr>\n<td><code>appStore</code></td>\n<td><code>productId</code>, <code>offerId</code>, <code>promotionalOfferId</code>, <code>offerType</code> — or <code>null</code></td>\n</tr>\n<tr>\n<td><code>playStore</code></td>\n<td><code>productId</code>, <code>basePlanId</code>, <code>offerId</code> — or <code>null</code></td>\n</tr>\n<tr>\n<td><code>stripe</code></td>\n<td><code>productId</code>, <code>priceId</code>, <code>trialPeriodDays</code> — or <code>null</code></td>\n</tr>\n<tr>\n<td><code>web2wave</code></td>\n<td><code>productId</code> — or <code>null</code></td>\n</tr>\n<tr>\n<td><code>custom</code></td>\n<td><code>productId</code> — or <code>null</code></td>\n</tr>\n</tbody>\n</table>\n</div><p>A block is <code>null</code> when the product has no identifier for that store, and <code>productId</code> <strong>inside</strong> a block is never <code>null</code>, so <code>if (!product.playStore)</code> is a well-defined \"not sold on Google Play\" check. Under the earlier flat shape, a product with no Google id and a genuine mapping bug both arrived as <code>productId: null</code>.</p>\n<p><code>paywallProductId</code> is the id of the row attaching the product to the Paywall, not the product's own id. One product can be attached to the same Paywall twice under different offers, and this keeps those entries distinguishable.</p>\n<p>The App Store offer fields come from the offer attached to <em>this entry</em>, not from the product. On Google Play, \"no base plan\" and \"no offer\" are stored as empty strings and returned as <code>null</code>; only a missing Google Play product id nulls the whole block.</p>\n<p>Botsi does not model a store-level discount or coupon concept, so there is no <code>discountId</code> — use <code>offerId</code> together with <code>offerType</code>.</p>\n<h3 id=\"timing\">Timing</h3>\n<p>Do not call this synchronously at render time. The response typically takes around 40 ms but can take up to 2 seconds depending on prediction speed. Trigger it early — immediately after the Profile is created is a good moment — and have the response cached before the user reaches the Paywall.</p>\n<h3 id=\"failure-behaviour\">Failure behaviour</h3>\n<p>A <code>5xx</code> here should <strong>fall back, not block</strong>. Retrying with backoff is right for events and validation, where arriving late is fine. In front of a user waiting on a Paywall, serve your baseline variant instead.</p>\n<p>The test script stores <code>paywallSessionId</code>, <code>id</code> (as <code>paywallId</code>), <code>aiPricingModelId</code>, and <code>isExperiment</code> into collection variables, which the event and purchase-validation requests then consume.</p>\n<p><em>Source: content/api-reference/fetch-paywall.md</em></p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}},"urlObject":{"path":["v2","paywall"],"host":["https://api.botsi.com"],"query":[],"variable":[]}},"response":[{"id":"747d75f5-0c38-4e8d-abab-73e41aeb04fd","name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"placementId\": \"ai-placement-id\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/paywall"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"id\": 42,\n    \"externalId\": \"paywall_premium_v2\",\n    \"name\": \"Premium Paywall\",\n    \"isExperiment\": true,\n    \"aiPricingModelId\": 32,\n    \"paywallSessionId\": \"v1.eyJhIjo0MDIxLCJ3Ijo5MDUsInAiOiJvbmJvYXJkaW5nIiwiaSI6MTc4NTMxMjAwMH0.QmzR1w\",\n    \"paywallProducts\": [\n      {\n        \"paywallProductId\": 907,\n        \"name\": \"Premium Monthly\",\n        \"period\": \"monthly\",\n        \"appStore\": {\n          \"productId\": \"com.example.premium.monthly\",\n          \"offerId\": \"intro-offer-7day\",\n          \"promotionalOfferId\": null,\n          \"offerType\": \"introductory\"\n        },\n        \"playStore\": {\n          \"productId\": \"premium_monthly\",\n          \"basePlanId\": \"monthly-base\",\n          \"offerId\": null\n        },\n        \"stripe\": {\n          \"productId\": \"prod_QxA1b2C3\",\n          \"priceId\": \"price_1OaBcMonthly\",\n          \"trialPeriodDays\": 7\n        },\n        \"web2wave\": {\n          \"productId\": \"w2w_premium_monthly\"\n        },\n        \"custom\": {\n            \"productId\": \"custom_premium_monthly\"\n        }\n      }\n    ]\n  }\n}"},{"id":"9ca68e62-697f-4c16-8832-ad6af29bfe82","name":"500 Internal Server Error — fall back to your baseline variant","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"placementId\": \"ai-placement-id\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/paywall"},"status":"Internal Server Error","code":500,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"error\": \"Server Error.\",\n  \"code\": \"internal_server_error\"\n}"},{"id":"af3b4876-40f6-414b-9003-d062ee8b95bd","name":"404 Not Found — unknown placement","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"profileId\": \"\",\n  \"placementId\": \"ai-placement-id\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/paywall"},"status":"Not Found","code":404,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"error\": \"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  \"code\": \"not_found\"\n}"}],"_postman_id":"19963bad-1843-4676-b54a-c3bd22efbfa8"}],"id":"aef788c3-7b3d-47b6-b674-d8f6a8552d7b","description":"<p>Retrieve the Paywall variant the AI Pricing Model selected for a user and Placement.</p>\n<p><em>Source: content/api-reference/fetch-paywall.md</em></p>\n","_postman_id":"aef788c3-7b3d-47b6-b674-d8f6a8552d7b","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}}},{"name":"3. Events","item":[{"name":"Send Paywall Shown Event","event":[{"listen":"test","script":{"type":"text/javascript","packages":{},"exec":["pm.test('Status is 200 OK', () => pm.response.to.have.status(200));","","const body = pm.response.json();","","pm.test('Response is the documented acknowledgement — { ok } with no data', () => {","    pm.expect(body).to.have.property('ok', true);\n    pm.expect(body).to.not.have.property('data');","});","","pm.test('A paywallSessionId was available to send', () => {","    pm.expect(pm.collectionVariables.get('paywallSessionId'), 'run Fetch Paywall first').to.be.a('string').and.not.empty;","});"],"id":"16cb7fb4-fc4d-47fe-98ce-15f816c64e96"}}],"id":"44c4e52f-d34f-4512-bebd-76b39fc45475","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"eventType\": \"paywall_shown\",\n  \"paywallSessionId\": \"\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/events","description":"<p>Call this immediately after the Paywall is displayed. The impression is what conversion is measured against, and what lets the model learn from the outcome.</p>\n<p><strong>The request body is a single JSON object.</strong> Sending a JSON array — v1's shape — is rejected with <code>400 Request body must be a single event object, not an array</code>.</p>\n<p><strong>The whole accepted field set is two fields:</strong> <code>eventType</code> (must be <code>paywall_shown</code>) and <code>paywallSessionId</code> (from <code>data.paywallSessionId</code> on the Fetch Paywall response). Everything else about the event — paywall, placement, A/B test, AI-pricing model, and the profile itself — is derived server-side from the token.</p>\n<p>Any other field is <strong>rejected, not ignored</strong>. Sending <code>profileId</code> or <code>customerUserId</code> gets a <code>400</code> naming the field: the signed token is the single source of identity, so a client value that disagrees with it is an error worth being told about rather than silently overridden.</p>\n<p>Impressions are <strong>not</strong> deduplicated. Sending the same event twice for one <code>paywallSessionId</code> records two impressions, so send it once per Paywall view.</p>\n<h3 id=\"working-with-the-token\">Working with the token</h3>\n<p>Every Fetch Paywall response includes a unique <code>paywallSessionId</code>. It is roughly 200 characters today with no fixed maximum — store it in <code>TEXT</code> or <code>VARCHAR(512)</code>. One token per Paywall fetch.</p>\n<p>A <code>paywall_shown</code> event is accepted for <strong>24 hours</strong> after the fetch that minted the token. Send it later and the request is rejected; fetch a new Paywall before reporting the view. Transactions do not expire this way.</p>\n<p><em>Source: content/api-reference/paywall-shown-event.md</em></p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}},"urlObject":{"path":["v2","events"],"host":["https://api.botsi.com"],"query":[],"variable":[]}},"response":[{"id":"9cb6238d-410e-440b-94d9-9f3f791999d9","name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"eventType\": \"paywall_shown\",\n  \"paywallSessionId\": \"\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/events"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true\n}"},{"id":"09992f29-b1f4-4b4a-b86c-c24bd39228d7","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  \"eventType\": \"paywall_shown\",\n  \"paywallSessionId\": \"\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/events"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"error\": \"paywallSessionId is not a valid Botsi paywall session token.\",\n  \"code\": \"bad_request\"\n}"},{"id":"4ce0ee28-c22a-4534-a2ff-ce7f4670e958","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  \"eventType\": \"paywall_shown\",\n  \"paywallSessionId\": \"\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/events"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"error\": \"paywallSessionId has expired for this event type. Fetch a new paywall before reporting the view.\",\n  \"code\": \"bad_request\"\n}"},{"id":"05ec1e53-85b7-4a18-be66-c6562603d597","name":"400 Bad Request — body sent as an array","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"[\n  {\n    \"eventType\": \"paywall_shown\",\n    \"paywallSessionId\": \"\"\n  }\n]","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/events"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"error\": \"Request body must be a single event object, not an array\",\n  \"code\": \"bad_request\"\n}"},{"id":"762c64bd-8fe9-4ffe-adf2-4794a1d59ed0","name":"400 Bad Request — identity field sent","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"eventType\": \"paywall_shown\",\n  \"paywallSessionId\": \"\",\n  \"profileId\": \"\"\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/events"},"status":"Bad Request","code":400,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"error\": \"profileId is not accepted on v2 events; the profile is taken from the paywallSessionId that minted the paywall\",\n  \"code\": \"bad_request\"\n}"}],"_postman_id":"44c4e52f-d34f-4512-bebd-76b39fc45475"}],"id":"9ca09b83-3dce-4de5-85ec-95dfd7c22c05","description":"<p>Record Paywall exposure, so the model can learn from what followed.</p>\n<p><em>Source: content/api-reference/paywall-shown-event.md</em></p>\n","_postman_id":"9ca09b83-3dce-4de5-85ec-95dfd7c22c05","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}}},{"name":"4. Purchases","item":[{"name":"Validate Apple Store Purchase","event":[{"listen":"test","script":{"type":"text/javascript","packages":{},"exec":["pm.test('Status is 200 OK', () => pm.response.to.have.status(200));","","const body = pm.response.json();","","pm.test('Response uses the documented { ok, data } envelope', () => {","    pm.expect(body).to.have.property('ok', true);","});","","pm.test('Response returns the updated Profile', () => {","    pm.expect(body.data).to.have.property('profileId');","    pm.expect(body.data).to.have.property('accessLevels');","    pm.expect(body.data).to.have.property('subscriptions');","});","","pm.test('Purchase stayed attached to the experiment that produced it', () => {","    const sent = JSON.parse(pm.request.body.raw);","    pm.expect(sent.paywallId, 'paywallId').to.be.a('number');","    pm.expect(sent.aiPricingModelId, 'aiPricingModelId').to.be.a('number');","    pm.expect(sent.isExperiment, 'isExperiment').to.be.a('boolean');","});"],"id":"573593fc-76b2-4d6f-a1ec-26b0a967bf19"}}],"id":"48bee783-c4a7-4fd1-88a5-942f7d078689","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"placementId\": \"ai-placement-id\",\n  \"paywallId\": 42,\n  \"source\": \"purchasing\",\n  \"productId\": \"premium_annual\",\n  \"isSubscription\": true,\n  \"originalTransactionId\": \"2000000261699248\",\n  \"environment\": \"sandbox\",\n  \"transactionId\": \"2000000871590381\",\n  \"profileId\": \"\",\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/purchases/apple-store/validate","description":"<p>Call this after a purchase completes in your app. It confirms the transaction with Apple and records the revenue against the Profile and the Paywall that produced it.</p>\n<p><strong>Required:</strong> <code>profileId</code>, <code>productId</code>, <code>transactionId</code>, <code>originalTransactionId</code>.</p>\n<p><strong>Optional:</strong> <code>placementId</code>, <code>paywallId</code>, <code>abTestId</code>, <code>isSubscription</code>, <code>environment</code>, <code>source</code>, <code>isExperiment</code>, <code>aiPricingModelId</code>.</p>\n<p><code>isExperiment</code> and <code>aiPricingModelId</code> are <strong>optional on v2</strong> (they are required on the SDK surface). Omitting them still validates the purchase — see the attribution note below for why you should send them anyway.</p>\n<p><code>source</code> is optional and defaults to <strong><code>purchasing</code></strong> when omitted. It is load-bearing: <code>restore</code> short-circuits validation entirely, and <code>purchasing</code> selects the deduplication key. Send it deliberately.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Parameter</th>\n<th>Type</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>transactionId</code></td>\n<td>string</td>\n<td>Transaction identifier from the StoreKit payment object</td>\n</tr>\n<tr>\n<td><code>originalTransactionId</code></td>\n<td>string</td>\n<td>Original transaction identifier from the StoreKit payment object</td>\n</tr>\n<tr>\n<td><code>productId</code></td>\n<td>string</td>\n<td>Product identifier from the payment object. Must match the Product configured in Botsi</td>\n</tr>\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>The internal Paywall ID, from <code>data.id</code> on the Fetch Paywall response</td>\n</tr>\n<tr>\n<td><code>isExperiment</code></td>\n<td>boolean</td>\n<td>The value Fetch Paywall returned</td>\n</tr>\n<tr>\n<td><code>aiPricingModelId</code></td>\n<td>number</td>\n<td>The model ID Fetch Paywall returned</td>\n</tr>\n<tr>\n<td><code>environment</code></td>\n<td>string</td>\n<td><code>production</code> for live apps, <code>sandbox</code> for testing</td>\n</tr>\n<tr>\n<td><code>profileId</code></td>\n<td>string</td>\n<td><strong>Required.</strong> Botsi profile ID from Create a Profile</td>\n</tr>\n<tr>\n<td><code>isSubscription</code></td>\n<td>boolean</td>\n<td>Optional. Whether the purchase is a subscription</td>\n</tr>\n<tr>\n<td><code>source</code></td>\n<td>string</td>\n<td>Optional. <code>restore</code>, <code>purchasing</code>, or <code>observing</code></td>\n</tr>\n</tbody>\n</table>\n</div><h3 id=\"attribution-fields-are-not-optional-in-practice\">Attribution fields are not optional in practice</h3>\n<p>This endpoint does <strong>not</strong> accept <code>paywallSessionId</code>. The token shortcut applies to Send Paywall Shown Event only. Keep passing <code>paywallId</code>, <code>isExperiment</code>, and <code>aiPricingModelId</code> exactly as Fetch Paywall returned them.</p>\n<p>Omit them and the purchase still validates, but it detaches from the experiment that produced it, so the model gets no credit for the revenue it earned.</p>\n<p><code>paywallId</code>, <code>aiPricingModelId</code>, and <code>isExperiment</code> are interpolated <strong>unquoted</strong> in the body below, because the API expects two numbers and a boolean. Keep them unquoted if you edit the body.</p>\n<h3 id=\"finding-the-identifiers\">Finding the identifiers</h3>\n<p><strong>StoreKit.</strong> <code>transactionId</code> and <code>originalTransactionId</code> come from the <code>SKPaymentTransaction</code> object. The Product identifier is <code>SKPaymentTransaction.payment.productIdentifier</code>, and the original price is on <code>SKProduct</code>.</p>\n<p><strong>StoreKit 2.</strong> All three come from the <code>Transaction</code> object, and the original price from <code>Product</code>.</p>\n<p><em>Source: content/api-reference/purchases/validate-apple.md</em></p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}},"urlObject":{"path":["v2","purchases","apple-store","validate"],"host":["https://api.botsi.com"],"query":[],"variable":[]}},"response":[{"id":"508ad5b2-362d-4662-b960-4725011951af","name":"200 OK","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  \"paywallId\": 42,\n  \"source\": \"purchasing\",\n  \"productId\": \"premium_annual\",\n  \"isSubscription\": true,\n  \"originalTransactionId\": \"2000000261699248\",\n  \"environment\": \"sandbox\",\n  \"transactionId\": \"2000000871590381\",\n  \"profileId\": \"\",\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/purchases/apple-store/validate"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"fff3c120-0ca8-480f-9b19-3955a5428e39\",\n    \"appUserId\": \"user-123\",\n    \"state\": \"subscribed\",\n    \"totalRevenueUsd\": 49.99,\n    \"accessLevels\": {},\n    \"subscriptions\": {},\n    \"nonSubscriptions\": {}\n  }\n}"}],"_postman_id":"48bee783-c4a7-4fd1-88a5-942f7d078689"},{"name":"Validate Google Play Purchase — subscription","event":[{"listen":"test","script":{"type":"text/javascript","packages":{},"exec":["pm.test('Status is 200 OK', () => pm.response.to.have.status(200));","","const body = pm.response.json();","","pm.test('Response uses the documented { ok, data } envelope', () => {","    pm.expect(body).to.have.property('ok', true);","});","","pm.test('Exactly one offer object was sent', () => {","    const sent = JSON.parse(pm.request.body.raw);","    const subscription = Object.prototype.hasOwnProperty.call(sent, 'subscriptionOfferDetails');","    const oneTime = Object.prototype.hasOwnProperty.call(sent, 'oneTimePurchaseOfferDetails');","    pm.expect(subscription !== oneTime, 'send one offer object, never both').to.be.true;","});"],"id":"fbb10558-dece-42b1-8154-a7876101c16c"}}],"id":"7d35bc7a-c541-4ccd-9984-4110ddff9eaa","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"placementId\": \"ai-placement-id\",\n  \"productId\": \"monthly\",\n  \"paywallId\": 42,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32,\n  \"environment\": \"sandbox\",\n  \"profileId\": \"\",\n  \"subscriptionOfferDetails\": {\n    \"basePlanId\": \"1month-sub\",\n    \"pricingPhases\": [\n      {\n        \"priceAmountMicros\": 359990000,\n        \"currencyCode\": \"UAH\",\n        \"billingPeriod\": \"P1M\",\n        \"recurrenceMode\": 1,\n        \"billingCycleCount\": 0\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/purchases/play-store/validate","description":"<p>Call this after a purchase completes in your app. It confirms the transaction with Google Play and records the revenue against the Profile and the Paywall that produced it.</p>\n<p><strong>Required:</strong> <code>profileId</code>, <code>productId</code>, <code>token</code>.</p>\n<p><strong>Optional:</strong> <code>placementId</code>, <code>paywallId</code>, <code>abTestId</code>, <code>isSubscription</code>, <code>environment</code>, <code>isExperiment</code>, <code>aiPricingModelId</code>, and exactly one of <code>subscriptionOfferDetails</code> / <code>oneTimePurchaseOfferDetails</code>.</p>\n<p><code>isExperiment</code> and <code>aiPricingModelId</code> are <strong>optional on v2</strong> (required on the SDK surface).</p>\n<p>⚠️ <strong>Send <code>environment</code> explicitly.</strong> It has no default: a missing value is treated as non-production, which silently drops the revenue event. <code>production</code> for live apps, <code>sandbox</code> for testing.</p>\n<div class=\"click-to-expand-wrapper is-table-wrapper\"><table>\n<thead>\n<tr>\n<th>Parameter</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 token uniquely identifying this purchase for an item and user pair</td>\n</tr>\n<tr>\n<td><code>productId</code></td>\n<td>string</td>\n<td>Must match the Play Store Product ID configured in Botsi. Use <code>sourceProductId</code> from the Products response</td>\n</tr>\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>The internal Paywall ID, from <code>data.id</code> on the Fetch Paywall response</td>\n</tr>\n<tr>\n<td><code>isExperiment</code></td>\n<td>boolean</td>\n<td>The value Fetch Paywall returned</td>\n</tr>\n<tr>\n<td><code>aiPricingModelId</code></td>\n<td>number</td>\n<td>The model ID Fetch Paywall returned</td>\n</tr>\n<tr>\n<td><code>environment</code></td>\n<td>string</td>\n<td><code>production</code> for live apps, <code>sandbox</code> for testing</td>\n</tr>\n<tr>\n<td><code>profileId</code></td>\n<td>string</td>\n<td><strong>Required.</strong> Botsi profile ID from Create a Profile</td>\n</tr>\n<tr>\n<td><code>subscriptionOfferDetails</code></td>\n<td>object</td>\n<td>For a subscription. From <code>ProductDetails.getSubscriptionOfferDetails()</code>, usually the first item</td>\n</tr>\n<tr>\n<td><code>oneTimePurchaseOfferDetails</code></td>\n<td>object</td>\n<td>For a one-time purchase</td>\n</tr>\n</tbody>\n</table>\n</div><p><code>subscriptionOfferDetails</code> and <code>oneTimePurchaseOfferDetails</code> are <strong>mutually exclusive</strong> — send one, never both.</p>\n<p><strong><code>subscriptionOfferDetails</code>:</strong> <code>basePlanId</code> (string), <code>offerId</code> (string, if the Product has an offer configured in the store), <code>pricingPhases</code> (array).</p>\n<p><strong>Each <code>pricingPhases</code> entry:</strong> <code>priceAmountMicros</code> (number — 1,000,000 micro-units is one unit of currency), <code>currencyCode</code> (ISO 4217), <code>billingPeriod</code> (ISO 8601), <code>recurrenceMode</code> (number), <code>billingCycleCount</code> (number).</p>\n<p><strong><code>oneTimePurchaseOfferDetails</code>:</strong> <code>priceAmountMicros</code> (number, e.g. <code>9990000</code> for $9.99), <code>currencyCode</code> (ISO 4217).</p>\n<h3 id=\"attribution-fields-are-not-optional-in-practice\">Attribution fields are not optional in practice</h3>\n<p>This endpoint does <strong>not</strong> accept <code>paywallSessionId</code>. The token shortcut applies to Send Paywall Shown Event only. Keep passing <code>paywallId</code>, <code>isExperiment</code>, and <code>aiPricingModelId</code> exactly as Fetch Paywall returned them.</p>\n<p>Omit them and the purchase still validates, but it detaches from the experiment that produced it, so the model gets no credit for the revenue it earned.</p>\n<p><code>paywallId</code>, <code>aiPricingModelId</code>, and <code>isExperiment</code> are interpolated <strong>unquoted</strong> in the body below, because the API expects two numbers and a boolean. Keep them unquoted if you edit the body.</p>\n<h3 id=\"finding-the-token\">Finding the token</h3>\n<p>The purchase token comes from <code>getPurchaseToken()</code> in the Google Play Billing Library.</p>\n<p><em>Source: content/api-reference/purchases/validate-google.md</em></p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}},"urlObject":{"path":["v2","purchases","play-store","validate"],"host":["https://api.botsi.com"],"query":[],"variable":[]}},"response":[{"id":"97a97e98-ef27-4a16-9cea-6490ab8ce8cd","name":"200 OK (shape inferred — see description)","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"placementId\": \"ai-placement-id\",\n  \"productId\": \"monthly\",\n  \"paywallId\": 42,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32,\n  \"environment\": \"sandbox\",\n  \"profileId\": \"\",\n  \"subscriptionOfferDetails\": {\n    \"basePlanId\": \"1month-sub\",\n    \"pricingPhases\": [\n      {\n        \"priceAmountMicros\": 359990000,\n        \"currencyCode\": \"UAH\",\n        \"billingPeriod\": \"P1M\",\n        \"recurrenceMode\": 1,\n        \"billingCycleCount\": 0\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/purchases/play-store/validate"},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"}],"cookie":[],"responseTime":null,"body":"{\n  \"ok\": true,\n  \"data\": {\n    \"profileId\": \"55bbd5f3-6de8-4d3b-8a55-006e6af6e3dd\",\n    \"appUserId\": \"user-123\",\n    \"state\": \"subscribed\",\n    \"totalRevenueUsd\": 8.72,\n    \"accessLevels\": {},\n    \"subscriptions\": {},\n    \"nonSubscriptions\": {}\n  }\n}"}],"_postman_id":"7d35bc7a-c541-4ccd-9984-4110ddff9eaa"},{"name":"Validate Google Play Purchase — subscription with free trial","event":[{"listen":"test","script":{"type":"text/javascript","packages":{},"exec":["pm.test('Status is 200 OK', () => pm.response.to.have.status(200));","","const body = pm.response.json();","","pm.test('Response uses the documented { ok, data } envelope', () => {","    pm.expect(body).to.have.property('ok', true);","});","","pm.test('A zero-priced phase is present, so the trial is not booked as revenue', () => {","    const sent = JSON.parse(pm.request.body.raw);","    const phases = sent.subscriptionOfferDetails.pricingPhases;","    pm.expect(phases.some((phase) => phase.priceAmountMicros === 0)).to.be.true;","});"],"id":"949ff502-b86f-4640-881f-db27eb17fe3b"}}],"id":"231af14c-36e3-4870-84fa-2378045e20bb","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"placementId\": \"ai-placement-id\",\n  \"productId\": \"monthly\",\n  \"paywallId\": 42,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32,\n  \"environment\": \"sandbox\",\n  \"profileId\": \"\",\n  \"subscriptionOfferDetails\": {\n    \"basePlanId\": \"1month-sub\",\n    \"offerId\": \"freetrial-7d\",\n    \"pricingPhases\": [\n      {\n        \"priceAmountMicros\": 0,\n        \"currencyCode\": \"UAH\",\n        \"billingPeriod\": \"P1W\",\n        \"recurrenceMode\": 2,\n        \"billingCycleCount\": 1\n      },\n      {\n        \"priceAmountMicros\": 359990000,\n        \"currencyCode\": \"UAH\",\n        \"billingPeriod\": \"P1M\",\n        \"recurrenceMode\": 1,\n        \"billingCycleCount\": 0\n      }\n    ]\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/purchases/play-store/validate","description":"<p>Same endpoint as the plain subscription request, sent with the zero-priced phase that identifies a free trial.</p>\n<p><strong>Botsi identifies a free trial by a <code>pricingPhases</code> element with <code>priceAmountMicros</code> set to <code>0</code>, alongside its usual <code>billingPeriod</code>, <code>recurrenceMode</code>, and <code>billingCycleCount</code>. Leave that phase out and the event is read as a paid subscription, which inflates your revenue statistics with money nobody paid.</strong></p>\n<p>The zero-priced phase comes first, followed by the phase that charges once the trial ends.</p>\n<p>Note on values: the docs specify that <code>recurrenceMode</code> and <code>billingCycleCount</code> must be present but do not enumerate their valid values. The <code>recurrenceMode: 2</code> / <code>billingCycleCount: 1</code> pairing on the trial phase follows the Google Play Billing Library convention for a finite recurring phase, and <code>recurrenceMode: 1</code> the convention for infinite recurring. Confirm against the <code>ProductDetails</code> object your app actually receives.</p>\n<p><em>Source: content/api-reference/purchases/validate-google.md</em></p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}},"urlObject":{"path":["v2","purchases","play-store","validate"],"host":["https://api.botsi.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"231af14c-36e3-4870-84fa-2378045e20bb"},{"name":"Validate Google Play Purchase — one-time","event":[{"listen":"test","script":{"type":"text/javascript","packages":{},"exec":["pm.test('Status is 200 OK', () => pm.response.to.have.status(200));","","const body = pm.response.json();","","pm.test('Response uses the documented { ok, data } envelope', () => {","    pm.expect(body).to.have.property('ok', true);","});","","pm.test('Exactly one offer object was sent', () => {","    const sent = JSON.parse(pm.request.body.raw);","    const subscription = Object.prototype.hasOwnProperty.call(sent, 'subscriptionOfferDetails');","    const oneTime = Object.prototype.hasOwnProperty.call(sent, 'oneTimePurchaseOfferDetails');","    pm.expect(subscription !== oneTime, 'send one offer object, never both').to.be.true;","});"],"id":"70a017e9-8faa-4c29-b532-4f05d04bc02d"}}],"id":"a8eba313-a462-4127-9359-216730c0efac","protocolProfileBehavior":{"disableBodyPruning":true},"request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"body":{"mode":"raw","raw":"{\n  \"token\": \"algojlnkodhhjceoicbpjngh.AO-J1Owvcga8j2uUoKEpn5AnvpSFr8dxEFm0c6qk5\",\n  \"placementId\": \"ai-placement-id\",\n  \"productId\": \"lifetime_unlock\",\n  \"paywallId\": 42,\n  \"isExperiment\": true,\n  \"aiPricingModelId\": 32,\n  \"environment\": \"sandbox\",\n  \"profileId\": \"\",\n  \"oneTimePurchaseOfferDetails\": {\n    \"priceAmountMicros\": 9990000,\n    \"currencyCode\": \"USD\"\n  }\n}","options":{"raw":{"language":"json"}}},"url":"https://api.botsi.com/v2/purchases/play-store/validate","description":"<p>Same endpoint as the subscription requests, sent with <code>oneTimePurchaseOfferDetails</code> instead of <code>subscriptionOfferDetails</code>. The two are mutually exclusive — send one, never both.</p>\n<p><code>oneTimePurchaseOfferDetails</code> takes <code>priceAmountMicros</code> (<code>9990000</code> is $9.99) and <code>currencyCode</code> (ISO 4217).</p>\n<p><em>Source: content/api-reference/purchases/validate-google.md</em></p>\n","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}},"urlObject":{"path":["v2","purchases","play-store","validate"],"host":["https://api.botsi.com"],"query":[],"variable":[]}},"response":[],"_postman_id":"a8eba313-a462-4127-9359-216730c0efac"}],"id":"8ef60873-796b-406b-b58c-a740e0cfa6b1","description":"<p>Confirm a store transaction and attach it to the Paywall that produced it. Neither endpoint accepts <code>paywallSessionId</code> — pass <code>paywallId</code>, <code>isExperiment</code>, and <code>aiPricingModelId</code> through from Fetch Paywall instead. Both reply with a strict subset of the Profile: <code>profileId</code>, <code>appUserId</code>, <code>state</code>, <code>totalRevenueUsd</code>, <code>accessLevels</code>, <code>subscriptions</code>, <code>nonSubscriptions</code> — never the PII the Profile row carries.</p>\n<p><em>Source: content/api-reference/purchases/validate-apple.md, validate-google.md</em></p>\n","_postman_id":"8ef60873-796b-406b-b58c-a740e0cfa6b1","auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]},"isInherited":true,"source":{"_postman_id":"e0698370-1beb-443b-968a-1ac87f4be07e","id":"e0698370-1beb-443b-968a-1ac87f4be07e","name":"Botsi API v2","type":"collection"}}}],"auth":{"type":"apikey","apikey":{"basicConfig":[{"key":"key","value":"Authorization"},{"key":"value","value":""}]}},"event":[{"listen":"prerequest","script":{"type":"text/javascript","packages":{},"exec":["// Fail loudly rather than reading a 401 as a wrong key.","const key = pm.collectionVariables.get('secretKey') || pm.environment.get('secretKey');","if (!key) {","    throw new Error('Set the secretKey variable to your app secret key before sending requests.');","}","if (/^(Bearer|Api-Key)\\s/i.test(key)) {","    throw new Error('Send the key raw. Botsi expects no Bearer or Api-Key prefix in the Authorization header.');","}"],"id":"f7d5cf3f-f998-4791-b7bd-1735011e2f20"}}],"variable":[{"key":"baseUrl","value":"https://api.botsi.com","type":"string","description":"API host. Combined with apiVersion to form the documented base URL."},{"key":"apiVersion","value":"v2","type":"string","description":"API version segment."},{"key":"secretKey","value":"","type":"string","description":"Your app secret key, from the App configuration page in the dashboard. Sent raw in the Authorization header with no scheme prefix. Leave the stored value blank and set it per environment."},{"key":"profileId","value":"","type":"string","description":"Set automatically by Create a Profile."},{"key":"appUserId","value":"user-123","type":"string","description":"Your own internal app user ID, which links the Profile across devices."},{"key":"placementId","value":"ai-placement-id","type":"string","description":"A Placement configured in your dashboard. Set this before running."},{"key":"storeEnvironment","value":"sandbox","type":"string","description":"production for live apps, sandbox for testing."},{"key":"paywallSessionId","value":"","type":"string","description":"Set automatically by Fetch Paywall. Valid for a paywall_shown event for 24 hours."},{"key":"paywallId","value":"42","type":"string","description":"Set automatically by Fetch Paywall from data.id. Interpolated unquoted, because the API expects a number."},{"key":"aiPricingModelId","value":"32","type":"string","description":"Set automatically by Fetch Paywall. Interpolated unquoted, because the API expects a number."},{"key":"isExperiment","value":"true","type":"string","description":"Set automatically by Fetch Paywall. Interpolated unquoted, because the API expects a boolean."}]}