Connect to Stripe
Adding your Stripe keys#
Botsi needs a Stripe API key so it can retrieve the Customer, Subscription, Invoice, and Charge behind each event.
Use a restricted API key#
Botsi only needs to read your billing data, so we recommend a restricted key instead of your full secret key.
- In Stripe, go to Developers → API keys → Create restricted key.
- Set these permissions and leave everything else at None:
- Invoices: Read
- Subscriptions: Read
- Prices: Read
- Charges and Refunds: Read
- Payment Intents: Read
- Balance: Read (used to report actual Stripe fees and net revenue)
- Customers: Read
- In the Botsi dashboard at https://app.botsi.com, go to App Settings → Stripe. Paste the
rk_live_…key into the Production API key field. Create the same key in test mode or your sandbox and paste therk_test_…key into the Sandbox API key field.
Botsi never uses your key to create charges, refunds, subscriptions, payouts or webhook endpoints, or to change your products or prices.
Keys are write-only. After you save, Botsi will not show them again.
Troubleshooting
In Stripe, open the key's ⋯ → View request logs. A 403 means a permission is missing; add it and Stripe's next retry of the webhook will succeed.
Configuring the webhook#
One webhook URL handles both live and test mode. It's in App Settings → Stripe as Webhook URL, labeled Production and sandbox, and has this form:
https://api.botsi.com/v2/notifications/stripe/<notification-key>- Copy the Webhook URL from Botsi.
- In Stripe, go to Developers → Webhooks and add an endpoint pointed at that URL in live mode. If you test, add the same URL in test mode too.
- Copy each endpoint's signing secret (
whsec_...) into Botsi: the live one into the Production signing secret field, the test one into the Sandbox signing secret field.
Each event's livemode flag decides which signing secret Botsi checks it against, and which environment its revenue is recorded in.
When an event arrives, Botsi verifies the signature, then loads the Customer, Subscription, Invoice, or Charge with your 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. 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.
What Botsi records#
Botsi records revenue from these events:
- Charges: paid invoices, covering the first payment, renewals, and trial conversions, plus one-off payments.
- Paid trials: the introductory invoice of a paid trial is a paid trial start.
- Refunds: negative revenue, net of any fee Stripe returned.
- Status changes: payment failures, disputes, and subscriptions created, updated, deleted, paused, or resumed, each at $0.
Amounts are what Stripe actually charged, net of Stripe's fee.
Responses#
Matching purchases to Profiles#
Botsi matches a Stripe event to a Profile by one identifier: metadata.customer_user_id on the Stripe Customer attached to the event. For every event Botsi takes the cus_... id off the Invoice, Subscription, PaymentIntent, or Charge, retrieves that Customer with your API key, and looks for a Profile in the same app whose appUserId or profileId equals the value.
For example, if you create Profiles with appUserId user-123, set metadata.customer_user_id to user-123 on the Stripe Customer.
Creating the Profile first#
Botsi never creates Profiles from Stripe events. Create the Profile with Create a Profile or the SDK before the buyer pays, using the same appUserId you put on the Stripe Customer.
Purchases that don't match#
When no Profile matches, the purchase is:
- Recorded: its revenue still counts in your totals.
- Not attributed: it isn't linked to any Profile.
- Not granted access: Botsi sets no subscription status or access level for it.
When the metadata must be set#
Botsi reads the Customer at the moment Stripe delivers each event, so customer_user_id must already be on the Customer by then. Events delivered before you added it are not matched later. Later events for the same subscription, such as renewals, cancellations, and refunds, do match once the metadata is there.
Matching by Botsi Profile ID#
You can set metadata.botsi_profile_id on the Stripe Customer instead. It holds the profileId Botsi returns when you create a Profile, not your appUserId.
- Takes precedence: when
botsi_profile_idis present and non-empty, Botsi uses it instead ofcustomer_user_id. - No fallback: if it names no Profile in the app, the purchase stays unmatched, even when
customer_user_idwould have matched. - Empty or missing: Botsi matches on
customer_user_idas described above.
const customer = await stripe.customers.create({
metadata: { botsi_profile_id: profileId },
});Creating a Checkout Session#
Create the Customer with the identifier in its metadata, then create the Checkout Session for that Customer:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const appUserId = 'user-123'; // the appUserId of a Profile you already created
const customer = await stripe.customers.create({
email: 'alex@example.com', // fine for Stripe; Botsi doesn't match on it
metadata: { customer_user_id: appUserId },
});
await stripe.checkout.sessions.create({
mode: 'subscription',
customer: customer.id,
line_items: [{ price: 'price_1MoBy5LkdIwHu7ixZhnattbh', quantity: 1 }],
success_url: 'https://www.example.com/success',
cancel_url: 'https://www.example.com/cancel',
});Recording one-time purchases#
Botsi only uses checkout.session.completed for subscription sessions. One-time revenue arrives through payment_intent.succeeded.
A payment without a Stripe Customer can't be matched to a Profile. Create the Customer the same way as above and pass it as customer:
await stripe.checkout.sessions.create({
mode: 'payment',
customer: customer.id, // required for Botsi to match the buyer
line_items: [{ price: 'price_1N4y8w2eZvKYlo2C8n1x0abc', quantity: 1 }],
success_url: 'https://www.example.com/success',
cancel_url: 'https://www.example.com/cancel',
});Updating an existing Customer#
For a Customer created without the identifier, add it before their next purchase:
await stripe.customers.update(customerId, {
metadata: { customer_user_id: appUserId },
});Events Stripe delivered before the update stay unmatched.
Renewals and retries#
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.
Updating an existing integration#
- Identifier only on the
Subscription,Invoice, orPaymentIntent: movecustomer_user_idonto theCustomer. - Relied on email matching or on Botsi creating Profiles: create Profiles with Create a Profile and set
customer_user_idon theCustomer. - Guest Checkout with no
Customer: pass aCustomerthat has the metadata. - Set
client_reference_idfor Botsi: Botsi no longer reads it or copies it onto theCustomer's metadata. Setcustomer_user_idon theCustomeryourself.