> ## Documentation Index
> Fetch the complete documentation index at: https://docs.craveup.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Accept Payments

> Set up Stripe integration for secure payment processing in your storefront.

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

<Steps>
  <Step title="Cart is finalized">
    The customer has added items, set fulfillment, and provided their details.
  </Step>

  <Step title="Create a payment intent">
    Your server calls the Crave API to get a Stripe `clientSecret`.
  </Step>

  <Step title="Collect payment on the client">
    Stripe.js securely collects card details using the `clientSecret`.
  </Step>

  <Step title="Stripe confirms the charge">
    The payment is processed through the restaurant's connected Stripe account.
  </Step>
</Steps>

Crave handles Stripe Connect setup for each restaurant. You only need to integrate Stripe.js on your storefront frontend.

## Create a payment intent

<CodeGroup>
  ```ts SDK theme={null}
  const payment = await storefront.payments.createIntent('loc_123', cartId);

  // payment.clientSecret — use with Stripe.js
  // payment.stripeAccountId — the restaurant's connected account
  ```

  ```bash REST theme={null}
  curl -X GET "https://api.craveup.com/api/v1/stripe/payment-intent?locationId=loc_123&cartId=cart_456" \
    -H "X-API-Key: $CRAVEUP_API_KEY"
  ```
</CodeGroup>

The response includes:

| Field             | Description                                                     |
| ----------------- | --------------------------------------------------------------- |
| `clientSecret`    | The Stripe PaymentIntent client secret                          |
| `stripeAccountId` | The connected Stripe account to pass to `stripe.confirmPayment` |

## Install Stripe.js

```bash theme={null}
pnpm add @stripe/stripe-js @stripe/react-stripe-js
```

## Set up the Stripe provider

Wrap your checkout page with the Stripe Elements provider.

```tsx filename="app/checkout/layout.tsx" theme={null}
'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>
  );
}
```

<Warning>
  You must use the restaurant's `stripeAccountId` from the payment intent response when initializing Stripe. This routes the payment to the correct connected account.
</Warning>

## Collect payment

```tsx filename="components/PaymentForm.tsx" theme={null}
'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

```tsx filename="app/checkout/page.tsx" theme={null}
'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 4242` | Successful payment                |
| `4000 0000 0000 3220` | Requires 3D Secure authentication |
| `4000 0000 0000 0002` | Card declined                     |
| `4000 0025 0000 3155` | Requires authentication (SCA)     |

Use any future expiry date and any 3-digit CVC.

<Tip>
  Set your environment to use Stripe test keys (`pk_test_...` / `sk_test_...`) during development. Crave sandbox locations automatically use Stripe test mode.
</Tip>

## Environment variables

Add these to your `.env.local`:

```env filename=".env.local" theme={null}
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

<CardGroup cols={2}>
  <Card title="Order Tracking" icon="location-dot" href="/guides/order-tracking">
    Track order status after payment.
  </Card>

  <Card title="Error Codes" icon="triangle-exclamation" href="/guides/error-codes">
    Handle payment and API errors.
  </Card>
</CardGroup>
