Skip to content

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

Web2Wave

Integrate Botsi AI Pricing with Web2Wave to optimize web funnel pricing. Web2Wave handles quiz funnels, paywalls, and Stripe-based payments. Botsi decides which paywall each user should see.

The integration uses JavaScript inserted into your Web2Wave quiz flow (via custom JS and the window.w2w API), plus a webhook that reports purchases back to Botsi.

Complete the AI Pricing Model Setup first. This page assumes the Botsi side already works end to end.

Integration flow#

Web2Wave integration flow across Quiz start, Quiz complete, Before the paywall, and Paywall and checkout

  1. Create a Botsi Profile at quiz start.
  2. Send quiz answers and attributes after the quiz completes.
  3. Call Fetch Paywall → receive externalId and paywallSessionId.
  4. Write paywallSlug and Botsi IDs with w2w.set() so Web2Wave can route and webhooks can attribute.
  5. Web2Wave serves the paywall matched by that slug.
  6. Send paywall_shown when the paywall is displayed.
  7. Purchase webhook from Web2Wave → Botsi (matched by botsiProfileId and related properties).

See the full API sequence in the AI Pricing Model API.

Webhook setup#

Do this once per product. Skip if webhooks are already configured.

  1. In Web2Wave, open Edit Project → API & Webhooks and copy your webhook secret. Paste it into Botsi App Settings.

Web2Wave API and Webhook settings showing API key and webhook secret

Botsi App Settings showing Web2Wave webhook configuration

  1. Copy the webhook URL from Botsi settings into your Web2Wave project's API & Webhooks tab. Web2Wave authenticates delivery with the Webhooks-Token header (the project secret). See Webhook Documentation.
  2. Add these keys to the userProperties allowlist in Web2Wave so they are included when properties and subscription events are forwarded:
ip
screen_size
user_city_name
user_country_code
user_language
user_platform
user_state_name
user_zip_code
botsiProfileId
botsiPaywallId
botsiPlacementId
botsiIsExperiment
botsiAiPricingModelId
botsiPaywallSessionId

Web2Wave user properties configuration with Botsi fields

Values you set with w2w.set() travel in webhooks under user properties. Botsi needs the botsi* fields to match the purchase to the right Profile and Paywall decision.

Product setup#

Also once per product.

  1. Create a Stripe product in your Stripe dashboard.

Stripe product creation dashboard

  1. Sync the Stripe product in Web2Wave under Plans & Prices so it is available when you build paywalls.

Web2Wave Plans and Prices tab showing Stripe product sync

  1. Create a Botsi Product and set the Stripe price ID as the web2wave_product_id.

Botsi product setup with Stripe price ID as web2wave_product_id

Where each script goes#

Point in the flowScript
Quiz startCreate the Botsi Profile
After quiz completionSync custom attributes from quiz answers
Before the paywallFetch the predicted Paywall and set paywallSlug
Paywall displaySend the paywall_shown event
PurchaseNothing client-side — Web2Wave/Stripe handle payment; the webhook reports it

Set every botsi* value before the paywall shows

w2w.set() values are what webhooks forward under user properties. A missing value at purchase time cannot be recovered later. Set botsiProfileId, attribution fields, and paywallSlug before routing to the paywall.

Creating the Profile#

Insert at quiz start. Web2Wave exposes window.user_properties and window.user_id (also readable via w2w.get()). Use user_id as appUserId so Botsi can match the user.

(async function createBotsiProfile() {
  try {
    const p = window.user_properties || {};
    const u = window.user_id || '';
    const apiKey = 'key...'; // Replace with your Botsi secret key

    const payload = {
      appUserId: u,
      country: p.user_country_code,
      platform: 'stripe',
      locale: p.user_language,
      device: p.user_platform,
      os: /Mac OS X/.test(p.user_agent) ? 'macos'
        : /Windows/.test(p.user_agent) ? 'windows'
        : /iPhone|iPad/.test(p.user_agent) ? 'ios'
        : /Android/.test(p.user_agent) ? 'android'
        : 'unknown',
      appVersion: '2.3.4',
      appBuild: '1.2.3',
    };

    const res = await fetch('https://api.botsi.com/v2/profiles', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
        Authorization: `${apiKey}`,
      },
      body: JSON.stringify(payload),
    });

    if (!res.ok) throw new Error(`Botsi API error: ${res.status}`);
    const data = await res.json();

    if (data.ok && data.data?.profileId) {
      w2w.set('botsiProfileId', data.data.profileId);
    }
  } catch (err) {
    console.warn('Botsi profile creation failed silently:', err.message);
  }
})();

Syncing custom attributes#

