Stripe Integration
A selfhosted install takes card payments with one set of platform keys. A SaaS operator needs two integrations: Connect for ticket sales and Cashier for plan subscriptions.
Overview
Ticket payments in Event Schedule run through Stripe Checkout: the buyer pays on Stripe's own hosted page, Stripe calls a webhook back, and the sale is marked paid. What you have to configure depends on who collects the money - one account for the whole install, or a separate account per event owner.
The same keys and the same webhook endpoint also cover gift card purchases and paid appointment bookings, so you only set this up once.
Event Schedule never takes a cut of a ticket sale. The Checkout Session is created without an application fee or a transfer, so the full amount lands in the account that took the payment - the one named in your .env on a selfhosted install, or the seller's own connected account under Connect - and Stripe's own processing fee is the only deduction.
Stripe is not the only option. If you want proper invoices, payment reminders and financial reporting on top of card processing, connect Invoice Ninja instead and add Stripe as a gateway inside it. Buyers still pay by card; the paperwork lives in Invoice Ninja. See Invoice Ninja below.
Where the payment method is chosen
Server configuration only makes Stripe available. Each event still picks one payment method in the event editor, under Tickets → Payment: Cash, Stripe, Invoice Ninja, Payfast or Payment Link. The five options are described on the Tickets page.
Selling tickets is included on the Free plan on eventschedule.com, capped at 25 paid tickets per schedule per calendar month; Pro and Enterprise lift the cap. Scanning tickets at the door is free too. A few extras around selling stay Pro there: the live check-in dashboard, the ticket widget embed, custom checkout fields and the ticket waitlist.
A selfhosted install has no monthly ticket allowance at all, and it resolves to the Enterprise tier, so nothing on this page is plan-gated on your own server.
Choose Your Setup
Find your setup type and follow the corresponding guide:
| You are... | Money flows to... | Follow this guide |
|---|---|---|
| Running your own Event Schedule instance for your organization | The one Stripe account named in your .env |
Selfhosted Users |
| Running a white-label SaaS platform | Each customer's own connected Stripe account, plus your account for plan subscriptions | SaaS Operators |
| Using eventschedule.com (nothing to install) | Your own connected Stripe account | No server setup. Connect Stripe in Settings → Payment Methods |
Which one am I?
- Will several people each need to be paid into their own Stripe account? You are a SaaS operator.
- Should every ticket sold anywhere on the install settle into one account you control? You are a selfhosted user.
Connect is only used when IS_HOSTED=true. On a plain selfhosted install every ticket charge is created with your platform keys, even if a user has linked a Stripe account of their own, so per-owner payouts are a SaaS-operator setup rather than a selfhost one.
For Selfhosted Users
If you're running your own Event Schedule instance for your organization, venue, or community, all ticket payments go to a single Stripe account that you control.
You want all ticket revenue from all events on your instance to go to one Stripe account. Event creators don't need Stripe accounts of their own, and there is nothing for them to connect.
1. Get Your Stripe API Keys
- Go to the Stripe Dashboard
- Open Developers → API keys
- Note your Publishable key and Secret key
2. Configure Environment Variables
Add these to your .env file:
# Stripe Direct Payments (selfhosted)
STRIPE_PLATFORM_KEY=pk_live_your_publishable_key
STRIPE_PLATFORM_SECRET=sk_live_your_secret_key
STRIPE_PLATFORM_WEBHOOK_SECRET=whsec_your_webhook_secret
STRIPE_PLATFORM_SECRET: Your secret key (starts withsk_live_orsk_test_). This is the value that switches Stripe on: with it set, Stripe becomes an available payment method for every event owner on the install.STRIPE_PLATFORM_KEY: Your publishable key (starts withpk_live_orpk_test_). Ticket checkout happens on Stripe's hosted page and does not need it, but the in-app card form for paid boosts and on-network promotions reads it, so set it too.STRIPE_PLATFORM_WEBHOOK_SECRET: Webhook signing secret (next step)
If you cache your configuration, run php artisan config:clear after editing .env so the new values are picked up.
3. Set Up Webhooks
- In Stripe Dashboard, go to Developers → Webhooks
- Click Add endpoint
- Set URL to:
https://yourdomain.com/stripe/webhook - Select event:
checkout.session.completed - Save and copy the Signing secret to
STRIPE_PLATFORM_WEBHOOK_SECRET
Select checkout.session.completed. That is different from a SaaS Connect setup, which uses payment_intent.succeeded.
The signing secret is not optional. Until STRIPE_PLATFORM_WEBHOOK_SECRET is set, every call to /stripe/webhook is rejected with 400 Invalid signature and no sale is ever marked paid.
4. Enable Stripe for Events
- Open Settings → Payment Methods. The Stripe tab should read "Stripe is configured".
- Edit an event and open its Tickets section, then choose Tickets rather than External or Registration.
- On the Payment tab, set Payment method to Stripe and pick the Currency the tickets are priced in.
- Add your ticket types on the General tab and save. Payments automatically use your platform Stripe account.
How Checkout Works
- The buyer selects tickets and fills out the checkout form
- Event Schedule creates a Stripe Checkout Session on your platform account, tagged with the sale ID
- The buyer completes payment on Stripe's hosted page
- Stripe calls
/stripe/webhook; Event Schedule verifies the signature, checks the amount charged against the amount owed, and marks the sale paid - The buyer is emailed their tickets, with a QR code for check-in
If the amount Stripe reports differs from the order total by more than one cent, the sale is not marked paid. It is flagged amount_mismatch instead, and the mismatch is written to your application log for you to reconcile by hand.
Gift card purchases and paid appointment bookings use exactly the same keys and the same endpoint, so no extra configuration is needed for either.
For SaaS Operators
If you're running your own white-label SaaS platform (like eventschedule.com but with your own branding), you need two Stripe integrations:
Both require IS_HOSTED=true. The rest of the SaaS setup, including domains and plan limits, is covered in the SaaS setup guide.
Stripe Connect
So your event creator customers can connect their Stripe accounts and receive ticket payments directly. You never hold their ticket money.
Laravel Cashier
To charge your customers for Pro and Enterprise plans (the money you make as the platform operator), billed to your own account.
Part A: Stripe Connect (ticket sales)
Lets your event creators receive payments for their own ticket sales.
1. Enable Stripe Connect
- Go to the Stripe Dashboard
- Open Settings → Connect
- Enable Connect for your platform
- Configure your branding and platform profile. Event Schedule creates the connected account for the user and sends them through Stripe's own hosted onboarding, so your branding is what they see.
- Get your API keys from Developers → API keys
2. Environment Configuration
# Stripe Connect (for event creators to receive ticket payments)
STRIPE_KEY=sk_live_your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_connect_webhook_secret
STRIPE_KEY: Your platform's Stripe secret key, despite the name. It is the key every Connect call is made with.STRIPE_WEBHOOK_SECRET: Signing secret of the Connect webhook endpoint (next step)
3. Webhook Configuration
- Go to Developers → Webhooks
- Click Add endpoint and choose to listen to events on connected accounts, since the charges are created on your customers' accounts rather than yours
- Set URL to:
https://yourdomain.com/stripe/webhook - Select event:
payment_intent.succeeded - Save and copy the signing secret to
STRIPE_WEBHOOK_SECRET
/stripe/webhook tries the Connect secret first and the platform secret second, so the same URL serves both. Event Schedule then checks that the secret matches the kind of sale: a Connect sale confirmed with the platform key, or a direct sale confirmed with the Connect key, is logged and ignored rather than marked paid.
What Your Event Creators Do
- Open Settings → Payment Methods → Stripe
- Click Connect Stripe
- Complete Stripe's onboarding
- They return to your platform, their Stripe business name is shown with an Unlink Account link, and Stripe becomes selectable as an event payment method
Onboarding that is started but not finished leaves the field labeled Account ID [Pending], and the event editor's Payment tab shows a "Stripe is verifying your details" notice. Stripe only becomes selectable as a payment method once Stripe reports that charges are enabled.
Connect API Endpoints
| Endpoint | Description |
|---|---|
GET /stripe/link |
Start Stripe Connect onboarding (signed in) |
GET /stripe/complete |
Complete onboarding callback (signed in) |
POST /stripe/unlink |
Disconnect Stripe account (signed in) |
POST /stripe/webhook |
Handle payment_intent.succeeded and checkout.session.completed |
Part B: Laravel Cashier (subscription billing)
Charges your customers for Pro and Enterprise plans on your own Stripe account.
1. Create Subscription Products
- In Stripe Dashboard, go to Products → Add product
- Create a Pro product with two recurring prices, one monthly and one yearly
- Optionally create an Enterprise product the same way. The Enterprise tier is hidden from the subscribe page unless both of its price IDs are configured.
- Note each Price ID (starts with
price_)
2. Environment Configuration
# Laravel Cashier (for subscription billing)
STRIPE_PLATFORM_KEY=pk_live_your_publishable_key
STRIPE_PLATFORM_SECRET=sk_live_your_secret_key
STRIPE_PLATFORM_WEBHOOK_SECRET=whsec_your_subscription_webhook_secret
STRIPE_PRICE_MONTHLY=price_monthly_price_id
STRIPE_PRICE_YEARLY=price_yearly_price_id
STRIPE_PLATFORM_KEY: Publishable key for your platform, used by the card form on the subscribe pageSTRIPE_PLATFORM_SECRET: Secret key for your platformSTRIPE_PLATFORM_WEBHOOK_SECRET: Signing secret of the subscription webhook endpointSTRIPE_PRICE_MONTHLYandSTRIPE_PRICE_YEARLY: Pro price IDsSTRIPE_ENTERPRISE_PRICE_MONTHLYandSTRIPE_ENTERPRISE_PRICE_YEARLY: Enterprise price IDs. Leave both unset to sell Pro only.STRIPE_PRICE_MONTHLY_AMOUNT,STRIPE_PRICE_YEARLY_AMOUNT,STRIPE_ENTERPRISE_PRICE_MONTHLY_AMOUNTandSTRIPE_ENTERPRISE_PRICE_YEARLY_AMOUNT: the amounts displayed in the app. They are labels only, so set them to match your Stripe prices or your pages will show the defaults of 9, 90, 29 and 290. A super-admin can override all four at/admin/settingswithout touching.env; keep these set anyway, because revenue reporting and renewal emails read the values here rather than the admin panel.PLATFORM_CURRENCY: the currency the platform quotes its own price in - a label there, like the amounts, so set it to match the currency of your Stripe prices. It does one real job beyond that: it is the currency a new event starts in when its schedule has no country set, or a country outside the built-in currency map. Defaults toUSD, and a super-admin can change it at/admin/settingswithout touching.env.STRIPE_LEGACY_PRICE_MONTHLY,STRIPE_LEGACY_PRICE_YEARLY,STRIPE_LEGACY_ENTERPRISE_PRICE_MONTHLYandSTRIPE_LEGACY_ENTERPRISE_PRICE_YEARLY: comma-separated retired price IDs. Stripe prices cannot be edited, so changing what a plan costs means creating a new price and pointing the variables above at it. List the old IDs here and anyone still billing on them keeps the tier and term they pay for. Leave empty until you have actually retired a price.STRIPE_LEGACY_PRICE_AMOUNTS: what those retired prices charge, asprice_id:amountpairs (for exampleprice_abc:9,price_def:90), so grandfathered subscribers are not counted at zero in reporting or quoted a price they are not paying. This one is.envonly by design: it records what existing customers are actually being charged, keyed by opaque Stripe IDs, and is never editable from the admin panel.
3. Subscription Webhook
- Go to Developers → Webhooks
- Click Add endpoint (this is a second webhook, separate from Connect, listening on your own account)
- Set URL to:
https://yourdomain.com/stripe/subscription-webhook - Select events:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.subscription.trial_will_endcustomer.updatedcustomer.deletedinvoice.payment_succeededinvoice.payment_failedinvoice.payment_action_requiredpayment_method.automatically_updated
- Save and copy signing secret to
STRIPE_PLATFORM_WEBHOOK_SECRET
These events are what keep a schedule's plan in step with Stripe: a successful invoice sets the plan and term, a deleted subscription drops the schedule back to Free, and a failed payment emails the owner and sends them a push notification.
The plan tier is decided by matching the subscription's price ID against the four configured in your .env. A price ID that matches none of them is treated as Pro monthly rather than being ignored, so an Enterprise customer whose price ID no longer matches quietly loses Enterprise. Re-check all four values whenever you change your Stripe products.
4. Customer Portal Setup
- Go to Settings → Billing → Customer portal
- Enable subscription management features:
- Subscription cancellation
- Plan switching
- Payment method updates
- Customize branding to match your platform
Subscription API Endpoints
| Endpoint | Description |
|---|---|
GET /{subdomain}/subscribe |
Show subscription page |
POST /{subdomain}/subscribe |
Create new subscription |
GET /{subdomain}/subscription/portal |
Redirect to Stripe Customer Portal |
POST /{subdomain}/subscription/cancel |
Cancel subscription |
POST /{subdomain}/subscription/resume |
Resume cancelled subscription |
POST /{subdomain}/subscription/swap |
Switch between monthly/yearly |
POST /stripe/subscription-webhook |
Handle subscription events |
Cashier bills the schedule, not the user account. Each schedule carries its own Stripe customer and subscription, so one person can own several schedules on different plans, and cancelling one leaves the others alone.
Invoice Ninja (Alternative Payment Method)
In addition to Stripe, Event Schedule supports Invoice Ninja as an alternative payment method for ticket sales and gift cards. Invoice Ninja is an open-source invoicing and payments platform that supports many payment gateways, Stripe among them.
Unlike Stripe, Invoice Ninja needs no .env configuration at all. Each user connects their own Invoice Ninja company from Settings → Payment Methods → Invoice Ninja in the admin portal, so different event owners on the same install can use different Invoice Ninja accounts.
Prerequisites
- An Invoice Ninja company, either selfhosted or on invoicing.co
- An API token from that company
- At least one payment gateway configured in Invoice Ninja
Setup Steps
- In Invoice Ninja, go to Settings → Account Management and create an API token
- In Event Schedule, open Settings → Payment Methods → Invoice Ninja
- Paste the token into API Token
- Fill in API URL with the base address of your instance, for example
https://invoicing.yourdomain.com, without a trailing/api/v1. Leave it blank to use invoicing.co. - Save. Event Schedule verifies the credentials and registers a webhook in your Invoice Ninja company, so the connection either works or fails outright rather than saving a broken one.
Once connected, the company name is shown with Edit and Unlink Account links. Editing the credentials replaces the old webhook rather than adding a second one, and leaving the token blank there means "keep the current token", so you can correct just the URL.
How It Works
- Edit an event, open Tickets → Payment and choose Invoice Ninja as the payment method
- At checkout the buyer is sent to Invoice Ninja: to an invoice they can pay, or to an Invoice Ninja purchase page, depending on the mode below
- Invoice Ninja processes the card through whichever gateway you configured there
- Invoice Ninja calls the webhook back so Event Schedule can mark the sale paid and email the tickets
Invoice Ninja Modes
Once the company is connected, a Checkout mode setting appears in the same Invoice Ninja tab. It applies to every event this user sells through Invoice Ninja. The full comparison lives on the Tickets page.
Invoice
Ticket selection and promo codes are handled in Event Schedule, and an invoice is created in Invoice Ninja for each purchase. Supports several promo codes.
Payment link
Buyers select tickets and enter promo codes on the Invoice Ninja purchase page, and invoices are grouped there. Event Schedule creates one Invoice Ninja product per ticket type and add-on the first time an event is bought, and passes one active promo code.
If building the Invoice Ninja purchase page fails, that checkout quietly falls back to Invoice mode so the buyer can still pay. If your buyers keep landing on an invoice instead of the purchase page, check your application log for the Invoice Ninja warning.
Payfast (Alternative Payment Method)
Payfast is a South African gateway, useful where Stripe is not available. It settles in South African rand only. Setup is documented in the user guide; the notes below are the parts specific to running your own install.
Two ways to set it up
Payfast works either way round, and you can mix the two on one install:
One account for the whole install
Put your merchant details in .env, exactly as you would STRIPE_PLATFORM_SECRET. Every schedule can then sell tickets straight away, with no setup of their own, and all the money reaches your account.
Each user brings their own
Leave .env alone and each user connects a merchant account from Settings → Payment Methods → Payfast, so different event owners on the same install are paid into different Payfast accounts.
Install-wide configuration
Take these from your Payfast dashboard, under Settings, and add them to your .env file:
# Payfast for every schedule on this install
PAYFAST_MERCHANT_ID=your_merchant_id
PAYFAST_MERCHANT_KEY=your_merchant_key
PAYFAST_PASSPHRASE=your_passphrase
PAYFAST_SANDBOX=false
PAYFAST_PASSPHRASEis optional at Payfast, but required here. Without one, the payment notification signature is a plain MD5 that anyone could reproduce, so Payfast is simply not offered until all three values are set.PAYFAST_SANDBOXsends payments to Payfast's sandbox instead of taking real money. Sandbox tickets look completely normal, so leave itfalseoutside of testing. When it is on, "Test mode" is shown next to Payfast wherever an owner picks it.PAYFAST_PAYMENT_TYPES(optional) pins the checkout to a single instrument, for exampleeffor Instant EFT. Leave it empty to let Payfast offer everything your account supports.
If you cache your configuration, run php artisan config:clear after editing .env so the new values are picked up.
The values above are a default, never an override. A user who has connected their own Payfast account in Settings → Payment Methods keeps using it, and their sales keep reaching them - so adding these to an install that has been running for a while cannot quietly re-route anybody's money. Users who have connected nothing see "Provided by this installation" on that tab instead, and can still enter their own account to opt out. Ignored entirely in hosted mode, where every owner must connect their own.
It is offered only on events priced in ZAR. Separately, a single payment below R5.00 is refused at checkout rather than hidden from the dropdown, because Payfast will not process it. If Payfast is not appearing in an event's Payment dropdown at all, check the event's ticket currency first: a schedule whose country is blank, or not in the built-in currency map, starts its events in your installation currency - PLATFORM_CURRENCY, or whatever is set at /admin/settings - which defaults to USD.
Making Payfast the default for new events
New events start on Cash unless you say otherwise. On an install where Payfast is the only way to take money, that means choosing it by hand every time. Name it once instead:
DEFAULT_PAYMENT_METHOD=payfast
Accepts any gateway key - cash, stripe, invoiceninja, payment_url or payfast - and applies to events created through the API as well as the form. It only takes effect where the gateway can actually be used: an owner who has not connected it, or an event in a currency it cannot settle, still starts on Cash. On the event form a schedule's own saved ticket defaults take priority over it; the API has never read those, so there it applies whenever payment_method is omitted.
Payfast confirms a payment by POSTing a notification to your server. It cannot reach localhost or a private address, so on a laptop or an internal-only host a payment will be taken and the ticket will never be issued. Use a public hostname, or a tunnel, before taking any payment - including a sandbox one.
You may see Payfast ITN from an unrecognised source address - continuing, see confirmsPayment in your logs on every successful payment. That is expected behind Cloudflare, a reverse proxy or Docker: the app sees your proxy's address rather than Payfast's, and the address check is advisory for exactly that reason. The notification is authenticated by its signature and by asking Payfast to confirm it, so no TRUSTED_PROXIES configuration is needed for payments to work.
Testing
Test Mode Setup
For development and testing:
- Toggle to Test mode in the Stripe Dashboard
- Use test API keys (starting with
pk_test_andsk_test_) - Create test webhook endpoints pointing to your development environment
- Keep keys, prices and webhooks in one mode. Test keys with live price IDs, or the reverse, fail with a "No such price" error.
Test Card Numbers
| Card Number | Result |
|---|---|
4242 4242 4242 4242 |
Successful payment |
4000 0000 0000 3220 |
3D Secure required |
4000 0000 0000 9995 |
Declined (insufficient funds) |
4000 0000 0000 0002 |
Declined (generic) |
Use any future expiration date and any 3-digit CVC.
Testing Webhooks Locally
Use the Stripe CLI to forward webhooks:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login to your Stripe account
stripe login
# Forward webhooks (selfhosted or Connect)
stripe listen --forward-to localhost:8000/stripe/webhook
# For SaaS: Forward subscription webhooks (separate terminal)
stripe listen --forward-to localhost:8000/stripe/subscription-webhook
The CLI prints a webhook signing secret for each listener. Put it in the variable that endpoint reads:
/stripe/webhookon a selfhosted install:STRIPE_PLATFORM_WEBHOOK_SECRET/stripe/webhookfor Connect:STRIPE_WEBHOOK_SECRET/stripe/subscription-webhook:STRIPE_PLATFORM_WEBHOOK_SECRET
Trigger Test Events
# Test checkout completion (selfhosted)
stripe trigger checkout.session.completed
# Test payment success (Connect)
stripe trigger payment_intent.succeeded
# Test subscription creation
stripe trigger customer.subscription.created
A triggered event proves the endpoint is reachable and the signature verifies, but it carries no sale ID in its metadata, so no sale changes state. To test the full path, buy a ticket with a test card.
Troubleshooting
Common Issues
Stripe is missing from the payment method list
Applies to: All setups
- Selfhosted:
STRIPE_PLATFORM_SECRETis not set, or the config cache is stale. Settings → Payment Methods → Stripe tells you which, since it reads "Stripe is configured" only when the secret is present. - SaaS: the user has not finished Connect onboarding, so Stripe is not offered yet.
"Stripe account not connected" error
Applies to: SaaS operators (Connect)
- User needs to complete Stripe Connect onboarding
- Check if
stripe_account_idandstripe_completed_atare set on the user. An account ID without a completion timestamp means Stripe has not enabled charges yet.
"Invalid signature" webhook error
Applies to: All setups
- Verify the webhook secret matches the one in Stripe Dashboard
- A missing secret produces the same error: with neither variable set, every webhook is rejected
- Make sure you're using the correct secret for each endpoint:
STRIPE_WEBHOOK_SECRETfor Connect webhooksSTRIPE_PLATFORM_WEBHOOK_SECRETfor selfhosted/subscription webhooks
Payments not being recorded
Applies to: Selfhosted users, SaaS operators
- Check webhook logs in Stripe Dashboard → Webhooks → View logs
- Verify the webhook endpoint is accessible (not blocked by firewall)
- Confirm the endpoint listens to the right event:
checkout.session.completedfor direct payments,payment_intent.succeededfor Connect - For Connect, confirm the endpoint listens on connected accounts, not only on your own
- Check
storage/logs/laravel.logfor errors, including a "webhook key mismatch" warning, which means the sale and the secret belong to different payment contexts
A sale is stuck as "amount mismatch"
Applies to: Selfhosted users, SaaS operators
- Stripe reported a total more than a cent away from the order total, so the sale was deliberately not marked paid
storage/logs/laravel.logrecords the expected and received amounts side by side- Usual causes are a currency mismatch between the event and the Stripe account, or ticket prices edited after the buyer reached Stripe
Subscription not updating after payment
Applies to: SaaS operators
- Verify webhook events are being received
- Check that all required subscription events are selected in Stripe
- Confirm the subscribed price ID is one of the four configured in your
.env. An unrecognized price ID is treated as Pro monthly, which looks like an Enterprise customer being downgraded - Review
storage/logs/laravel.logfor errors
"No such price" error
Applies to: SaaS operators
- Verify
STRIPE_PRICE_MONTHLYandSTRIPE_PRICE_YEARLYcontain valid Price IDs - Ensure the prices exist in the same Stripe mode (test vs live)
Enterprise is missing from the subscribe page
Applies to: SaaS operators
- Both
STRIPE_ENTERPRISE_PRICE_MONTHLYandSTRIPE_ENTERPRISE_PRICE_YEARLYmust be set. With one missing, the request is served as Pro.
Debugging Logs
- Application:
storage/logs/laravel.log - Stripe API: Dashboard → Developers → Logs
- Webhooks: Dashboard → Developers → Webhooks → Select endpoint → View logs
Security
- API key security: Secret keys belong in
.envonly. Never put them in client-side code or commit them to version control. - Webhook verification: Both endpoints are public URLs, so both signing secrets matter.
/stripe/webhookfails closed: a signature it cannot verify, and any request at all when no secret is configured, is rejected with a 400./stripe/subscription-webhookfails open: the signature check is only installed onceSTRIPE_PLATFORM_WEBHOOK_SECRETis set, so leaving it blank leaves an unauthenticated endpoint that can move a schedule between plans. - Amount and account checks: A confirmed payment is also checked against the amount owed, and on Connect against the account that took the money, before anything is marked paid.
- HTTPS required: Stripe requires HTTPS for webhook endpoints in production.
- PCI compliance: Using Stripe Checkout and Elements keeps you out of PCI scope. Card data never touches your server.