Setup guide · 2 of 4
Wire PayPal Subscriptions
PayPal handles recurring revenue. Money flows to your merchant account; we never act as merchant of record. Typical setup: 45–90 minutes including sandbox smoke testing.
1.Register a PayPal REST app
In PayPal Developer Dashboard, create a new REST app under your Business account. PayPal generates a Client ID and Secret for both Sandbox and LIVE. Copy the Sandbox pair first — you will switch to LIVE only after the end-to-end smoke test.
2.Set PAYPAL_* environment variables
Add PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, and PAYPAL_ENV=sandbox to your .env.local and Vercel Production. PAYPAL_WEBHOOK_ID stays blank until step 4. Trigger a Vercel redeploy.
3.Create subscription plans in PayPal + mirror them locally
In the PayPal Dashboard, create a Product, then create one Plan per pricing tier (Starter monthly, Starter annual, Pro monthly, Pro annual). Note each PayPal plan ID. Insert matching rows into the public.plans Supabase table — paypal_plan_id, key, name, price_usd, interval, entitled_strategies. Pro plans store entitled_strategies as array["*"]; Starters store one specific key.
4.Register the webhook
In the PayPal app settings, add a webhook pointed at https://<your-domain>/api/paypal/webhook. Subscribe to BILLING.SUBSCRIPTION.ACTIVATED, BILLING.SUBSCRIPTION.CANCELLED, BILLING.SUBSCRIPTION.EXPIRED, and PAYMENT.SALE.COMPLETED. Save and copy the Webhook ID into your PAYPAL_WEBHOOK_ID env var. Redeploy.
5.Run a sandbox subscription end-to-end
Create a Sandbox buyer account in the PayPal Developer Dashboard. Open your /pricing page, pick Starter, run through PayPal Sandbox checkout. Confirm in your Supabase subscriptions table that a row appears with status=active. Check /account/subscriptions renders the subscription. Verify the Telegram invite link arrives.
6.Cancel the sandbox subscription + verify the kick
Cancel the test subscription through your /account/subscriptions page (which calls /api/billing/portal). Confirm subscriptions.status flips to cancelled, the user_entitlements row updates, and on next billing cycle the chat_member webhook kicks the test account from your paid Telegram channel.
7.Switch to LIVE
Only when steps 5 and 6 are green: change PAYPAL_ENV=live, swap PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET to the LIVE pair, register a separate LIVE webhook (different ID), update PAYPAL_WEBHOOK_ID. Redeploy. Run one small real subscription against yourself to confirm money actually moves, then refund.
Common gotchas
- Business account required.PayPal Personal accounts can't accept subscriptions. Upgrade before you start.
- Sandbox and LIVE webhooks are separate.They have different webhook IDs. Don't reuse one for the other or signature verification fails silently.
- Plan ID mismatch. If checkout 400s with PLAN_DOES_NOT_EXIST, your local
plans.paypal_plan_iddoesn't match what PayPal returns. Re-copy from the PayPal Dashboard. - Emerging markets. PayPal supports more African/Asian markets than Stripe at the moment — relevant if your subscribers are in regions where Stripe pulls the plug. The codebase has Stripe scaffolding in
src/lib/stripe/if you ever need to switch back.