Stripe
Stripe (Payment Links)
Primary support: Stripe Payment Links (no-code). Webhooks are the source of truth for payment confirmation, and client_reference_id maps Stripe sessions back to internal orders.
1) Create the order before redirect
Create an orders record before sending the user to Stripe. This keeps your system authoritative and avoids granting access without an internal order.
Recommended fields (from current pricing):
productId:lifetimeproductName:Lifetimeamount:19900(USD cents) or9900if you switch to $99currency:USDstatus:pendingprovider:stripe
Use the order id as the Stripe client_reference_id so the webhook can find the order.
Example:
https://buy.stripe.com/your_payment_link?client_reference_id=ORDER_IDclient_reference_id supports only letters, numbers, underscores, and hyphens (max 200 chars). A UUID order id is valid.
2) Create the Stripe Payment Link
- In Stripe Dashboard, open Payment Links and create a new link.
- Create/select the product with one-time price in USD.
- Set the "after payment" redirect:
https://{your-domain.com}/dashboard/billing?provider=stripe&session_id={CHECKOUT_SESSION_ID} - Publish the link and use it in your pricing CTA.
Add client_reference_id to the link so Stripe sends your order id back in the webhook. You can do this in the Dashboard by editing URL parameters for the Payment Link or by appending it yourself:
https://buy.stripe.com/your_payment_link?client_reference_id=ORDER_ID3) Webhook (source of truth)
Webhook endpoint:
https://{your-domain.com}/api/webhooks/stripeEvents to subscribe:
checkout.session.completedcheckout.session.async_payment_succeededcheckout.session.async_payment_failedcharge.refunded
Webhook logic (production-safe):
- Read
client_reference_idfrom the Checkout Session. - Load the order by that id (provider must be
stripe). - Validate amount + currency against the order before marking paid.
- On success: mark
paid, storeproviderOrderId = session.id,providerCaptureId = payment_intent. - On
async_payment_failedorpayment_status = unpaid: markfailed. - On
charge.refunded: markrefunded.
Do not rely only on the return page. Users may leave before redirect, so always confirm payment via webhook delivery.
4) Return route (UX only)
The billing page already reads session_id and calls POST /api/orders/capture to show immediate status.
This is not the source of truth. It should only show a temporary success/failure message while you wait for webhook updates.
5) Test flow (safe in production)
You can test the full flow on a production deployment using Stripe test mode:
- In Stripe Dashboard, switch to Test mode and create a test Payment Link.
- Use your live domain in the redirect URL so the app behavior matches production.
- Create a pending order in your DB and open the Payment Link with
client_reference_id={ORDER_ID}. - Complete payment using Stripe test cards:
- Success:
4242 4242 4242 4242 - Insufficient funds (fail):
4000 0000 0000 9995
- Success:
- Verify:
- Stripe Dashboard shows the payment.
/api/webhooks/stripereceivescheckout.session.completedor async events.- Your order status updates to
paidorfailed.
- Issue a refund in Stripe and confirm
charge.refundedupdates your order torefunded.
6) Production checklist
- Use live Payment Links and live webhook signing secret.
- Ensure
client_reference_idis always included in the Payment Link URL. - Confirm webhook endpoint is live and reachable over HTTPS.
- Keep
NEXT_PUBLIC_APP_URLaligned with your production domain. - Monitor webhook delivery failures and retry logs.
7) Environment variables
STRIPE_SECRET_KEY=your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret
NEXT_PUBLIC_APP_URL=https://your-domain.comReferences: