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

# Stripe 결제

> 토큰 할당량 및 셀프서비스 플랜 관리를 포함한 사용자별 Stripe 구독(Free + Pro) 활성화

FIM One은 관리자가 제어하는 기능 플래그 뒤에 완전한 Stripe 결제 파이프라인을 제공합니다. 결제가 필요 없는 프라이빗 배포는 이를 비활성화하고 UI를 볼 수 없습니다. SaaS 운영자는 한 번의 토글로 호스팅된 Checkout, Customer Portal, 웹훅 기반 구독 생명주기 및 할당량 적용을 즉시 사용할 수 있습니다.

<Note>
  결제는 **기본적으로 비활성화**되어 있습니다. 새로운 설치 및 기존 셀프호스트 모두 `system_settings.billing_enabled = FALSE`로 시작합니다. 관리자가 명시적으로 활성화할 때까지 결제 UI는 표시되지 않습니다.
</Note>

## 무엇을 얻을 수 있는가

* **Free + Pro 티어** — 월간 토큰 할당량 포함 (기본값: Free 1M / Pro 5M; 활성화 후 조정 가능)
* **Stripe 호스팅 Checkout** — 사용자가 업그레이드할 때 카드 데이터가 코드를 거치지 않음
* **Customer Portal** — 사용자가 결제 방법 업데이트, 청구서 다운로드, 구독 취소 — 모두 Stripe UI에서 처리
* **Webhook 기반 라이프사이클** — 구독 자동 프로비저닝 및 갱신; 취소된 구독은 기간 종료 시 Free로 강등
* **할당량 적용** — 기간당 토큰 사용량 추적; 스트림 중 차단 및 구조화된 업그레이드 프롬프트
* **관리자 페이지** — 요금제 CRUD 및 구독 모니터링

## 전제 조건

1. **Stripe 계정** (라이브 모드 활성화됨). 싱가포르 법인은 KYC 완료 필요(사업 UEN, 이사 신분증, 은행 계좌). 승인은 일반적으로 1-3일 소요됩니다.
2. **Stripe 라이브 API 키** (제한된 유형 권장 — 표준 `sk_live_***`보다 해지하기 쉽고 권한 범위 지정 가능).
3. **웹훅 엔드포인트** (`<your-domain>/api/webhooks/stripe`에서 공개적으로 접근 가능).
4. **은행 계좌** (지급용). 다중 통화 결제(예: USD 계좌로 USD 지급)는 USD 기본값이 아닌 Stripe 계정의 경우 거래당 1.5-2% FX 손실을 피하기 위해 권장됩니다.

## 설정

### 1. Stripe 대시보드

#### Pro 상품 만들기

1. **Catalog → Products → + Add product**
2. Name: `Pro`, description: `5M tokens / month, priority support`
3. Pricing: Recurring, monthly, \$20.00 USD (adjust to your pricing strategy)
4. Save → copy the resulting `price_***` ID (you will UPDATE the local `billing_plans` table with this value after activation)

#### 제한된 API 키 생성

1. **Developers → API keys → + Create restricted key**
2. Name: `fim-one production`
3. Permissions (minimum):
   * **Customers**: Write
   * **Subscriptions**: Write
   * **Checkout Sessions**: Write
   * **Customer portal**: Write
   * **Prices**: Read
   * **Products**: Read
4. Save → copy `rk_live_***`

#### 웹훅 엔드포인트 등록

1. **Developers → Webhooks → + Add endpoint**
2. URL: `https://<your-domain>/api/webhooks/stripe`
3. 수신할 이벤트:
   * `checkout.session.completed`
   * `customer.subscription.created`
   * `customer.subscription.updated`
   * `customer.subscription.deleted`
   * `invoice.payment_succeeded`
   * `invoice.payment_failed`
4. 저장 후 "Reveal signing secret" 클릭 → `whsec_***` 복사

#### 다중 통화 결제 구성(권장)

Stripe 계정의 기본 통화가 청구하는 가격 통화와 다른 경우(일반적인 경우: SGD 계정에서 USD 청구):

1. **Settings → Bank accounts and currencies → Add a settlement currency**
2. 가격 통화 선택(예: USD)
3. 일치하는 은행 계좌 연결(예: Aspire USD 가상 계좌)
4. 저장 — Stripe가 USD 청구를 USD 지급으로 직접 라우팅하므로 환율 변환 없음

### 2. Backend `.env`

프로덕션 `.env`에서 다음 세 개의 키를 설정하세요:

