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

# Laravel Cashier (Stripe)

> Laravel Cashier (Stripe)를 사용해 Subscription 과금, 1회 결제, 청구서, Webhook을 구현하는 기본 플로우를 설명합니다.

## 개요

Laravel Cashier (Stripe)는 Stripe의 과금 기능을 Laravel에서 다루기 위한 공식 패키지입니다. Subscription의 생성·상태 확인·취소, 1회 결제, 청구서 다운로드, Webhook 처리를 통일된 API로 구현할 수 있습니다.

## 설치와 설정

먼저 Cashier를 설치하고, 필요한 테이블을 생성해 주세요.

```shell theme={null}
composer require laravel/cashier
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate
```

다음으로 `App\Models\User` 같은 과금 대상 모델에 `Billable` 트레이트를 추가합니다.

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}
```

`.env`에 Stripe 키를 설정합니다.

```ini theme={null}
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
```

## 고객 관리

Stripe Customer를 아직 생성하지 않았을 가능성이 있는 경우 `createOrGetStripeCustomer()`를 사용합니다.

```php theme={null}
$stripeCustomer = $user->createOrGetStripeCustomer();
```

명시적으로 생성하려면 `createAsStripeCustomer()`를 사용합니다.

```php theme={null}
$stripeCustomer = $user->createAsStripeCustomer();
```

## Subscription

### 신규 생성

`newSubscription()`과 `create()`로 Subscription을 시작합니다.
`$paymentMethodId`에는 Stripe.js 등에서 취득한 Payment Method ID를 전달합니다.

```php theme={null}
$user->newSubscription('default', 'price_monthly')
    ->create($paymentMethodId);
```

`price_monthly`는 예시입니다. 실제 구현 시에는 Stripe 대시보드에서 생성한 실제 Price ID를 지정해 주세요.

### 상태 확인

`subscribed()`로 유효한 Subscription인지 확인할 수 있습니다.

```php theme={null}
if ($user->subscribed('default')) {
    // Active subscription...
}
```

### 취소와 재개

```php theme={null}
$user->subscription('default')->cancel();

if ($user->subscription('default')->onGracePeriod()) {
    // The user is on the grace period...
}

$user->subscription('default')->resume();
```

```mermaid theme={null}
stateDiagram-v2
    [*] --> Active
    Active --> GracePeriod: cancel()
    GracePeriod --> Active: resume()
    GracePeriod --> Canceled: grace period ends
    Active --> Canceled: cancelNow()
```

## 1회 결제 (Charge)

`charge()`로 1회만 과금할 수 있습니다. 금액은 통화의 최소 단위로 전달해 주세요(예: USD라면 `100`은 `$1.00`).
여기서도 `$paymentMethodId`에는 Stripe에서 생성한 Payment Method ID를 사용합니다.

```php theme={null}
$payment = $user->charge(100, $paymentMethodId);
```

과금에 실패한 경우 `charge()`는 예외를 던집니다.

## Payment Element

Stripe의 [Payment Element](https://stripe.com/docs/payments/payment-element)를 사용하면 카드·Apple Pay·Google Pay·iDEAL 등 여러 결제 수단을 통일적으로 다룰 수 있습니다.

### Subscription에서의 이용

Setup Intent를 생성해 뷰에 전달합니다.

```php theme={null}
return view('subscribe', [
    'intent' => $user->createSetupIntent()
]);
```

Blade 뷰에서 Payment Element를 마운트하고, Setup Intent의 `client_secret`을 전달합니다.

```html theme={null}
<div id="payment-element"></div>
<button id="submit">Subscribe</button>

<script src="https://js.stripe.com/v3/"></script>
<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements({
        clientSecret: '{{ $intent->client_secret }}'
    });

    const paymentElement = elements.create('payment');

    paymentElement.mount('#payment-element');

    document.getElementById('submit').addEventListener('click', async () => {
        const { error } = await stripe.confirmSetup({
            elements,
            confirmParams: {
                return_url: '{{ route("subscription.complete") }}',
            },
        });

        if (error) {
            // 오류 메시지를 사용자에게 표시...
        }
    });
</script>
```

Stripe의 리다이렉트 대상 URL에는 `setup_intent` 파라미터가 부여됩니다. 이를 사용해 Payment Method ID를 취득하고, Subscription을 생성합니다.

```php theme={null}
use Illuminate\Http\Request;

