Stripe billing.
Set up Stripe subscription billing for partners, step by step.
This is the full setup guide for Stripe. For the billing overview, the plan tiers, and the other modes, see Billing Configuration.
What this mode does
Stripe runs your partner subscriptions through Laravel Cashier. A partner subscribes with Stripe Checkout, and Stripe calls a webhook on every change. The app reads that change and sets the partner's plan. Existing subscribers manage their card, invoices, upgrades, and downgrades in Stripe's hosted billing portal.
When to use it
Use Stripe when you want automated card billing with self-service upgrades. Partners pay by card, Stripe handles the checkout and the hosted portal, and the app keeps each partner's plan in step through the webhook.
Before you start
- A Stripe account.
- Your site reachable over HTTPS at a public domain, so Stripe can deliver webhooks.
- Plan tiers set the way you want. See Plans & pricing for the tiers, the Plans editor, and the environment overrides.
⚠️ Keep test and live separate. Stripe keeps test mode and live mode apart, including the API keys, the price IDs, and the webhook signing secret. Build and confirm everything in test mode first, then repeat the same steps in live mode. Never mix a test value with a live value.
Step 1: Get your API keys
- Sign in to the Stripe Dashboard.
- Set the dashboard mode switch to Test mode while you set up. Turn it off later for live.
- Open Developers → API keys (direct link: dashboard.stripe.com/apikeys).
- Copy the Publishable key (
pk_test_…in test,pk_live_…in live). - In the Secret key row, click Reveal and copy it (
sk_test_…orsk_live_…).
Keep the secret key private. Test keys and live keys are different values, so copy the pair that matches the mode you are configuring.
Step 2: Create a product and its prices
- Open the Product catalog (direct link: dashboard.stripe.com/products) and click Add product.
- Name the product after your plan tier, for example "Gold".
- Under Pricing, add a Recurring price for the monthly cycle. Add a second recurring price for the yearly cycle only if you offer annual billing for that tier. Save.
- Open each price and copy its API ID. A price ID starts with
price_…. Copy the price ID, not the product ID (prod_…).
Create one product per paid tier with a monthly price. Add a yearly price only for tiers where you turn on Offer annual billing in the Plans editor. A monthly-only tier needs no yearly price. Tier 1 is free in the stock plans, so it needs no price. See Stripe's manage prices for more.
Step 3: Add the keys and price IDs
There are two ways to store these; pick one.
Admin UI (recommended). Paste the API keys and the webhook signing secret into Provider credentials (SaaS overview → Billing settings → Provider credentials), and paste each paid tier's Stripe Price IDs into the Plans editor (each tier's Pricing section, when Stripe is the active provider). A saved value overrides the environment, secrets are encrypted at rest, and the Plans editor shows a short setup checklist to walk you through it.
Environment file. Or set everything in .env, a server-managed alternative that suits Docker and cloud deployments. A value here is used only when no admin value is saved:
# Billing provider
BILLING_PROVIDER=stripe
# Stripe API keys (Developers → API keys)
STRIPE_KEY=pk_live_...
STRIPE_SECRET=sk_live_...
# Webhook signing secret (you get this in Step 4)
STRIPE_WEBHOOK_SECRET=whsec_...
# Price IDs: a monthly id per paid tier. Add a yearly id only for tiers that offer annual billing.
# Tier 1 is the free tier, so it needs no price ID.
STRIPE_PRICE_TIER2_MONTHLY=price_1Abc...
STRIPE_PRICE_TIER2_YEARLY=price_1Def...
STRIPE_PRICE_TIER3_MONTHLY=price_1Ghi...
STRIPE_PRICE_TIER3_YEARLY=price_1Jkl...
STRIPE_PRICE_TIER4_MONTHLY=price_1Mno...
STRIPE_PRICE_TIER4_YEARLY=price_1Pqr...
⚠️ All three credentials are required. The provider stays unconfigured until
STRIPE_KEY,STRIPE_SECRET, andSTRIPE_WEBHOOK_SECRETare all set. Without the webhook secret, the webhook endpoint rejects every request with403 Forbidden.
A price ID saved in the Plans editor overrides the environment. On the environment-file path, price IDs load through config/default.php, not env() at runtime, so they keep working with php artisan config:cache in production.
Step 4: Create the webhook
Your webhook endpoint is:
https://your-domain.com/api/stripe/webhook
New Stripe accounts use Workbench. Open Workbench from the dashboard and select the Webhooks tab:
- Click Create new destination.
- Keep the default API version, choose Events on your account, and click Continue.
- Select these event types, then click Continue:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.updatedcustomer.deleted
- Choose Webhook as the destination type.
- In Endpoint URL, enter
https://your-domain.com/api/stripe/webhook, then click Create destination.
Older accounts use the Developers Dashboard: open Developers → Webhooks, click Create an event destination (labelled Add endpoint on some accounts), enter the Endpoint URL, pick the same events under Select events, and click Add endpoint.
Open the new endpoint, find Signing secret, click Click to reveal, and copy the value. It starts with whsec_. Paste it into Provider credentials as the webhook signing secret (or, on the environment-file path, set STRIPE_WEBHOOK_SECRET in .env). The signing secret is separate from your API keys, and it differs between test and live, so create the webhook in the same mode as your keys. See Stripe's webhooks guide for more.
Step 5: Confirm
Open the Health center. The Billing Provider check reads Stripe in green once the keys and the webhook secret are all set. To confirm delivery, send a test event from the webhook's page in Stripe and check that it returns 200.
Test before you go live
- With test-mode keys, the test price IDs, and a test-mode webhook in place, register a test partner and subscribe through Stripe Checkout with a Stripe test card.
- Confirm the partner's My Plan page shows the new tier and an Active badge.
- When the flow works end to end, repeat Steps 1 to 4 in live mode: copy the live keys, create live prices, and create a live-mode webhook. Save the live keys and webhook secret in Provider credentials and the live price IDs in the Plans editor (or, on the environment-file path, put the live values in
.env).
What partners see on My Plan
Every partner sees a subscription status badge and the available-plans comparison whenever Stripe is configured. A monthly/yearly toggle appears when at least one shown plan offers annual billing; monthly-only plans are shown without it.
Self-registered partners (signed up through the registration page) manage their own billing:
- Without an active subscription, they see Upgrade on higher tiers. The button opens a Stripe Checkout session, and Cashier creates the Stripe customer during checkout.
- With an active subscription, they see Change Plan and go to the Stripe hosted portal, which handles upgrades and downgrades.
Admin-created and legacy partners (those with a created_by value, including v3 imports) see the plan cards but no self-service buttons. They contact their administrator for changes.
Subscription states
The provider maps Stripe state onto the shared entitlement vocabulary (see the state reference):
| Stripe status | Entitlement state | Access |
|---|---|---|
| (no Stripe customer) | legacy |
Full plan access (pre-billing) |
active |
active |
Full plan access |
trialing |
trialing |
Full plan access |
past_due |
past_due |
Restricted: creating new items is blocked |
canceled |
cancelled |
Restricted once the subscription reports cancelled |
incomplete |
incomplete |
Limited until the first payment clears |
incomplete_expired |
cancelled |
Restricted; subscription deleted |
unpaid |
suspended |
Restricted |
paused |
suspended |
Restricted |
legacy never restricts access. Partners assigned a plan before you enabled Stripe keep working with no billing setup.
Webhook synchronization safety
The Stripe webhook handler is hardened so a partner can never reach a paid tier they did not pay for:
- Reads the database, not the payload. After Cashier processes the event,
StripeWebhookControllerreads the local Cashier subscription row, rather than trusting raw values in the incoming payload. - Active-subscription guard. It moves the plan only for
activeortrialingsubscriptions. It blockspast_due,incomplete,canceled, andincomplete_expiredfrom granting a paid tier. - Strict price-ID mapping. It reverse-maps the active Stripe Price ID against
config/default.phpto find the tier. It ignores an unknown price ID. - Permission sync. When a plan change passes the guards, it derives and saves the partner's feature gates, resource limits, and metadata so entitlements match the new tier.
Troubleshooting
| Symptom | Likely cause | Resolution |
|---|---|---|
Webhook deliveries return 403 |
STRIPE_WEBHOOK_SECRET is missing |
Reveal the signing secret on the webhook endpoint and save it in Provider credentials (or set STRIPE_WEBHOOK_SECRET in .env). The Health center shows Stripe (webhook insecure) when it is missing. |
| Signature verification fails | The signing secret is from the other mode | The secret differs between test and live. Use the secret from the same mode as your keys. |
Webhook returns 200 but the plan does not change |
The subscription is not active or trialing, or the price ID is not mapped |
Confirm the STRIPE_PRICE_* mapping holds the active price ID, and that the subscription is active. |
| Checkout fails with "No such price" | The price ID is from the other mode, or it is the product ID | Use a price_… ID, not a prod_… ID, from the same mode as your keys. |
| Partner sees no Upgrade buttons | The partner is admin-created (created_by is set) |
Admin-created and legacy partners are admin-managed. Change their plan from the admin screens. |
| Keys or webhook rejected after go-live | A test value is mixed with live values | Use live keys, live price IDs, and a live-mode webhook together. |
Quick reference
# === Stripe (.env) ===
BILLING_PROVIDER=stripe
# Credentials: Developers → API keys
STRIPE_KEY=pk_live_...
STRIPE_SECRET=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_... # Step 4, reveal on the webhook endpoint
# Price IDs: Product catalog → your product → each price's API ID (price_...)
STRIPE_PRICE_TIER2_MONTHLY=price_...
STRIPE_PRICE_TIER2_YEARLY=price_...
STRIPE_PRICE_TIER3_MONTHLY=price_...
STRIPE_PRICE_TIER3_YEARLY=price_...
STRIPE_PRICE_TIER4_MONTHLY=price_...
STRIPE_PRICE_TIER4_YEARLY=price_...