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

# Stripe Integration

> Set up Stripe payments with Headless Commerce

# Stripe Integration

Accept credit cards, Apple Pay, Google Pay, and 40+ payment methods globally using Stripe as your payment provider.

## Prerequisites

Before you begin, make sure you have:

* A **Stripe account** — [sign up at stripe.com](https://stripe.com) if you don't have one
* Your **Stripe API keys** from the [Stripe Dashboard](https://dashboard.stripe.com/apikeys)
* A working Headless Commerce store with at least one product

<Note>
  Use **test mode** keys (`sk_test_...` / `pk_test_...`) during development. Switch to live keys only when you're ready to accept real payments.
</Note>

## Environment Variables

Add the following environment variables to your API server:

```bash theme={null}
# Stripe secret key (server-side only — never expose this to the client)
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx

# Stripe webhook signing secret (from the Stripe Dashboard → Webhooks)
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
```

For your frontend application, add the publishable key:

```bash theme={null}
# Stripe publishable key (safe for client-side usage)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxx
```

<Warning>
  Never expose `STRIPE_SECRET_KEY` or `STRIPE_WEBHOOK_SECRET` to the client. These must only be used server-side.
</Warning>

## Checkout Flow

The complete Stripe checkout flow involves four steps: creating a cart, initiating checkout, confirming payment on the client, and handling the webhook confirmation.

<Steps>
  ### Create a cart and add items

  Use the SDK to create a cart and add products:

  <CodeGroup>
    ```typescript TypeScript theme={null}
    import { createStorefrontClient } from '@headless-commerce/sdk';

    const client = createStorefrontClient({
      apiKey: process.env.NEXT_PUBLIC_HC_API_KEY!,
    });

    // Create a cart
    const cart = await client.carts.create({
      session_id: 'session_abc123',
    });

    // Add items
    await client.carts.addItem(cart.id, {
      variant_id: 'var_xxx',
      quantity: 2,
    });
    ```

    ```python Python theme={null}
    import requests

    base_url = "https://api.headlesscommerce.io/v1"
    headers = {
        "Authorization": "Bearer pk_test_your_key",
        "Content-Type": "application/json",
    }

    # Create a cart
    cart = requests.post(f"{base_url}/storefront/carts", headers=headers, json={
        "session_id": "session_abc123",
    }).json()

    # Add items
    requests.post(
        f"{base_url}/storefront/carts/{cart['id']}/items",
        headers=headers,
        json={"variant_id": "var_xxx", "quantity": 2},
    )
    ```

    ```bash cURL theme={null}
    # Create a cart
    curl -X POST https://api.headlesscommerce.io/v1/storefront/carts \
      -H "Authorization: Bearer pk_test_your_key" \
      -H "Content-Type: application/json" \
      -d '{"session_id": "session_abc123"}'

    # Add items
    curl -X POST https://api.headlesscommerce.io/v1/storefront/carts/{cart_id}/items \
      -H "Authorization: Bearer pk_test_your_key" \
      -H "Content-Type: application/json" \
      -d '{"variant_id": "var_xxx", "quantity": 2}'
    ```
  </CodeGroup>

  ### Initiate checkout with Stripe

  Call the checkout endpoint with `payment_method: 'stripe'`. The API creates a Stripe PaymentIntent and returns a `client_secret`:

  <CodeGroup>
    ```typescript TypeScript theme={null}
    const order = await client.carts.checkout(cart.id, {
      email: 'customer@example.com',
      shipping_address: {
        line1: '123 Main St',
        city: 'San Francisco',
        state: 'CA',
        postal_code: '94105',
        country: 'US',
      },
      payment_method: 'stripe',
    });

    // The response includes the Stripe client_secret
    const { client_secret } = order.payment;
    console.log(order.id);           // order_xxxxxxxx
    console.log(client_secret);      // pi_xxx_secret_xxx
    ```

    ```python Python theme={null}
    order = requests.post(
        f"{base_url}/storefront/carts/{cart['id']}/checkout",
        headers=headers,
        json={
            "email": "customer@example.com",
            "shipping_address": {
                "line1": "123 Main St",
                "city": "San Francisco",
                "state": "CA",
                "postal_code": "94105",
                "country": "US",
            },
            "payment_method": "stripe",
        },
    ).json()

    # The response includes the Stripe client_secret
    client_secret = order["payment"]["client_secret"]
    print(order["id"])          # order_xxxxxxxx
    print(client_secret)        # pi_xxx_secret_xxx
    ```

    ```bash cURL theme={null}
    curl -X POST https://api.headlesscommerce.io/v1/storefront/carts/{cart_id}/checkout \
      -H "Authorization: Bearer pk_test_your_key" \
      -H "Content-Type: application/json" \
      -d '{
        "email": "customer@example.com",
        "shipping_address": {
          "line1": "123 Main St",
          "city": "San Francisco",
          "state": "CA",
          "postal_code": "94105",
          "country": "US"
        },
        "payment_method": "stripe"
      }'
    ```
  </CodeGroup>

  ### Confirm payment on the client

  Use Stripe.js on your frontend to confirm the payment with the `client_secret`:

  ```typescript theme={null}
  import { loadStripe } from '@stripe/stripe-js';

  const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);

  const { error } = await stripe!.confirmPayment({
    clientSecret: client_secret,
    confirmParams: {
      return_url: 'https://your-store.com/order/confirmation',
    },
  });

  if (error) {
    // Show error to the customer (e.g., insufficient funds, card declined)
    console.error(error.message);
  } else {
    // Payment is processing — Stripe will redirect to return_url
  }
  ```

  If you're using Stripe Elements for a custom payment form:

  ```typescript theme={null}
  import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';

  function CheckoutForm({ clientSecret }: { clientSecret: string }) {
    const stripe = useStripe();
    const elements = useElements();

    const handleSubmit = async (e: React.FormEvent) => {
      e.preventDefault();
      if (!stripe || !elements) return;

      const { error } = await stripe.confirmPayment({
        elements,
        confirmParams: {
          return_url: 'https://your-store.com/order/confirmation',
        },
      });

      if (error) {
        console.error(error.message);
      }
    };

    return (
      <form onSubmit={handleSubmit}>
        <PaymentElement />
        <button type="submit" disabled={!stripe}>
          Pay now
        </button>
      </form>
    );
  }
  ```

  ### Webhook confirms payment automatically

  Once the customer completes payment, Stripe sends a webhook event to your server. Headless Commerce processes this automatically and updates the order status to `confirmed`.

  No additional code is needed on your part — the platform handles webhook verification and order updates internally.
</Steps>

## Webhook Setup

Headless Commerce listens for Stripe webhook events to keep order and payment statuses in sync. Configure webhooks in the Stripe Dashboard:

1. Go to **Developers** → **Webhooks** in the [Stripe Dashboard](https://dashboard.stripe.com/webhooks)
2. Click **Add endpoint**
3. Enter your webhook URL: `https://api.headlesscommerce.io/v1/webhooks/stripe`
4. Select the following events:
   * `payment_intent.succeeded`
   * `payment_intent.payment_failed`
   * `charge.refunded`
   * `charge.dispute.created`
5. Copy the **Signing secret** and set it as `STRIPE_WEBHOOK_SECRET`

### Event handling

| Stripe Event                    | Headless Commerce Action                                    |
| ------------------------------- | ----------------------------------------------------------- |
| `payment_intent.succeeded`      | Order status set to `confirmed`, payment marked `completed` |
| `payment_intent.payment_failed` | Payment marked `failed`, `payment.failed` webhook sent      |
| `charge.refunded`               | Refund recorded, `payment.refunded` webhook sent            |
| `charge.dispute.created`        | Order flagged for review                                    |

## Refunds

Issue full or partial refunds through the Admin API:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { createAdminClient } from '@headless-commerce/sdk';

  const admin = createAdminClient({
    apiKey: process.env.HC_ADMIN_API_KEY!,
  });

  // Full refund
  const refund = await admin.orders.refund('order_xxxxxxxx', {
    reason: 'customer_request',
  });

  // Partial refund
  const partialRefund = await admin.orders.refund('order_xxxxxxxx', {
    amount: 1500,
    reason: 'Item damaged during shipping',
  });
  ```

  ```python Python theme={null}
  import requests

  admin_url = "https://api.headlesscommerce.io/v1"
  admin_headers = {
      "Authorization": "Bearer sk_test_your_key",
      "Content-Type": "application/json",
  }

  # Full refund
  refund = requests.post(
      f"{admin_url}/admin/orders/order_xxxxxxxx/refunds",
      headers=admin_headers,
      json={"reason": "customer_request"},
  ).json()

  # Partial refund
  partial_refund = requests.post(
      f"{admin_url}/admin/orders/order_xxxxxxxx/refunds",
      headers=admin_headers,
      json={"amount": 1500, "reason": "Item damaged during shipping"},
  ).json()
  ```

  ```bash cURL theme={null}
  # Full refund
  curl -X POST https://api.headlesscommerce.io/v1/admin/orders/{orderId}/refunds \
    -H "Authorization: Bearer sk_test_your_key" \
    -H "Content-Type: application/json" \
    -d '{"reason": "customer_request"}'

  # Partial refund
  curl -X POST https://api.headlesscommerce.io/v1/admin/orders/{orderId}/refunds \
    -H "Authorization: Bearer sk_test_your_key" \
    -H "Content-Type: application/json" \
    -d '{
      "amount": 1500,
      "reason": "Item damaged during shipping"
    }'
  ```
</CodeGroup>

The refund is processed through Stripe automatically and the order is updated accordingly.

## Testing

### Test mode keys

Always use test mode API keys during development. These keys create test-only transactions that never hit real payment networks.

| Key type        | Prefix     | Example            |
| --------------- | ---------- | ------------------ |
| Publishable key | `pk_test_` | `pk_test_51ABC...` |
| Secret key      | `sk_test_` | `sk_test_51ABC...` |

### Test card numbers

Use these card numbers in test mode:

| Card Number           | Scenario                              |
| --------------------- | ------------------------------------- |
| `4242 4242 4242 4242` | Successful payment                    |
| `4000 0000 0000 3220` | 3D Secure authentication required     |
| `4000 0000 0000 9995` | Payment declined (insufficient funds) |
| `4000 0000 0000 0002` | Generic card decline                  |

Use any future expiration date, any 3-digit CVC, and any postal code.

### Testing webhooks locally

Use the Stripe CLI to forward webhook events to your local development server:

```bash theme={null}
# Install the Stripe CLI
brew install stripe/stripe-cli/stripe

# Login to your Stripe account
stripe login

# Forward events to your local endpoint
stripe listen --forward-to localhost:3000/v1/webhooks/stripe

# In another terminal, trigger a test event
stripe trigger payment_intent.succeeded
```

## Error Handling

Handle payment errors gracefully in your frontend:

<CodeGroup>
  ```typescript TypeScript theme={null}
  try {
    const order = await client.carts.checkout(cart.id, {
      email: 'customer@example.com',
      shipping_address: { /* ... */ },
      payment_method: 'stripe',
    });
  } catch (error: unknown) {
    if (error instanceof Error && 'code' in error) {
      const apiError = error as { code: string; message: string };
      switch (apiError.code) {
        case 'cart_empty':
          // Cart has no items
          break;
        case 'insufficient_stock':
          // One or more items are out of stock
          break;
        case 'payment_provider_error':
          // Stripe returned an error during PaymentIntent creation
          break;
        default:
          // Unexpected error
          console.error(apiError.message);
      }
    }
  }
  ```

  ```python Python theme={null}
  try:
      order = requests.post(
          f"{base_url}/storefront/carts/{cart_id}/checkout",
          headers=headers,
          json={
              "email": "customer@example.com",
              "shipping_address": { ... },
              "payment_method": "stripe",
          },
      )
      order.raise_for_status()
  except requests.exceptions.HTTPError as e:
      error = e.response.json()
      code = error.get("code")
      if code == "cart_empty":
          pass  # Cart has no items
      elif code == "insufficient_stock":
          pass  # One or more items are out of stock
      elif code == "payment_provider_error":
          pass  # Stripe returned an error
      else:
          print(error.get("message"))
  ```
</CodeGroup>

Common Stripe-specific errors after `confirmPayment()`:

| Error Code           | Description                | Suggested Action                     |
| -------------------- | -------------------------- | ------------------------------------ |
| `card_declined`      | The card was declined      | Ask customer to use a different card |
| `expired_card`       | The card has expired       | Ask customer to update card details  |
| `insufficient_funds` | Insufficient funds         | Ask customer to use a different card |
| `processing_error`   | Processing error at Stripe | Retry the payment                    |
| `incorrect_cvc`      | Incorrect CVC              | Ask customer to re-enter CVC         |

## Next Steps

<CardGroup cols={2}>
  <Card title="Webhooks" icon="bell" href="/webhooks">
    Learn about all webhook events and payload formats.
  </Card>

  <Card title="TossPayments" icon="credit-card" href="/guides/tosspayments-integration">
    Set up TossPayments for Korean payment methods.
  </Card>

  <Card title="API Reference" icon="book-open" href="/api-reference/introduction">
    Explore all checkout and payment endpoints.
  </Card>

  <Card title="SDKs" icon="code" href="/sdks">
    Full SDK reference with every resource and method.
  </Card>
</CardGroup>
