> ## 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 연동

> Headless Commerce에서 Stripe 결제 설정하기

# Stripe 연동

Stripe를 결제 수단으로 사용하여 신용카드, Apple Pay, Google Pay 및 40개 이상의 결제 수단을 전 세계적으로 지원할 수 있습니다.

## 사전 요구 사항

시작하기 전에 다음을 준비해주세요:

* **Stripe 계정** — 계정이 없다면 [stripe.com](https://stripe.com)에서 가입하세요
* [Stripe Dashboard](https://dashboard.stripe.com/apikeys)에서 **Stripe API 키** 확인
* 최소 하나 이상의 상품이 등록된 Headless Commerce 스토어

<Note>
  개발 중에는 **테스트 모드** 키(`sk_test_...` / `pk_test_...`)를 사용하세요. 실제 결제를 받을 준비가 되었을 때만 라이브 키로 전환하세요.
</Note>

## 환경 변수

API 서버에 다음 환경 변수를 추가하세요:

```bash theme={null}
# Stripe 시크릿 키 (서버 전용 — 클라이언트에 절대 노출하지 마세요)
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx

# Stripe 웹훅 서명 시크릿 (Stripe Dashboard → Webhooks에서 확인)
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
```

프론트엔드 애플리케이션에는 퍼블리셔블 키를 추가하세요:

```bash theme={null}
# Stripe 퍼블리셔블 키 (클라이언트에서 안전하게 사용 가능)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxx
```

<Warning>
  `STRIPE_SECRET_KEY`와 `STRIPE_WEBHOOK_SECRET`은 절대 클라이언트에 노출하지 마세요. 서버 측에서만 사용해야 합니다.
</Warning>

## 결제 흐름

Stripe 결제 흐름은 네 단계로 구성됩니다: 장바구니 생성, 체크아웃 시작, 클라이언트에서 결제 확인, 웹훅 처리.

<Steps>
  ### 장바구니 생성 및 상품 추가

  SDK를 사용하여 장바구니를 생성하고 상품을 추가합니다:

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

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

    // 장바구니 생성
    const cart = await client.carts.create({
      session_id: 'session_abc123',
    });

    // 상품 추가
    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",
    }

    # 장바구니 생성
    cart = requests.post(f"{base_url}/storefront/carts", headers=headers, json={
        "session_id": "session_abc123",
    }).json()

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

    ```bash cURL theme={null}
    # 장바구니 생성
    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"}'

    # 상품 추가
    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>

  ### Stripe로 체크아웃 시작

  `payment_method: 'stripe'`를 지정하여 체크아웃 엔드포인트를 호출합니다. API가 Stripe PaymentIntent를 생성하고 `client_secret`을 반환합니다:

  <CodeGroup>
    ```typescript TypeScript theme={null}
    const order = await client.carts.checkout(cart.id, {
      email: 'customer@example.com',
      shipping_address: {
        line1: '서울특별시 강남구 테헤란로 123',
        city: '서울',
        postal_code: '06234',
        country: 'KR',
      },
      payment_method: 'stripe',
    });

    // 응답에 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",
                "city": "서울",
                "postal_code": "06234",
                "country": "KR",
            },
            "payment_method": "stripe",
        },
    ).json()

    # 응답에 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",
          "city": "서울",
          "postal_code": "06234",
          "country": "KR"
        },
        "payment_method": "stripe"
      }'
    ```
  </CodeGroup>

  ### 클라이언트에서 결제 확인

  프론트엔드에서 Stripe.js를 사용하여 `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) {
    // 고객에게 오류 표시 (예: 잔액 부족, 카드 거절)
    console.error(error.message);
  } else {
    // 결제 처리 중 — Stripe가 return_url로 리다이렉트합니다
  }
  ```

  Stripe Elements를 사용한 커스텀 결제 폼의 경우:

  ```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}>
          결제하기
        </button>
      </form>
    );
  }
  ```

  ### 웹훅이 자동으로 결제를 확인합니다

  고객이 결제를 완료하면 Stripe가 서버로 웹훅 이벤트를 전송합니다. Headless Commerce가 이를 자동으로 처리하여 주문 상태를 `confirmed`로 업데이트합니다.

  별도의 추가 코드가 필요하지 않습니다 — 플랫폼이 웹훅 검증과 주문 업데이트를 내부적으로 처리합니다.
</Steps>

## 웹훅 설정

Headless Commerce는 Stripe 웹훅 이벤트를 수신하여 주문 및 결제 상태를 동기화합니다. Stripe Dashboard에서 웹훅을 설정하세요:

1. [Stripe Dashboard](https://dashboard.stripe.com/webhooks)에서 **Developers** → **Webhooks**로 이동
2. **Add endpoint** 클릭
3. 웹훅 URL 입력: `https://api.headlesscommerce.io/v1/webhooks/stripe`
4. 다음 이벤트를 선택:
   * `payment_intent.succeeded`
   * `payment_intent.payment_failed`
   * `charge.refunded`
   * `charge.dispute.created`
5. **Signing secret**을 복사하여 `STRIPE_WEBHOOK_SECRET`으로 설정

### 이벤트 처리

| Stripe 이벤트 | Headless Commerce 동작 |
| - | - |
| `payment_intent.succeeded` | 주문 상태 `confirmed`로 변경, 결제 `completed` 처리 |
| `payment_intent.payment_failed` | 결제 `failed` 처리, `payment.failed` 웹훅 전송 |
| `charge.refunded` | 환불 기록, `payment.refunded` 웹훅 전송 |
| `charge.dispute.created` | 주문에 검토 플래그 설정 |

## 환불

Admin API를 통해 전액 또는 부분 환불을 처리할 수 있습니다:

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

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

  // 전액 환불
  const refund = await admin.orders.refund('order_xxxxxxxx', {
    reason: 'customer_request',
  });

  // 부분 환불
  const partialRefund = await admin.orders.refund('order_xxxxxxxx', {
    amount: 1500,
    reason: '배송 중 상품 파손',
  });
  ```

  ```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",
  }

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

  # 부분 환불
  partial_refund = requests.post(
      f"{admin_url}/admin/orders/order_xxxxxxxx/refunds",
      headers=admin_headers,
      json={"amount": 1500, "reason": "배송 중 상품 파손"},
  ).json()
  ```

  ```bash cURL theme={null}
  # 전액 환불
  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"}'

  # 부분 환불
  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": "배송 중 상품 파손"
    }'
  ```
</CodeGroup>

환불은 Stripe를 통해 자동으로 처리되며 주문 상태가 그에 따라 업데이트됩니다.

## 테스트

### 테스트 모드 키

개발 중에는 항상 테스트 모드 API 키를 사용하세요. 테스트 키로 생성된 거래는 실제 결제 네트워크를 사용하지 않습니다.

| 키 유형 | 접두사 | 예시 |
| - | - | - |
| 퍼블리셔블 키 | `pk_test_` | `pk_test_51ABC...` |
| 시크릿 키 | `sk_test_` | `sk_test_51ABC...` |

### 테스트 카드 번호

테스트 모드에서 다음 카드 번호를 사용할 수 있습니다:

| 카드 번호 | 시나리오 |
| - | - |
| `4242 4242 4242 4242` | 결제 성공 |
| `4000 0000 0000 3220` | 3D Secure 인증 필요 |
| `4000 0000 0000 9995` | 결제 거절 (잔액 부족) |
| `4000 0000 0000 0002` | 일반 카드 거절 |

만료일은 미래 날짜, CVC는 아무 3자리 숫자, 우편번호는 아무 값이나 사용할 수 있습니다.

### 로컬에서 웹훅 테스트

Stripe CLI를 사용하여 로컬 개발 서버로 웹훅 이벤트를 전달할 수 있습니다:

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

# Stripe 계정 로그인
stripe login

# 로컬 엔드포인트로 이벤트 전달
stripe listen --forward-to localhost:3000/v1/webhooks/stripe

# 다른 터미널에서 테스트 이벤트 트리거
stripe trigger payment_intent.succeeded
```

## 오류 처리

프론트엔드에서 결제 오류를 적절히 처리하세요:

<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':
          // 장바구니가 비어 있음
          break;
        case 'insufficient_stock':
          // 하나 이상의 상품 재고가 부족함
          break;
        case 'payment_provider_error':
          // Stripe에서 PaymentIntent 생성 중 오류 반환
          break;
        default:
          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  # 장바구니가 비어 있음
      elif code == "insufficient_stock":
          pass  # 하나 이상의 상품 재고가 부족함
      elif code == "payment_provider_error":
          pass  # Stripe에서 오류 반환
      else:
          print(error.get("message"))
  ```
</CodeGroup>

`confirmPayment()` 이후 발생할 수 있는 주요 Stripe 오류:

| 오류 코드 | 설명 | 권장 조치 |
| - | - | - |
| `card_declined` | 카드가 거절됨 | 다른 카드 사용 요청 |
| `expired_card` | 카드가 만료됨 | 카드 정보 업데이트 요청 |
| `insufficient_funds` | 잔액 부족 | 다른 카드 사용 요청 |
| `processing_error` | Stripe 처리 오류 | 결제 재시도 |
| `incorrect_cvc` | CVC 불일치 | CVC 재입력 요청 |

## 다음 단계

<CardGroup cols={2}>
  <Card title="웹훅" icon="bell" href="/ko/webhooks">
    모든 웹훅 이벤트와 페이로드 형식을 확인하세요.
  </Card>

  <Card title="TossPayments" icon="credit-card" href="/ko/guides/tosspayments-integration">
    한국 결제 수단을 위한 TossPayments를 설정하세요.
  </Card>

  <Card title="API 레퍼런스" icon="book-open" href="/ko/api-reference/introduction">
    모든 체크아웃 및 결제 엔드포인트를 살펴보세요.
  </Card>

  <Card title="SDK" icon="code" href="/ko/sdks">
    전체 SDK 레퍼런스와 모든 리소스 및 메서드를 확인하세요.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.