Stripe
Dynamic checkout sessions for paid tiers and sponsors — no products/prices to create in Stripe.
Launchy uses Stripe Checkout (hosted pages) with dynamic price_data — nothing to create in the Stripe Dashboard, prices come from your settings.
Setup
- Sign up for Stripe
- Copy your Secret key (
sk_test_...in test mode)
STRIPE_SECRET_KEY=sk_test_...That's the minimum. Free-tier submissions work without Stripe configured at all.
No products to create
All three checkout routes use price_data inline:
/api/stripe/checkout— Boost/Highlight tier payment (submission flow)/api/sponsor/checkout— sponsor slot booking/api/template-checkout— selling the template itself (excluded from customer distribution)
Prices come from:
boost_price_cents/highlight_price_cents— from/admin/settings- Sponsor pricing — computed by lib/sponsor-pricing.ts based on duration + settings
All three have allow_promotion_codes: true — discount codes work out of the box.
Webhook setup
The webhook at app/api/stripe/webhook/route.ts handles only one event: checkout.session.completed.
Set the event in your webhook endpoint configuration — Stripe will only send that one.
Local development
Install the Stripe CLI:
brew install stripe/stripe-cli/stripe # macOS
stripe loginForward events:
stripe listen --forward-to localhost:3000/api/stripe/webhookCopy the webhook signing secret the CLI prints:
STRIPE_WEBHOOK_SECRET=whsec_...Trigger a test checkout with card 4242 4242 4242 4242, any future expiry, any CVC.
Production
- Stripe Dashboard → Developers → Webhooks → + Add endpoint
- URL:
https://your-domain.com/api/stripe/webhook - Events:
checkout.session.completed(only one) - Reveal signing secret → copy to your prod env as
STRIPE_WEBHOOK_SECRET
What the webhook does
Switches on session.metadata to figure out which flow completed:
- Submission Boost: sets the submission to
accepted, auto-schedules to the next open week, sends the payment confirmation email - Submission Highlight: creates the product row immediately in the current batch, sets
highlight_expires_at = now() + highlight_duration_days, sends the payment confirmation email - Sponsor: inserts the
sponsorsrow with the booked dates, sends the sponsor confirmation email - Template sale (sales site only): processed by app/api/template-webhook/route.ts — not in the distribution repo
Refunds are manual
lib/stripe-refund.ts does not call Stripe's refund API. It only builds a link to the Stripe Dashboard payment page. The admin clicks it and refunds manually in the dashboard.
If you want automated refunds on submission rejection:
// lib/stripe-refund.ts (proposed change)
import { getStripe } from "./stripe"
export async function refundPayment(paymentIntentId: string) {
return getStripe().refunds.create({ payment_intent: paymentIntentId })
}Then call refundPayment() from the reject action in lib/actions/admin.ts.
Tax handling
Stripe Tax is not enabled. To enable, add to each checkout session creation:
automatic_tax: { enabled: true },
customer_update: { address: "auto" },You'll also need to set up tax registration in the Stripe Dashboard.
Common issues
"No signature" in webhook logs — The request didn't come from Stripe. Usually harmless noise.
"Invalid signature" — STRIPE_WEBHOOK_SECRET is wrong. The local CLI secret differs from the prod dashboard secret.
Webhook returns 400 with "Webhook error..." — Most likely causes: STRIPE_WEBHOOK_SECRET empty, or the secret doesn't match the one Stripe is signing with (local CLI secret vs prod dashboard secret mix-up).
Session creates but nothing happens after payment — Webhook isn't reaching you. Check the Dashboard webhook logs (Events tab) for delivery failures.