Skip to main content

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:
Use test mode keys (sk_test_... / pk_test_...) during development. Switch to live keys only when you’re ready to accept real payments.

Environment Variables

Add the following environment variables to your API server:
# 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:
# Stripe publishable key (safe for client-side usage)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxx
Never expose STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET to the client. These must only be used server-side.

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.
1
Create a cart and add items
2
Use the SDK to create a cart and add products:
3
TypeScript
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
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},
)
cURL
# 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}'
4
Initiate checkout with Stripe
5
Call the checkout endpoint with payment_method: 'stripe'. The API creates a Stripe PaymentIntent and returns a client_secret:
6
TypeScript
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
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
cURL
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"
  }'
7
Confirm payment on the client
8
Use Stripe.js on your frontend to confirm the payment with the client_secret:
9
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
}
10
If you’re using Stripe Elements for a custom payment form:
11
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>
  );
}
12
Webhook confirms payment automatically
13
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.
14
No additional code is needed on your part — the platform handles webhook verification and order updates internally.

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 DevelopersWebhooks in the Stripe Dashboard
  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 EventHeadless Commerce Action
payment_intent.succeededOrder status set to confirmed, payment marked completed
payment_intent.payment_failedPayment marked failed, payment.failed webhook sent
charge.refundedRefund recorded, payment.refunded webhook sent
charge.dispute.createdOrder flagged for review

Refunds

Issue full or partial refunds through the Admin API:
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',
});
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 typePrefixExample
Publishable keypk_test_pk_test_51ABC...
Secret keysk_test_sk_test_51ABC...

Test card numbers

Use these card numbers in test mode:
Card NumberScenario
4242 4242 4242 4242Successful payment
4000 0000 0000 32203D Secure authentication required
4000 0000 0000 9995Payment declined (insufficient funds)
4000 0000 0000 0002Generic 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:
# 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:
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);
    }
  }
}
Common Stripe-specific errors after confirmPayment():
Error CodeDescriptionSuggested Action
card_declinedThe card was declinedAsk customer to use a different card
expired_cardThe card has expiredAsk customer to update card details
insufficient_fundsInsufficient fundsAsk customer to use a different card
processing_errorProcessing error at StripeRetry the payment
incorrect_cvcIncorrect CVCAsk customer to re-enter CVC

Next Steps

Webhooks

Learn about all webhook events and payload formats.

TossPayments

Set up TossPayments for Korean payment methods.

API Reference

Explore all checkout and payment endpoints.

SDKs

Full SDK reference with every resource and method.