Connect to Stripe
Connect your Stripe account so web purchases show up in Botsi. Renewals, refunds, and subscription changes arrive the same way: Stripe sends them to a webhook Botsi hosts. Botsi matches each event to a Profile using the same appUserId you already store.
An unknown Stripe Price is dropped before matching
Register the Stripe Product ID and Price ID in Botsi before live traffic flows. An invoice whose Price has no matching Botsi product is discarded. Botsi never matches that invoice to a Profile. See Set up Stripe Products.
Adding your Stripe keys#
Botsi needs a Stripe secret API key (sk_live or sk_test) so it can retrieve the Customer, Subscription, Invoice, and Charge behind each event. Matching cannot happen without it.
- In Stripe, open Developers → API keys and copy the secret key.
- In the Botsi dashboard at https://app.botsi.com, go to App Settings → Stripe.
- Paste the
sk_livekey into the Production API key field. If you test with test-mode keys, paste thesk_testkey into the Sandbox API key field.
Keys are write-only. After you save, Botsi will not show them again.
Configuring the webhook#
App Settings → Stripe shows two copyable webhook URLs, one for production and one for sandbox.
- Copy the production URL from Botsi.
- In Stripe, go to Developers → Webhooks and add an endpoint pointed at that URL.
- Enable every event in the table below.
- Copy the endpoint's signing secret (
whsec_...) and paste it into the Production signing secret field in Botsi. - Repeat with the sandbox URL and Sandbox field if you test with
sk_testkeys.
When an event arrives, Botsi verifies the signature, then loads the Customer, Subscription, Invoice, or Charge with your secret API key.
A signing secret that does not match returns 400
Botsi verifies every delivery against the secret you pasted. An unsigned event, or one signed with a different secret, is rejected. That is deliberate: an unverified endpoint is one anybody can post fabricated purchases to.
Selecting events#
Enable all of the following events on each endpoint:
invoice.payment_succeeded and invoice_payment.paid are accepted as aliases of invoice.paid. Any other event is ignored.
Leaving an event out means Botsi does not see that part of the subscription lifecycle.
Botsi tells test and live events apart by the event's livemode flag, not by which URL received it. A test-mode event that hits the production URL is accepted, but it is not written to analytics.
Matching purchases to Profiles#
Botsi links a Stripe event to a Profile through the Stripe Customer, not the webhook payload itself. For every event it takes the cus_... id off the Invoice, Subscription, PaymentIntent, or Charge. It then retrieves that Customer with your secret API key and matches it against Profiles.
Set metadata.customer_user_id on the Stripe Customer to the same appUserId you send when you create a Profile. That is the lookup Botsi always tries first. For example, if you create Profiles with appUserId user-123, set metadata.customer_user_id to user-123 on the Stripe Customer.
Stamp the same value on the Subscription or PaymentIntent as well, so the match still works if the Customer was created during checkout without that metadata.
Creating a Checkout Session#
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const appUserId = 'user-123';
const customer = await stripe.customers.create({
email: 'alex@example.com',
metadata: { customer_user_id: appUserId },
});
await stripe.checkout.sessions.create({
mode: 'subscription',
customer: customer.id,
client_reference_id: appUserId,
subscription_data: {
metadata: {
customer_user_id: appUserId,
// optional attribution, all as strings
paywallId: '123',
placementId: 'main_paywall',
abTestId: '45',
aiPricingModelId: '7',
isExperiment: 'true',
},
},
line_items: [{ price: 'price_1MoBy5LkdIwHu7ixZhnattbh', quantity: 1 }],
success_url: 'https://www.example.com/success',
cancel_url: 'https://www.example.com/cancel',
});The paywallId, placementId, abTestId, aiPricingModelId, and isExperiment keys are optional. They tie the revenue row to a Paywall, Placement, A/B test, or AI Pricing Model. Stripe metadata values are strings, so send '123' rather than 123. When those keys are absent, Botsi falls back to the Profile's most recent paywall impression.
The same customer_user_id is what reattaches a web checkout to the Profile that was shown the Paywall in your app.
Recording one-time purchases#
For Checkout Sessions in payment mode, put customer_user_id on payment_intent_data.metadata instead. Botsi only uses checkout.session.completed for subscription sessions. One-time revenue arrives through payment_intent.succeeded.
Create the Customer the same way as above, then:
await stripe.checkout.sessions.create({
mode: 'payment',
customer: customer.id,
client_reference_id: appUserId,
payment_intent_data: {
metadata: {
customer_user_id: appUserId,
},
},
line_items: [{ price: 'price_1N4y8w2eZvKYlo2C8n1x0abc', quantity: 1 }],
success_url: 'https://www.example.com/success',
cancel_url: 'https://www.example.com/cancel',
});If customer_user_id is missing from the Customer, Botsi tries three fallbacks. First it looks up the Customer email as appUserId. Then it tries metadata.client_reference_id on the Customer. Then it reads customer_user_id, customerUserId, or profileId on the event's own metadata. For invoices, that last step merges Invoice metadata with the subscription's metadata. For one-time purchases it reads the PaymentIntent metadata.
Email matching does not use the Profile email field
If customer_user_id is missing, Botsi looks up the Customer email as appUserId. It does not match against email on the Profile. Register the user with that email address as appUserId if you want this fallback to work.
If no identifier matches, invoice.paid auto-creates a Profile. Every other event is stored without a Profile. Do not rely on auto-creation. Set customer_user_id on the Customer.
The creation behavior setting in App Settings → Stripe changes less than its labels suggest:
Once the first invoice is matched, later invoices chain to the same original transaction through the Profile. You do not need to keep writing metadata on renewal invoices.
A retried webhook does not double-count. Botsi writes only the first revenue row per invoice id.