MeetyyAPI
Documentation / API Reference / Stripe Setup

Stripe Setup Guide

Everything to configure inside the Stripe Dashboard and in .env to take real payments. For the endpoints clients call see payments-api.md; for how the engine works see payment-system.md.

Do this once for Test mode and again for Live mode β€” they are separate environments with separate keys, separate webhook endpoints, and separate settings. Nothing configured in test carries over to live.


1. Environment variables

STRIPE_SK=sk_test_...           # Secret key
STRIPE_WEBHOOK_SECRET=whsec_... # Endpoint signing secret (step 3)
PAYMENTS_FAKE_GATEWAY=false
APP_FRONTEND_URL=https://app.meetyy.com
Variable Where it comes from If missing
STRIPE_SK Developers β†’ API keys β†’ Secret key App silently uses the offline fake gateway
STRIPE_WEBHOOK_SECRET Created in step 3 Every webhook is rejected with 400
APP_FRONTEND_URL Your frontend origin Redirect URLs are malformed

Only the secret key is needed. There is no publishable key here β€” the buyer never enters card details in our app, they are redirected to Stripe's hosted page.

⚠️ STRIPE_WEBHOOK_SECRET is not optional. Without it the endpoint refuses every delivery rather than skipping verification, because an unverified webhook endpoint would let anyone mark any order paid. Payments will silently never settle if you deploy with a key but no webhook secret.

After changing .env on a deployed environment:

php artisan config:clear

2. Account settings

Settings β†’ Business β†’ Public details

Setting Value Why
Public business name Meetyy Shown on the checkout page and receipts
Support email / URL your support address Shown to buyers; reduces disputes
Statement descriptor MEETYY What appears on the card statement. A recognisable descriptor is the single biggest lever on chargeback rate β€” buyers dispute charges they cannot identify

Settings β†’ Payments β†’ Customer emails

Enable Successful payments and Refunds. Stripe then emails receipts directly; the app does not send payment receipts of its own.

Settings β†’ Payment methods

Enable at minimum Cards. Enabling Apple Pay and Google Pay is free and they appear automatically on hosted checkout β€” worth doing, since a large share of buyers arrive from a mobile app.

Currencies must be enabled for anything you price in. The app allows usd, eur, gbp, cad, aud, inr, bdt, jpy (App\Enums\Payment\Currency) β€” a course priced in a currency your Stripe account cannot settle will fail at checkout.


3. The webhook endpoint β€” the critical step

Developers β†’ Webhooks β†’ Add endpoint

Field Value
Endpoint URL https://api.meetyy.com/stripe/webhook β€” https://, and the API host
Description Meetyy payment settlement
API version Your account's default (see Β§4)

The URL is deliberately unversioned and outside /api, so it does not move when the client API is versioned. It must be publicly reachable over HTTPS β€” Stripe cannot reach localhost (see Β§7 for local development).

⚠️ Two ways to get this URL wrong, both of which look like a broken endpoint:

  1. http:// instead of https://. The server 307-redirects to HTTPS, and Stripe does not follow redirects β€” every delivery fails with "Temporary Redirect" and nothing ever settles. This is the single most common setup failure.
  2. The frontend host instead of the API host. success_url points at the frontend, so it is easy to reuse that domain here. The webhook route only exists on the API host; anywhere else returns 404 or 405.

Verify before trusting it β€” a healthy endpoint answers 400 to an unsigned POST, because it is refusing the bad signature:

curl -i -X POST -H 'Stripe-Signature: t=1,v1=x' -d '{}' \
  https://api.meetyy.com/stripe/webhook

400 = reachable and verifying. 307/301 = wrong scheme. 404/405 = wrong host.

Events to subscribe to

Select these under Select events:

Event What it does Needed for
checkout.session.completed Grants access. The single most important event All surfaces
checkout.session.expired Releases what was held for an abandoned buyer All surfaces
payment_intent.payment_failed Marks a declined card All surfaces
charge.refunded Syncs refunds and revokes access on a full refund All surfaces
invoice.paid Renews a subscription period Communities
invoice.payment_failed Failed renewal β€” dunning Communities
customer.subscription.updated Syncs period end and cancel-at-period-end Communities
customer.subscription.deleted Ends membership when a subscription lapses Communities