```bash theme={null}
STRIPE_SECRET_KEY=rk_live_***
STRIPE_WEBHOOK_SECRET=whsec_***
STRIPE_BILLING_RETURN_URL=https://<your-domain>/settings?tab=billing
```

전체 참조는 [Environment Variables](/configuration/environment-variables)를 참조하세요.

`.env`를 편집한 후 백엔드를 다시 시작하여 키가 적용되도록 하세요:

```bash theme={null}
./deploy.sh   # or: docker compose restart fim-one
```

### 3. Admin에서 활성화

1. 관리자로 로그인
2. **Admin → System Settings → Billing**
3. **Enable Stripe Billing** 토글을 ON으로 설정
4. 백엔드가 `STRIPE_SECRET_KEY`와 `STRIPE_WEBHOOK_SECRET`이 모두 존재하는지 검증합니다 — 둘 중 하나라도 누락되면 400을 반환하고 토글은 OFF 상태로 유지됩니다
5. 처음 활성화할 때만 백엔드가 **멱등성 설정**을 실행합니다:
   * Free + Pro 플랜을 시드합니다 (이미 존재하면 건너뜀)
   * `system_settings.default_plan_id`를 Free 플랜 id로 설정합니다
   * 플랜이 없는 모든 사용자에 대해 `users.plan_id = free.id`를 역채우기합니다
   * `default_token_quota` → Free 플랜 `monthly_token_quota`를 동기화하여 기존 관리자 제어 전역 할당량이 유지되도록 합니다
6. 이후 토글 끄기/켜기는 **순수 플래그 전환**이며 데이터 부작용이 없습니다

### 4. Pro 플랜을 라이브 가격으로 업데이트

활성화 후, 시드된 Pro 플랜을 라이브 Stripe 가격으로 업데이트하세요:

* **Admin → Billing → Plans → Pro → Edit**
* 1단계에서 얻은 `price_1***`을 `Stripe Price ID`에 붙여넣기
* 저장

또는 SQL을 통해 (직접 DB 접근을 선호하는 경우):

```sql theme={null}
UPDATE billing_plans
SET stripe_price_id = 'price_1***'
WHERE slug = 'pro';
```

### 5. Smoke test

1. `/settings?tab=billing`을 일반 사용자로 열기
2. **Switch to Pro** 클릭
3. Stripe Checkout이 열림; 소액 실제 카드로 완료 (이후 환불)
4. Webhook이 실행되어야 함 — Stripe Dashboard → Webhooks → 최근 이벤트에서 2xx 응답 확인
5. `subscriptions` 테이블에 구독 행이 나타남; `users.plan_id`가 `pro`로 변경됨
6. UI에 Pro 플랜 + "Manage subscription" 버튼 표시

## 청구 비활성화

Admin → System Settings → Billing에서 **Enable Stripe Billing** 스위치를 OFF로 전환합니다.

비활성화되면:

* 모든 `/api/billing/*` 엔드포인트가 503을 반환합니다
* 웹훅 엔드포인트가 503을 반환합니다 (Stripe가 재시도한 후 Dashboard에 실패로 표시됩니다 — 청구가 영구적으로 비활성화된 경우 Stripe Dashboard에서 웹훅을 비활성화할 수 있습니다)
* `Plan & Billing` 사용자 대면 탭이 사라집니다
* Admin → Billing 네비게이션 그룹이 숨겨집니다
* 할당량 체인이 요금제 계층을 건너뛰고 `default_token_quota`로 직접 폴백합니다

**데이터는 보존됩니다**: 기존 `subscriptions`, `billing_plans`, `users.plan_id` 행은 변경되지 않습니다. 다시 활성화하면 마이그레이션 없이 동일한 상태에서 재개됩니다.

## 계산 참고 — 할당량 및 토큰 수학

이것은 사용자가 소비할 수 있는 것, 카운터가 언제 재설정되는지, 그리고 해결 체인이 어떻게 구성되는지를 결정하는 모든 숫자 규칙의 권위 있는 참고 자료입니다. 가격 책정을 변경하거나, 할당량을 조정하거나, 사용량 대시보드를 구축하거나, v2/v3 작업을 계획하기 전에 이것을 읽으세요. 아직 배포되지 않은 향후 규칙은 예약된 슬롯에 문서화되어 있으므로 기여자들이 새로운 로직이 어디에 연결되는지 알 수 있습니다.

### 용어집

