ShipKit.one
Payment

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: lifetime
  • productName: Lifetime
  • amount: 19900 (USD cents) or 9900 if you switch to $99
  • currency: USD
  • status: pending
  • provider: 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_ID

client_reference_id supports only letters, numbers, underscores, and hyphens (max 200 chars). A UUID order id is valid.

  1. In Stripe Dashboard, open Payment Links and create a new link.
  2. Create/select the product with one-time price in USD.
  3. Set the "after payment" redirect: https://{your-domain.com}/dashboard/billing?provider=stripe&session_id={CHECKOUT_SESSION_ID}
  4. 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_ID

3) Webhook (source of truth)

Webhook endpoint:

https://{your-domain.com}/api/webhooks/stripe

Events to subscribe:

  • checkout.session.completed
  • checkout.session.async_payment_succeeded
  • checkout.session.async_payment_failed
  • charge.refunded

Webhook logic (production-safe):

  • Read client_reference_id from 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, store providerOrderId = session.id, providerCaptureId = payment_intent.
  • On async_payment_failed or payment_status = unpaid: mark failed.
  • On charge.refunded: mark refunded.

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:

  1. In Stripe Dashboard, switch to Test mode and create a test Payment Link.
  2. Use your live domain in the redirect URL so the app behavior matches production.
  3. Create a pending order in your DB and open the Payment Link with client_reference_id={ORDER_ID}.
  4. Complete payment using Stripe test cards:
    • Success: 4242 4242 4242 4242
    • Insufficient funds (fail): 4000 0000 0000 9995
  5. Verify:
    • Stripe Dashboard shows the payment.
    • /api/webhooks/stripe receives checkout.session.completed or async events.
    • Your order status updates to paid or failed.
  6. Issue a refund in Stripe and confirm charge.refunded updates your order to refunded.

6) Production checklist

  • Use live Payment Links and live webhook signing secret.
  • Ensure client_reference_id is always included in the Payment Link URL.
  • Confirm webhook endpoint is live and reachable over HTTPS.
  • Keep NEXT_PUBLIC_APP_URL aligned 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.com

References:

On this page