Stripe
One Stripe account processes the store, events and donations. Development uses test mode: test cards work, real cards are refused, and nothing is charged. Production uses live mode.
Test cards (test mode only)
Any future expiry (for example 12/34), any CVC (123; four digits for Amex), any postal code.
Successful payments
| Card | Number | Use |
|---|---|---|
| Visa | 4242 4242 4242 4242 | The standard successful payment |
| Visa Debit | 4000 0566 5566 5556 | Debit transaction |
| Mastercard | 5555 5555 5555 4444 | Successful Mastercard |
| Mastercard Debit | 5200 8282 8282 8210 | Debit card |
| American Express | 3782 822463 10005 | Amex (4-digit CVC) |
| Discover | 6011 1111 1111 1117 | Discover |
| JCB | 3566 0020 2036 0505 | International card |
Declined payments
| Scenario | Number | Expected behaviour |
|---|---|---|
| Generic decline | 4000 0000 0000 0002 | Bank rejects the payment |
| Insufficient funds | 4000 0000 0000 9995 | Customer lacks funds |
| Lost card | 4000 0000 0000 9987 | Card reported lost |
| Stolen card | 4000 0000 0000 9979 | Card reported stolen |
| Expired card | 4000 0000 0000 0069 | Fails on expiry |
| Incorrect CVC | 4000 0000 0000 0127 | Security code rejected |
| Processing error | 4000 0000 0000 0119 | Processing error |
For ACH (donations), Stripe's test mode offers a test bank account in the "US bank account" tab. Full list: docs.stripe.com/testing.
Where to check a test transaction
| Where | What you see |
|---|---|
| Stripe Dashboard (test mode) → Payments | the PaymentIntent, its metadata (app, order/registration/donation id) |
| Stripe Dashboard → Developers → Webhooks | delivery attempts to https://dev.emaw-app.com/rest/stripe/webhook/ |
| Admin CRM (dev) → Store → Orders / Events → Registrations / Contact → Orders tab | the record, now paid, and the linked contact |
| Deposco sandbox | the Sales Order (store only) |
| Your inbox | confirmation email via Microsoft Graph |
Test versus live
| Test | Live | |
|---|---|---|
| Keys | sk_test_ on the backend, pk_test_ on the website | sk_live_, pk_live_ |
| Webhook endpoint | dev.emaw-app.com/rest/stripe/webhook/ with its own whsec_ | app.emaw-app.com/rest/stripe/webhook/ with its own whsec_ |
| Stripe objects | payment method configurations, donation product, billing portal configuration created in test mode | created again in live mode; different ids |
| Where configured | backend dev .env, website GitHub variables | backend prod .env (Portainer), Pages production variables |
Objects do not carry over between modes. Every pmc_, prod_, bpc_ and whsec_ must be created twice.
Webhook
Single endpoint POST /rest/stripe/webhook/, signature-checked, de-duplicated in Redis, dispatched on metadata.app. It must be subscribed to all nine events (checkout.session.completed, async_payment_succeeded, async_payment_failed, expired, charge.refunded, invoice.paid, invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted). Updating an endpoint replaces its event list, so re-list all nine. The endpoint's API version must match STRIPE_API_VERSION (2026-09-30.endive).
Local bridging with the Stripe CLI: Backend → Stripe.
Go-live checklist
Follow docs/donations-go-live.md for donations; the store and events use the same steps minus the subscription objects:
- Activate the live account and payment methods (cards; ACH Direct Debit for donations).
- Create the live webhook endpoint (or update the existing one) with all nine events; copy its
whsec_. - Create live payment method configuration(s), the donations product and the billing portal configuration; copy the ids.
- Set
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,STORE_*,EVENTS_*,DONATIONS_*in the production backend.env; redeploy. - Set
VITE_STRIPE_PUBLISHABLE_KEY(pk_live_) in the website's production variables; deploy. - Smoke test with a small real charge, then refund it from the dashboard.
- Watch webhook deliveries and
error.logfor the first days.
Refunds and disputes
Refund from the Stripe Dashboard; the charge.refunded webhook updates the order, registration or donation. Deposco orders already submitted must be cancelled in Deposco by hand (deposco/README.md "Refunds and cancellations").