| 변수                                    | 저장소                 | 의미                                           | 범위                    |
| ------------------------------------- | ------------------- | -------------------------------------------- | --------------------- |
| `users.token_quota`                   | per-user (override) | 3가지 상태 오버라이드; 아래 의미 참조                       | `NULL`, `0`, 또는 양의 정수 |
| `users.tokens_used_this_period`       | per-user (counter)  | 마지막 리셋 이후 누적 토큰                              | 음이 아닌 정수              |
| `users.quota_reset_at`                | per-user (anchor)   | 유료 사용자의 `Subscription.current_period_end` 반영 | timestamp             |
| `users.plan_id`                       | per-user (FK)       | 활성 플랜                                        | FK `billing_plans.id` |
| `billing_plans.monthly_token_quota`   | per-plan            | 이 플랜의 사용자에 대한 하드 캡                           | 음이 아닌 정수              |
| `system_settings.default_token_quota` | singleton           | 적용되는 플랜이 없을 때의 방어적 폴백                        | 음이 아닌 정수              |
| `system_settings.default_plan_id`     | singleton           | 신규/미할당 사용자를 위한 무료 플랜 포인터                     | FK 또는 `NULL`          |
| `system_settings.billing_enabled`     | singleton           | 마스터 스위치 — 체인의 2단계를 제어                        | boolean               |

### 토큰으로 계산되는 항목

토큰 소비는 LLM 호출 레이어에서 계산되며, 모든 완료 시 LiteLLM의 `usage` 객체에서 소싱됩니다.

* **계산됨**: 모든 모델 호출에서 프롬프트 토큰 + 완료 토큰
* **계산됨**: 다단계 / 도구 사용 에이전트 흐름의 모든 왕복 (각 모델 호출은 자체 차감)
* **계산됨**: 임베딩 요청 (KB 수집, 검색 점수 매김)
* **계산 안 됨**: 모델로 전송되지 않은 입력 (예: 사용자가 버린 업로드된 파일)
* **계산 안 됨**: 제공자에 도달하기 전에 실패한 요청 (인증 오류, 속도 제한 사전 확인)
* **캐시된 입력**: v1에서는 정가로 계산됨 (제공자 캐시 할인이 표시되지 않음). v2는 캐시된 프롬프트 토큰을 별도로 크레딧할 수 있습니다.

### 3단계 오버라이드 의미론

`users.token_quota`는 사용자별 관리 오버라이드입니다. 하나의 열에 세 가지 의미를 담고 있습니다:

| 값       | 의미                      | 사용 사례                                 |
| ------- | ----------------------- | ------------------------------------- |
| `NULL`  | 설정되지 않음 — 플랜 / 기본값으로 위임 | 모든 일반 사용자의 기본 상태                      |
| `0`     | 무제한                     | 관리자 / 내부 계정; "VIP 선물"                 |
| `N > 0` | `N`에서 하드 캡              | 유료 구독을 취소하지 않고 악용자 차단; 선결제 엔터프라이즈 할당량 |

오버라이드는 항상 플랜과 기본값을 우선합니다. 관리자가 Stripe를 건드리지 않고도 개별 사용자를 플랜 계층 위 또는 아래에 고정할 수 있도록 존재합니다.

### 할당량 해결 체인 — v1 (현재)

인증된 모든 요청에 대해 상한은 하향식으로 계산됩니다 — **첫 번째 일치가 우선입니다**:

```
1. users.token_quota        ── NULL? skip. 0? unlimited. N>0? cap at N.
2. users.plan.monthly_token_quota   ── only when billing_enabled = TRUE
3. system_settings.default_token_quota  ── defensive fallback
4. unlimited                ── last resort if everything above is NULL
```

청구가 활성화되면 3단계에 거의 도달하지 않습니다. 활성화가 모든 사용자에 대해 `users.plan_id`를 역채우기하기 때문입니다. 잘못 구성된 플랜이 사용자를 자동으로 무제한으로 설정하지 않도록 하는 심층 방어 메커니즘으로 존재합니다.

### 기간 재설정

* 유료 사용자의 경우, `quota_reset_at`은 `Subscription.current_period_end`를 반영합니다. `invoice.payment_succeeded` 웹훅 핸들러는 각 갱신 성공 시 `tokens_used_this_period = 0`으로 설정하고 `quota_reset_at`을 새로운 기간 종료로 진행합니다.
* Free 사용자(Stripe 구독 없음)의 경우, 시간별 크론이 계획 할당 날짜를 기준으로 한 달력 월 경계에서 `tokens_used_this_period`를 0으로 롤링합니다.
* 기간 중 계획 변경은 카운터를 재설정하지 **않습니다** — 갱신만 재설정합니다. 이는 할당량 순환 악용("구독 → Pro 할당량 사용 → 취소 → 다시 구독")을 방지합니다.

