Billing Configuration.
Configure manual or Stripe-based billing for partner subscriptions.
v4 Status: The billing foundation is implemented and functional. Manual billing works out of the box. Stripe mode processes subscription lifecycle events automatically. Partners can view their plan, features, and usage from the Partner Billing Page. For the full end-to-end SaaS setup, see the SaaS Setup Guide.
Overview
Reward Loyalty v4 supports two billing modes:
| Mode | How It Works | Best For |
|---|---|---|
| Manual (default) | Admin assigns plans. You invoice externally. | Agencies, network operators, white-label deployments |
| Stripe | Stripe manages subscriptions via Laravel Cashier. Webhook events sync plan state automatically. | Operators wanting automated subscription billing |
Both modes use the same plan tier system (config/plans.php). The billing provider controls how subscription state is resolved. It does not change plan definitions, feature gates, or resource limits.
Manual Mode (Default)
No configuration needed. This is the default when BILLING_PROVIDER is not set in .env.
# .env - manual billing (or simply omit this line)
BILLING_PROVIDER=
How it works:
- Admins assign plans via the partner management CRUD
- The EntitlementService treats all partners as
manualstatus (full plan access with no billing restrictions) - You handle invoicing through your accounting software (QuickBooks, Xero, bank transfers, etc.)
Health Center status: Billing Provider: Manual / Offline, always OK.
Stripe Mode
Stripe mode uses Laravel Cashier to manage subscriptions. When a partner's subscription state changes in Stripe, the webhook automatically syncs that state to the local database.
Required Environment Variables
Add these to your .env file:
# Billing provider
BILLING_PROVIDER=stripe
# Stripe API keys (from https://dashboard.stripe.com/apikeys)
STRIPE_KEY=pk_live_...
STRIPE_SECRET=sk_live_...
# Webhook signing secret (REQUIRED - see Webhook Setup below)
STRIPE_WEBHOOK_SECRET=whsec_...
⚠️ All three Stripe values are required. The billing provider is not considered configured until
STRIPE_KEY,STRIPE_SECRET, andSTRIPE_WEBHOOK_SECRETare all set. Without the webhook secret, the webhook endpoint actively rejects all requests with403 Forbidden.
Stripe Price IDs
Each plan tier can have monthly and yearly Stripe Price IDs. Create your products and prices in the Stripe Dashboard, then map them:
# Tier 1 is typically the free tier - no price ID needed
# STRIPE_PRICE_TIER1_MONTHLY=
# STRIPE_PRICE_TIER1_YEARLY=
# Paid tiers
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...
These values are loaded through config/default.php (not env() at runtime), so they are safe with php artisan config:cache in production.
Webhook Setup
The webhook endpoint is:
https://your-domain.com/api/stripe/webhook
Configure in Stripe Dashboard:
- Go to Stripe Dashboard → Webhooks
- Click Add endpoint
- Enter your webhook URL:
https://your-domain.com/api/stripe/webhook - Select events to listen for:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.updatedcustomer.deleted
- Click Add endpoint
- Copy the Signing secret (starts with
whsec_) - Add it to
.envasSTRIPE_WEBHOOK_SECRET
Security: When STRIPE_WEBHOOK_SECRET is not set, the webhook controller returns 403 Forbidden for all requests. It does not silently accept unsigned payloads. When the secret IS set, Laravel Cashier verifies the Stripe-Signature header on every request.
Subscription State Resolution
The billing provider resolves subscription state for the EntitlementService:
| State | Meaning |
|---|---|
manual |
Billing disabled or null provider. Full plan access. |
legacy |
Partner has a plan but no Stripe customer ID. Treated as pre-existing, with full access. |
active |
Active Stripe subscription. Full plan access. |
trialing |
Active trial period. Full plan access. |
past_due |
Payment failed. Access continues during grace period. |
canceled |
Subscription cancelled. Access continues until period end. |
incomplete |
Initial payment pending. Limited access. |
incomplete_expired |
Initial payment failed and expired. Subscription deleted. |
unpaid |
Repeated payment failures. Access restricted. |
paused |
Subscription paused. Access restricted. |
Note:
legacyandmanualstatuses never restrict access. Partners assigned plans before Stripe was enabled continue working without any billing configuration.
Health Center
The Health Center reports billing status in the Services category:
| Status | Meaning | Action |
|---|---|---|
✅ Manual / Offline |
Manual billing mode. No Stripe needed. | None |
✅ Stripe |
Stripe fully configured with API keys and webhook secret. | None |
⚠️ Stripe (not configured) |
BILLING_PROVIDER=stripe but API keys are missing. |
Add STRIPE_KEY and STRIPE_SECRET to .env |
🔴 Stripe (webhook insecure) |
API keys present but STRIPE_WEBHOOK_SECRET is missing. Webhook rejects all requests. |
Add STRIPE_WEBHOOK_SECRET to .env |
Background Processing
Stripe Webhooks
Stripe webhook processing is synchronous. No cron job or queue worker is required. When Stripe sends an event to /api/stripe/webhook, Cashier processes it in the HTTP request cycle and returns a response immediately. This means billing state updates happen in real time with no additional infrastructure.
Laravel Scheduler (Cron)
The Laravel scheduler handles background tasks like cache cleanup, scheduled notifications, and the Health Center heartbeat. All core features work without cron. It is recommended but not required. Without it, the Health Center reports a Scheduler Heartbeat warning, but billing and loyalty features continue working normally.
If you want to enable it, add this to your server's crontab:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
See the Health Center documentation for verifying scheduler status.
Queue Workers
By default, Reward Loyalty uses the sync queue driver. All jobs run inline during the HTTP request. This works fine for small installations and manual billing mode.
For SaaS/high-volume deployments with Stripe billing, consider switching to database or redis queue drivers and running a persistent queue worker:
# .env
QUEUE_CONNECTION=database
php artisan queue:work --sleep=3 --tries=3
This offloads email notifications, campaign processing, and other background work from the web request cycle. The billing webhook itself remains synchronous regardless of your queue driver.
📖 Verify your setup: The Health Center shows scheduler heartbeat and queue driver status. Use it to confirm both are operational after deployment.
Partner Billing Page
Partners can access My Plan from the sidebar or user menu. The page displays:
- Plan card: Current plan name, description, and subscription status badge (Stripe mode)
- Feature gates: Visual grid of enabled/disabled features for the current plan
- Resource usage: Progress bars showing usage vs. limits for each resource type
- Status banners: Contextual alerts for trial, past due, cancelled, or suspended states
- Plan comparison: Available plans with pricing (Stripe mode only)
- Billing portal: "Manage Billing" button linking to Stripe's hosted portal for payment methods and invoices
Manual Mode
When billing is disabled or set to manual, partners see their plan details, features, and limits with a note to contact their administrator for plan changes.
Stripe Mode
When Stripe is configured, all partners see:
- Subscription status badge (Active, Trial, Past Due, etc.), displayed for every partner when billing is enabled
- Available plans comparison with pricing, displayed whenever Stripe is configured, regardless of the partner's Stripe status
Self-registered partners (those who signed up through the registration page) can self-service their billing:
- Partners without an active subscription see Upgrade buttons on higher-tier plan cards. Clicking a button initiates a Stripe Checkout session. Cashier creates the Stripe customer automatically during the process.
- Partners with an active subscription see Change Plan buttons and are redirected to the Stripe billing portal, which handles both upgrades and downgrades.
Admin-created and legacy partners (those created by an admin, imported from v3, or otherwise assigned a created_by value) see plan cards but no self-service action buttons. These partners must contact their administrator for plan changes.
How it works: The system distinguishes self-registered partners (
created_byis null) from admin-created partners (created_byis set). This ensures that newly registered partners can subscribe immediately, while legacy accounts that predate Stripe integration remain under admin control until explicitly migrated.
Quick Reference
# === Billing Configuration (.env) ===
# Provider: leave empty for manual, set to "stripe" for Stripe
BILLING_PROVIDER=stripe
# Stripe credentials
STRIPE_KEY=pk_live_...
STRIPE_SECRET=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Price IDs (create in Stripe Dashboard → Products → Prices)
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_...