Insert after quiz completion. Quiz answers, the initial URL, and geographic data feed LTV prediction — send as much relevant signal as you have. See Add Custom Attributes.

(async function syncBotsiProfile() {
  try {
    const p = window.user_properties || {};
    const u = window.user_id || '';
    const apiKey = 'key...';

    const payload = {
      appUserId: u,
      custom: [
        { key: 'has_app', value: p['has-app'] || '' },
        { key: 'web2app_funnels', value: p['web2app-funnels'] || '' },
        { key: 'initial_url', value: p.initial_url || '' },
        { key: 'ip', value: p.ip || '' },
        { key: 'user_state_name', value: p.user_state_name || '' },
        { key: 'user_zip_code', value: p.user_zip_code || '' },
      ],
    };

    const res = await fetch(
      'https://api.botsi.com/v2/custom-attributes',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Accept: 'application/json',
          Authorization: `${apiKey}`,
        },
        body: JSON.stringify(payload),
      },
    );

    if (!res.ok) throw new Error(`Botsi API error: ${res.status}`);
    const data = await res.json();

    if (data.profileId) {
      w2w.set('botsiProfileId', data.profileId);
    }
  } catch (err) {
    console.warn('Botsi sync failed silently:', err.message);
  }
})();

Fetching the Paywall#

Insert before the paywall step. The returned externalId becomes the paywallSlug Web2Wave uses to route to the correct paywall in the flow. Store the attribution fields for webhook matching and for the impression event.

(async function fetchBotsiPaywall() {
  try {
    const p = window.user_properties || {};
    const ua = p.user_agent || '';
    const apiKey = 'key...';

    const payload = {
      profileId: p.botsiProfileId || '',
      placementId: 'OnboardingPlacement_ID123',
    };

    const res = await fetch('https://api.botsi.com/v2/paywall', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
        Authorization: `${apiKey}`,
      },
      body: JSON.stringify(payload),
    });

    if (!res.ok) throw new Error(`Botsi API error: ${res.status}`);
    const data = await res.json();

    const paywallExternalId = data?.data?.externalId;
    const isExperiment = data?.data?.isExperiment ?? false;
    const placementId = data?.data?.placementId || '';
    const aiPricingModelId = data?.data?.aiPricingModelId || '';
    const paywallId = data?.data?.id || '';
    const paywallSessionId = data?.data?.paywallSessionId || '';

    // Route Web2Wave to the selected paywall
    if (paywallExternalId) {
      w2w.set('paywallSlug', paywallExternalId);
    }

    // Carry attribution into the webhook payload
    w2w.set('botsiIsExperiment', isExperiment);
    w2w.set('botsiPlacementId', placementId);
    w2w.set('botsiAiPricingModelId', aiPricingModelId);
    w2w.set('botsiPaywallId', paywallId);
    // Roughly 200 characters. The four values above are still needed for
    // webhook parsing; the token is what paywall_shown uses.
    w2w.set('botsiPaywallSessionId', paywallSessionId);
  } catch (err) {
    console.warn('Botsi paywall fetch failed silently:', err.message);
  }
})();

Match each Botsi Paywall's external ID to the corresponding Web2Wave paywall slug so routing lands on the right page.

Sending the impression#

Insert when the paywall is displayed. Send eventType and paywallSessionId only. See Send Paywall Shown Event.

(async function insertUserEvent() {
  const p = window.user_properties || {};
  const apiKey = 'key...';

  try {
    const res = await fetch('https://api.botsi.com/v2/events', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
        Authorization: `${apiKey}`,
      },
      body: JSON.stringify({
        eventType: 'paywall_shown',
        paywallSessionId: p.botsiPaywallSessionId || '',
      }),
    });

    if (!res.ok) throw new Error(`Botsi API error: ${res.status}`);
  } catch (err) {
    console.warn('Botsi paywall shown event failed silently:', err.message);
  }
})();

Purchase#

Web2Wave and Stripe process the payment. Botsi receives the transaction through the webhook configured above, matched via botsiProfileId and the other botsi* user properties. Enable subscription (and preferably user-properties-with-subscription) delivery on that webhook card so renewals stay attributed.

Notes#

  • Replace key... with your app secret key in every script.
  • Fetch Paywall takes profileId and placementId only. Read product identifiers off the web2wave block of each entry in data.paywallProducts, or the stripe block when you charge through Stripe directly.
  • Configure Stripe webhooks for both Web2Wave and Botsi, or purchases never validate cleanly. See Connect to Stripe.
  • Web funnel events and custom profile data sharpen the model — send as many relevant attributes as you can.
  • Test in a staging project before production.