Billing Setup
Stripe products, webhooks, and environment variables for operators.
Self-hosting preview — Managed AttendiBot hosting is the recommended path today. Self-hosting support is coming soon; this guide is published for early adopters and operators preparing their infrastructure.
AttendiBot uses Stripe Checkout (subscriptions) and the Customer Portal. Configure these in the Stripe Dashboard (test mode first).
Products and prices
Create two products with recurring prices:
| Product | Monthly price | Annual price |
|---|---|---|
| AttendiBot Pro | $6.00 / month | $60.00 / year |
| AttendiBot Enterprise | $18.00 / month | $180.00 / year |
Copy each Price ID (price_...) into the web environment variables below.
Customer Portal
Enable the Customer Portal in Stripe Dashboard → Settings → Billing → Customer portal:
- Allow customers to update payment methods
- Allow switching between all four prices (Pro/Enterprise × monthly/annual)
- Allow cancellation (at period end recommended)
- End active trials when customers switch plans
- Prorate charges and credits on plan changes
- Invoice prorations immediately at update time
- Downgrades to a cheaper plan: at period end (recommended)
- Switch to shorter interval (annual → monthly): update immediately
Customers with an active subscription should use Manage billing in the dashboard — not Upgrade, which is only for new subscriptions.
Webhook
Add endpoint: https://attendibot.com/api/billing/webhook
Events to subscribe:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_failed
For local development:
stripe listen --forward-to localhost:3000/api/billing/webhookUse the webhook signing secret from the CLI output as STRIPE_WEBHOOK_SECRET.
Environment variables (web)
Add to web/.env.local (dev) or deployment secrets (production):
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_PRO_MONTHLY=price_...
STRIPE_PRICE_PRO_ANNUAL=price_...
STRIPE_PRICE_ENTERPRISE_MONTHLY=price_...
STRIPE_PRICE_ENTERPRISE_ANNUAL=price_...ATTENDIBOT_API_INTERNAL_SECRET must match the backend API_INTERNAL_SECRET so webhooks can sync tiers.
Optional backend override
For staging or local testing without Stripe:
BILLING_ALLOW_MANUAL_TIER=1When set, dashboard admins can still self-select paid tiers without checkout.
Monitoring
- In Stripe Dashboard → Developers → Webhooks, enable email alerts for failed webhook deliveries
- Check web service logs for
[Stripe webhook]entries (signature failures return 400; sync failures return 500 and Stripe retries) - Verify
stripe_webhook_eventsandguild_billingin PostgreSQL after test checkouts
Troubleshooting
Payment succeeded but tier did not update
- Stripe Dashboard → Webhooks → select endpoint → check recent delivery attempts (look for 500 responses)
- Confirm web can reach the backend (
ATTENDIBOT_API_BASE_URLinternal URL) - Query
SELECT * FROM stripe_webhook_events ORDER BY processed_at DESC LIMIT 10;— event should appear after successful sync - Query
SELECT * FROM guild_billing WHERE guild_id = ...;andguild_voice_settings.subscription_tier - Replay the failed event from Stripe Dashboard if sync failed before marking processed
Customer has two Stripe subscriptions
Usually caused by clicking Upgrade while already subscribed. Cancel the duplicate in Stripe Dashboard and direct the customer to Manage billing for plan changes.
Unknown price ID in webhook logs
Verify all four STRIPE_PRICE_* env vars on the web service match live Stripe Price IDs. A mismatch causes webhooks to fail without downgrading the tier.
Manual recovery
If tier and Stripe are out of sync, update via Stripe Customer Portal (triggers customer.subscription.updated) or replay the webhook event from Stripe Dashboard.