### 스트림 중 적용

* 채팅 호출 진입 시 사전 점검: 가장 저렴한 경로로, 사용자가 감당할 수 없는 요청을 차단합니다.
* 스트리밍 중에는 모든 청크에서 실행 중인 토큰 수를 다시 평가합니다. 한도를 초과하면 네트워크 오류가 아닌 **구조화된 종료 프레임**으로 스트림을 닫습니다.
* 프론트엔드는 종료자를 해석하고 `/settings?tab=billing`으로의 딥 링크와 함께 `<QuotaExceededDialog>`를 표시합니다.
* 비스트리밍 응답은 HTTP `402`를 반환하며, 본문은 `{ code: "QUOTA_EXCEEDED", reset_at, upgrade_url }`입니다.

### 청구 비활성화 폴백

`system_settings.billing_enabled = FALSE`일 때:

* 체인의 2단계가 건너뛰어집니다 — 체인이 `override → default → unlimited`로 축소됩니다.
* `/api/billing/*` 및 `/api/webhooks/stripe`는 `503`을 반환합니다.
* `Plan & Billing` 사용자 탭 및 Admin → Billing 네비게이션 그룹이 숨겨집니다.
* 모든 청구 데이터(구독, 플랜, `users.plan_id`)는 보존됩니다 — 다시 활성화하면 마이그레이션 없이 동일한 상태에서 재개됩니다.

### Reserved: quota chain v2 — Team seats

<Note>아직 출시되지 않았습니다. v2 작업이 알려진 착륙 지점을 가지도록 여기에 문서화되어 있습니다.</Note>

Team 플랜이 출시될 때:

* `Subscription.quantity`는 좌석 수를 전달합니다(Stripe 기본).
* 사용자의 유효한 플랜은 Team 멤버십을 통해 해결되며, 그 후 개인 플랜으로 폴백됩니다:
  ```
  effective_plan = team.plan if team_member(user) else user.plan
  ```
* 할당량은 **좌석당**(각 Team 멤버는 전체 `monthly_token_quota`를 받음)이며, 풀링된 버킷이 아닙니다. 풀링된 버킷은 선착순 소진을 만들고 고객 친화적이지 않습니다.
* 재정의 의미는 변경되지 않습니다 — Team 관리자는 여전히 `users.token_quota = N`을 통해 개별 멤버를 하드 캡할 수 있으며, 이는 team 플랜 체인 위에 있습니다.

### Reserved: quota chain v3 — native Org allocation (no Stripe)

<Note>아직 출시되지 않음. Stripe를 통해 사용자당 비용을 지불하지 않고 내부적으로 할당량을 배분하는 온프레미스/엔터프라이즈 배포용으로 예약됨.</Note>

* 새로운 테이블 `org_quota_allocations(user_id, monthly_token_quota, org_id)`는 상위 예산을 멤버 전체에 배분합니다.
* 할당량은 **공유 풀이 아닌 사용자별** — 모든 멤버가 명확한 개별 SLA를 가집니다.
* 업데이트된 체인:
  ```
  override → max(plan_quota, org_allocation) → default → unlimited
  ```
* `max()`, `sum()` 아님. 유료 Pro 사용자는 조직 관리자가 낮은 할당량을 설정하더라도 결코 지불한 것보다 적게 받지 않습니다. Stripe 지불 할당량은 절대 불변입니다.

### 예약됨: 종량제 크레딧 잔액 (v3 별도 차원)

<Note>아직 출시되지 않음. 위의 체인과 별개의 축——크레딧은 구독 티어가 아닌 일회성 충전입니다.</Note>

* 새로운 테이블 `user_credits(user_id, balance_cents, currency)` — Stripe Checkout `mode='payment'`를 통해 자금 조달됨.
* 소비 순서: **구독 할당량 우선**, 그 다음 크레딧 잔액 (구독이 소진된 후에만 크레딧 차감 시작).
* 크레딧 잔액은 환불 불가능 (선불식 업계 표준).
* UI는 두 개의 바를 노출: `Subscription quota: 4.2M / 5M used` + `Credits: $7.40 remaining`.