Subscribe to all eight. All three surfaces are live, and the four subscription events are what keep community memberships in step with Stripe's billing. Do not select "all events": that adds significant noise for no benefit. An event the app does not handle is recorded in stripe_events as ignored and causes no error.

Copy the signing secret

After creating the endpoint, click Reveal under Signing secret and copy the whsec_… value into STRIPE_WEBHOOK_SECRET.

Each endpoint has its own signing secret. Test-mode and live-mode endpoints do not share one, and neither does the Stripe CLI (Β§7).


4. API version

This app uses stripe/stripe-php 21.2.0.

Leave the webhook endpoint on your account's default API version. Do not pin the endpoint to an older version than the account default: webhook payload shapes vary by API version, and a mismatch changes the structure of the objects the app parses.

If Stripe later prompts you to upgrade the account API version, treat it as a change requiring a test-mode run of the payment suite first β€” not a routine acceptance.


5. Checkout settings

Settings β†’ Payments β†’ Checkout and Payment Links

Setting Value Why
Branding (logo, colours) Your brand The hosted page is the only part of the purchase the buyer sees outside your app
Enable promotion codes On Required before community coupons can be applied at checkout
Card statement descriptor MEETYY See Β§2

Do not enable Stripe Tax without a deliberate decision. It changes the amount charged relative to the price the app quoted and recorded in price_snapshot, which would make the app's records disagree with Stripe's. If you need tax, that is its own piece of work.

Do not enable "Save payment details" on one-off course or session purchases; it is only needed for subscriptions, where Stripe handles it automatically.


6. Subscription settings

Settings β†’ Billing β†’ Subscriptions and emails

Setting Recommendation Why
Retry schedule on failed payment Smart Retries, 3–4 attempts over ~2 weeks Recovers most involuntary churn
After all retries fail Cancel subscription Fires customer.subscription.deleted, which is what ends the membership. Choosing "leave unpaid" leaves members with free access indefinitely
Send emails on failed payment On Buyer can fix their card without you building dunning

That "cancel after retries" setting is load-bearing: the app revokes community access on customer.subscription.deleted. If Stripe never cancels, that event never fires and a lapsed member keeps access.


7. Local development

Without any Stripe account β€” leave STRIPE_SK empty. The app binds an offline gateway and checkout_url points at a local page with Pay/Cancel buttons that drive the same settlement path. The entire purchase flow works.

Against real Stripe β€” install the Stripe CLI:

stripe login
stripe listen --forward-to localhost:8000/stripe/webhook

stripe listen prints its own whsec_… β€” put that in STRIPE_WEBHOOK_SECRET, not the dashboard endpoint's secret. It is different, and using the wrong one makes every webhook fail signature verification.

Trigger events by hand:

stripe trigger checkout.session.completed
stripe trigger charge.refunded

Test cards (any future expiry, any CVC):

Number Result
4242 4242 4242 4242 Succeeds
4000 0000 0000 0002 Declined
4000 0000 0000 9995 Declined β€” insufficient funds
4000 0025 0000 3155 Requires 3DS authentication

8. Go-live checklist

  • Live-mode secret key in STRIPE_SK
  • Live-mode webhook endpoint created at https://<api-host>/stripe/webhook
  • All eight events subscribed
  • Live-mode signing secret in STRIPE_WEBHOOK_SECRET (not the test one)
  • PAYMENTS_FAKE_GATEWAY=false
  • APP_FRONTEND_URL points at the production frontend
  • php artisan config:clear run after deploy
  • Queue worker running and consuming the payments queue β€” settlement happens on the queue, so payments never settle without it. STRIPE_QUEUE and the --queue= list in nixpacks.toml must name the same queue
  • Confirm settlement end to end after deploy β€” a stalled payments queue is silent, so check stripe_events shows processed rather than assuming
  • Statement descriptor set
  • Business details and support email completed (Stripe blocks live charges otherwise)
  • One real end-to-end purchase made and refunded

