Crave uses Stripe Connect to process payments. This guide covers how to create payment intents, integrate Stripe.js on the frontend, and test in development.
How payments work
Cart is finalized
The customer has added items, set fulfillment, and provided their details.
Create a payment intent
Your server calls the Crave API to get a Stripe clientSecret.
Collect payment on the client
Stripe.js securely collects card details using the clientSecret.
Stripe confirms the charge
The payment is processed through the restaurant’s connected Stripe account.
Crave handles Stripe Connect setup for each restaurant. You only need to integrate Stripe.js on your storefront frontend.
Create a payment intent
const payment = await storefront . payments . createIntent ( 'loc_123' , cartId );
// payment.clientSecret — use with Stripe.js
// payment.stripeAccountId — the restaurant's connected account
The response includes:
Field Description clientSecretThe Stripe PaymentIntent client secret stripeAccountIdThe connected Stripe account to pass to stripe.confirmPayment
Install Stripe.js
pnpm add @stripe/stripe-js @stripe/react-stripe-js
Set up the Stripe provider
Wrap your checkout page with the Stripe Elements provider.
'use client' ;
import { loadStripe } from '@stripe/stripe-js' ;
import { Elements } from '@stripe/react-stripe-js' ;
const stripePromise = loadStripe (
process . env . NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ! ,
// For Stripe Connect, pass the connected account
// { stripeAccount: payment.stripeAccountId }
);
export default function CheckoutLayout ({ children } : { children : React . ReactNode }) {
return (
< Elements stripe = { stripePromise } >
{ children }
</ Elements >
);
}
You must use the restaurant’s stripeAccountId from the payment intent response when initializing Stripe. This routes the payment to the correct connected account.
Collect payment
'use client' ;
import { useState } from 'react' ;
import { useStripe , useElements , PaymentElement } from '@stripe/react-stripe-js' ;
export function PaymentForm ({ clientSecret } : { clientSecret : string }) {
const stripe = useStripe ();
const elements = useElements ();
const [ error , setError ] = useState < string | null >( null );
const [ processing , setProcessing ] = useState ( false );
async function handleSubmit ( e : React . FormEvent ) {
e . preventDefault ();
if ( ! stripe || ! elements ) return ;
setProcessing ( true );
setError ( null );
const { error : stripeError } = await stripe . confirmPayment ({
elements ,
confirmParams: {
return_url: ` ${ window . location . origin } /order/confirmation` ,
},
});
if ( stripeError ) {
setError ( stripeError . message ?? 'Payment failed' );
setProcessing ( false );
}
// If successful, Stripe redirects to return_url
}
return (
< form onSubmit = { handleSubmit } >
< PaymentElement />
{ error && < p className = "text-red-600 mt-2 text-sm" > { error } </ p > }
< button
type = "submit"
disabled = { ! stripe || processing }
className = "mt-4 w-full py-3 bg-teal-600 text-white rounded-lg disabled:opacity-50"
>
{ processing ? 'Processing...' : 'Pay now' }
</ button >
</ form >
);
}
Full checkout integration
'use client' ;
import { useEffect , useState } from 'react' ;
import { loadStripe } from '@stripe/stripe-js' ;
import { Elements } from '@stripe/react-stripe-js' ;
import { storefront } from '@/lib/storefront' ;
import { PaymentForm } from '@/components/PaymentForm' ;
export default function CheckoutPage () {
const [ clientSecret , setClientSecret ] = useState < string | null >( null );
const [ stripePromise , setStripePromise ] = useState < ReturnType < typeof loadStripe > | null >( null );
useEffect (() => {
async function initPayment () {
const cartId = localStorage . getItem ( 'cartId' );
if ( ! cartId ) return ;
const payment = await storefront . payments . createIntent (
process . env . NEXT_PUBLIC_LOCATION_ID ! ,
cartId
);
setClientSecret ( payment . clientSecret );
setStripePromise (
loadStripe ( process . env . NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ! , {
stripeAccount: payment . stripeAccountId ,
})
);
}
initPayment ();
}, []);
if ( ! clientSecret || ! stripePromise ) {
return < p > Loading payment... </ p > ;
}
return (
< Elements stripe = { stripePromise } options = { { clientSecret } } >
< PaymentForm clientSecret = { clientSecret } />
</ Elements >
);
}
Testing payments
Use Stripe’s test card numbers in development:
Card Number Scenario 4242 4242 4242 4242Successful payment 4000 0000 0000 3220Requires 3D Secure authentication 4000 0000 0000 0002Card declined 4000 0025 0000 3155Requires authentication (SCA)
Use any future expiry date and any 3-digit CVC.
Set your environment to use Stripe test keys (pk_test_... / sk_test_...) during development. Crave sandbox locations automatically use Stripe test mode.
Environment variables
Add these to your .env.local:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_key
The Stripe secret key is managed by Crave on the backend — you only need the publishable key on the client.
Next steps
Order Tracking Track order status after payment.
Error Codes Handle payment and API errors.