### 기본값

배포 시 기본값 — 명시된 경우를 제외하고 모두 설치 후 조정 가능합니다.

| 변수                                         | 기본값                           | 조정 방법                                 |
| ------------------------------------------ | ----------------------------- | ------------------------------------- |
| `billing_plans.monthly_token_quota` (Free) | `1,000,000`                   | Admin → Billing → Plans → Free → Edit |
| `billing_plans.monthly_token_quota` (Pro)  | `5,000,000`                   | Admin → Billing → Plans → Pro → Edit  |
| `system_settings.default_token_quota`      | `1,000,000` (활성화 시 Free와 동기화) | Admin → System Settings → Quotas      |
| `system_settings.billing_enabled`          | `FALSE`                       | Admin → System Settings → Billing     |
| Pro 정가                                     | `$20.00 USD / month`          | Stripe Dashboard (price object)       |
| 구독한 Stripe 웹훅 이벤트                          | 6                             | Stripe Dashboard → Webhooks           |
| Stripe 가격 캐시 TTL                           | `5 minutes`                   | `stripe_client.py`에 하드코딩됨             |
| 구독 수명 주기 cron                              | 매시간                           | `web/main.py`의 APScheduler            |
| Free 티어 리셋 cron                            | 매시간 (달력 월 경계)                 | `web/main.py`의 APScheduler            |

## 가격 책정 모델

V1은 정액 구독입니다. Free + Pro, 월간 청구, USD만 지원합니다.

**V1 범위 외** (의도적으로 로드맵으로 연기됨):

* Team 플랜 (Stripe seats / `subscription.quantity`)
* 연간 청구
* 다중 통화 표시
* 쿠폰 / 프로모션 코드
* 세금 처리 (Stripe Tax 통합 — 별도 규정 검토 필요)
* 사용량 기반 계량 / 초과 요금
* 선불 크레딧 잔액 (일회성 충전)

다음 계획된 사항은 [로드맵](/roadmap)을 참조하세요.

## 문제 해결

<Accordion title="토글이 활성화되지 않음 — 400 오류">
  활성화 엔드포인트에는 `STRIPE_SECRET_KEY`와 `STRIPE_WEBHOOK_SECRET`이 모두 설정되어야 합니다. `.env`에 존재하는지 확인하고 편집 후 백엔드가 다시 시작되었는지 확인하세요.
</Accordion>

<Accordion title="웹훅이 503을 반환함">
  청구가 비활성화되어 있거나(토글이 OFF) 요청 서명 검증에 실패했습니다(`STRIPE_WEBHOOK_SECRET` 불일치). Stripe 대시보드 → 웹훅 → 최근 이벤트에서 실제 오류 본문을 확인하세요.
</Accordion>

<Accordion title="사용자가 구독했지만 여전히 무료 요금제를 봄">
  `checkout.session.completed` 웹훅이 백엔드에 도달하지 않았습니다. Stripe 대시보드의 엔드포인트 URL이 `<your-domain>/api/webhooks/stripe`와 정확히 일치하는지(후행 경로 포함) 확인하세요. 웹훅 최근 전달 기록에서 실패를 확인하세요.
</Accordion>

<Accordion title="Pro 사용자가 테스트 모드 가격 ID를 봄">
  시드 마이그레이션이 테스트 모드 가격 ID를 작성합니다. 프로덕션 청구를 활성화한 후 관리자 → 청구 → 요금제 → Pro → 편집을 통해 Pro 요금제를 라이브 `price_1***`을 사용하도록 업데이트하거나 직접 SQL UPDATE를 실행하세요.
</Accordion>

<Accordion title="영수증이 Stripe 기본값으로 브랜딩됨">
  Stripe 대시보드 → 설정 → 브랜딩에서 비즈니스 브랜딩을 구성하세요. 로고, 비즈니스 이름(예: "FIM Labs Pte. Ltd."), 주소를 추가하세요. Stripe는 이를 모든 자동 생성 영수증 및 청구서에 적용합니다.
</Accordion>

<Accordion title="지급 시 환율 손실">
  Stripe 계정 기본 통화가 청구 통화와 다르면 Stripe는 매번 지급할 때 환전합니다(1.5-2% 스프레드). 설정 → 은행 계좌 및 통화에서 일치하는 결제 통화를 추가하고 동일 통화 은행 계좌를 연결하면 Stripe가 환전 없이 동일 통화 결제를 라우팅합니다.
</Accordion>