Verifying it works

# 0. Confirm no settlement jobs are piling up unconsumed:
php artisan tinker --execute 'DB::table("jobs")->selectRaw("queue, count(*) c")->groupBy("queue")->get();'

# 1. Buy something small in live mode, then check the payment settled:
php artisan tinker --execute 'App\Models\Payment\Payment::latest()->first()->only(["status","amount_minor","paid_at"]);'

# 2. Confirm webhooks are arriving and being processed, not ignored or failed:
php artisan tinker --execute 'App\Models\Payment\StripeEvent::latest()->take(5)->get(["type","status","attempts"]);'

In the Dashboard, Developers β†’ Webhooks β†’ your endpoint shows every delivery and its response. A healthy endpoint returns 200 quickly; the app acknowledges first and settles on the queue precisely so Stripe never times out and retries.

Recovering deliveries that failed

Nothing is lost when an endpoint was misconfigured β€” the events still exist. After fixing the URL, replay them:

stripe events resend evt_XXXXXXXXXXXX

Or Dashboard β†’ Developers β†’ Events β†’ the event β†’ Resend. Replaying is safe: the stripe_events unique constraint and the idempotent settlers mean a already-settled payment is a no-op.


9. Troubleshooting

Symptom Cause
Dashboard shows "Temporary Redirect" (307) Endpoint registered as http://. The server redirects to HTTPS and Stripe does not follow redirects. Re-register with https://
Dashboard shows 404 or 405 Endpoint points at the frontend host rather than the API host
Webhook returns 200 "Event received." but nothing settles The worker is not consuming the payments queue. Stripe is satisfied and never retries, so this is silent β€” check STRIPE_QUEUE against the --queue= list in nixpacks.toml, then recover as below
Buyer paid, nothing unlocked As above, or STRIPE_WEBHOOK_SECRET wrong/missing
All webhooks show 400 in the Dashboard Signing secret mismatch β€” CLI secret used in production, or test secret in live mode
checkout_url is a local /fake-checkout/... link STRIPE_SK empty or PAYMENTS_FAKE_GATEWAY=true
Webhooks show 500 and retry Settlement threw. Check stripe_events.error and the log; Stripe will redeliver
stripe_events rows stuck at received Job dispatched but never ran β€” no queue worker
Events recorded as ignored Normal for event types the app does not handle
Checkout rejected on expiry Domain deadline outside Stripe's 30-minute–24-hour window. The gateway clamps into range, so this should not occur

Where to look

  • stripe_events β€” every delivery, its status, attempts, and error
  • payments β€” the money ledger; status and paid_at tell you if settlement ran
  • Application log β€” payment code logs directly (never via log_entry(), which is a no-op when APP_DEBUG=false)
  • Dashboard β†’ Developers β†’ Webhooks β†’ endpoint β†’ delivery attempts

Nothing is lost by a temporary failure: Stripe retries for up to three days, the stripe_events unique constraint makes a redelivery safe, and every settler is idempotent. A webhook outage is recoverable by resending events from the Dashboard.

Events that arrived but never settled

A stopped worker, or a queue nobody consumes, leaves rows in stripe_events stuck at received. Stripe considers those delivered and will not resend them on its own.

⚠️ Resending from the Dashboard alone does not fix them. stripe_events.stripe_event_id is unique, so a resent event is recognised as a duplicate and skipped β€” that guard is what makes normal retries safe, but here it blocks recovery. Delete the stuck row first, then resend:

php artisan tinker --execute '
  App\Models\Payment\StripeEvent::where("status", "received")->delete();
'
# then: Dashboard β†’ Developers β†’ Events β†’ Resend (or `stripe events resend evt_…`)

Nothing detects a stalled settlement queue automatically, so stripe_events is worth checking after any deploy that touches the worker.