Route::get('/subscription/complete', function (Request $request) {
    $setupIntent = $request->user()->findSetupIntent(
        $request->setup_intent
    );

    $paymentMethod = $setupIntent->payment_method;

    $request->user()
        ->newSubscription('default', 'price_xxx')
        ->create($paymentMethod);

    return redirect('/dashboard');
})->name('subscription.complete');
```

### 1회 결제에서의 이용

1회 결제에서는 `pay()`로 Payment Intent를 생성합니다. Order 모델에는 `user_id`, `amount`, `status`, `stripe_payment_intent_id` 컬럼이 필요합니다.

```php theme={null}
use App\Models\Order;
use Illuminate\Http\Request;

Route::post('/pay', function (Request $request) {
    $amount = 1000;

    $payment = $request->user()->pay($amount);

    $order = Order::create([
        'user_id' => $request->user()->id,
        'amount' => $amount,
        'status' => 'pending',
        'stripe_payment_intent_id' => $payment->id,
    ]);

    return view('checkout', [
        'clientSecret' => $payment->client_secret,
        'order' => $order,
    ]);
});
```

Payment Element를 마운트해 결제를 확정합니다.

```html theme={null}
<div id="payment-element"></div>
<button id="submit">Pay Now</button>

<script src="https://js.stripe.com/v3/"></script>
<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements({
        clientSecret: '{{ $clientSecret }}'
    });

    const paymentElement = elements.create('payment');

    paymentElement.mount('#payment-element');

    document.getElementById('submit').addEventListener('click', async () => {
        const { error } = await stripe.confirmPayment({
            elements,
            confirmParams: {
                return_url: '{{ route("payment.complete") }}',
            },
        });

        if (error) {
            // 오류 메시지를 사용자에게 표시...
        }
    });
</script>
```

리다이렉트 후에는 `payment_intent` 파라미터로 Order를 검색하고, Payment Intent가 인증된 사용자의 것이며 `succeeded` 상태인지 확인한 후 주문을 확정합니다.

```php theme={null}
use App\Models\Order;
use Illuminate\Http\Request;

Route::get('/payment/complete', function (Request $request) {
    $order = Order::where('user_id', $request->user()->id)
        ->where('stripe_payment_intent_id', $request->payment_intent)
        ->firstOrFail();

    $paymentIntent = $request->user()
        ->stripe()
        ->paymentIntents
        ->retrieve($request->payment_intent);

    if ($paymentIntent->customer === $request->user()->stripe_id &&
        $paymentIntent->status === 'succeeded') {
        $order->update(['status' => 'paid']);

        // 주문을 확정하는 처리...
    }

    return redirect('/dashboard');
})->name('payment.complete');
```

```mermaid theme={null}
sequenceDiagram
    participant User as 사용자
    participant App as Laravel<br>앱
    participant Stripe

    User->>App: POST /pay
    App->>Stripe: pay()로 PaymentIntent 생성
    Stripe-->>App: client_secret
    App-->>User: checkout 뷰 표시
    User->>Stripe: confirmPayment()
    Stripe-->>User: return_url로 리다이렉트
    User->>App: GET /payment/complete
    App->>Stripe: PaymentIntent 가져오기·검증
    App->>App: order.status = paid
    App-->>User: 대시보드로 리다이렉트
```

## 청구서

청구서 목록은 `invoices()`로 가져올 수 있습니다.

```php theme={null}
$invoices = $user->invoices();
```

PDF 다운로드에는 `dompdf/dompdf`를 설치하고, `downloadInvoice()`를 사용합니다. `$invoiceId`에는 `invoices()`로 가져온 청구서 ID를 전달해 주세요.

```shell theme={null}
composer require dompdf/dompdf
```

```php theme={null}
return $user->downloadInvoice($invoiceId);
```

## Webhook 설정

Cashier는 Stripe Webhook용 라우트를 자동 등록하며, 기본적으로 `/stripe/webhook`을 사용합니다. Stripe 대시보드에 이 URL을 설정해 주세요.

Webhook 생성은 `cashier:webhook`으로 실행할 수 있습니다.

```shell theme={null}
php artisan cashier:webhook
```

CSRF 보호에서 `stripe/*`를 제외해 주세요.

```php theme={null}
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(except: [
        'stripe/*',
    ]);
})
```

`STRIPE_WEBHOOK_SECRET`을 `.env`에 설정하면 Cashier의 서명 검증 미들웨어에서 Webhook 요청을 검증할 수 있습니다.


## Related topics

- [Laravel 10에서 11로의 업그레이드](/ko/blog/upgrade-10-to-11.md)
- [2026년 6월 Laravel 업데이트](/ko/blog/changelog/202606.md)
- [CSRF 보호](/ko/csrf.md)
- [Laravel 11 이후의 애플리케이션 구조](/ko/advanced/app-structure.md)
- [서비스 컨테이너](/ko/service-container.md)
