> For the complete documentation index, see [llms.txt](https://developers-apps-in-toss.toss.im/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers-apps-in-toss.toss.im/documentation/common/monetization/iap/in-app-subscription.md).

# IAP 정기결제

자동 갱신 구독 상품에 사용해요. 정해진 주기마다 자동으로 결제되며, 취소 전까지 계속 이용할 수 있어요. 서비스 소개와 콘솔 설정 방법은 인앱 결제 소개 문서를 참고해 주세요.

`getProductItemList`에서 구독 상품이 어떻게 내려오는지와 구독 주문을 생성하는 `createSubscriptionPurchaseOrder`의 사용법을 포함해요. 갱신·해지 등 구독 상태 변경 시 서버로 웹훅을 받는 방법도 안내해요.

{% hint style="info" %}
**현재 샌드박스 앱에서는 구독 기능 테스트를 지원하지 않아요.**

추후 지원 예정이에요.
{% endhint %}

연동 흐름은 아래 순서를 따라 주세요.

1. [구독 상품 목록 가져오기](#getproductitemlist) — `getProductItemList`
2. [구독 주문 생성하기](#createsubscriptionpurchaseorder) — `createSubscriptionPurchaseOrder`
3. [구독 상태 조회하기](#getsubscriptioninfo) — `getSubscriptionInfo`
4. [웹훅으로 구독 상태 변경 받기](#webhook) — 서버 콜백
5. [구매 복구하기](#purchase-recovery) — `getPendingOrders`, `completeProductGrant`

***

### 인앱 결제 객체

**SDK 객체:** `IAP`

기존 IAP 객체에 다음 기능이 추가/확장되었어요.

**시그니처**

```tsx
IAP {
  getProductItemList: typeof getProductItemList;
  createOneTimePurchaseOrder: typeof createOneTimePurchaseOrder;
  createSubscriptionPurchaseOrder: typeof createSubscriptionPurchaseOrder;
  getSubscriptionInfo: typeof getSubscriptionInfo;
  getPendingOrders: typeof getPendingOrders;
  getCompletedOrRefundedOrders: typeof getCompletedOrRefundedOrders;
  completeProductGrant: typeof completeProductGrant;
}
```

`createSubscriptionPurchaseOrder`는 구독 전용 주문 생성 함수로, 기존 일회성 주문 흐름과 유사하지만 구독 전용 파라미터(offerId, renewalCycle 노출 등)를 처리합니다. 반환되는 cleanup 함수는 기존과 동일하게 앱브릿지 리소스 해제용입니다.

### 상품 목록 조회하기 <a href="#getproductitemlist" id="getproductitemlist"></a>

**SDK 함수:** `getProductItemList`

`getProductItemList()`는 이제 구독 상품(type: 'SUBSCRIPTION')을 포함한 상품 목록을 반환할 수 있어요. 구독 상품은 추가 필드를 가져요.

**시그니처**

```tsx
function getProductItemList(): Promise<{ products: IapProductListItem[] } | undefined>;
```

**반환값**

* `Promise<{ products: IapProductListItem\[] } | undefined>`

  상품 목록을 포함한 객체를 반환해요. 앱 버전이 최소 지원 버전(Android `5.248.0`, iOS `5.250.0`)보다 낮으면 `undefined`를 반환해요.

**프로퍼티**

```tsx
/** 기본 반환 **/
interface IapProductListItemBase {
  type: 'CONSUMABLE' | 'NON_CONSUMABLE' | 'SUBSCRIPTION';
  sku: string;
  displayAmount: string;
  displayName: string;
  iconUrl: string;
  description: string;
  hint?: Record<string, string>;
}

/** 구독 전용 확장 반환 **/
interface IapSubscriptionProduct extends IapProductListItemBase {
  type: 'SUBSCRIPTION';
  renewalCycle: 'WEEKLY' | 'MONTHLY' | 'YEARLY';
  offers?: Offer[];
}

/** 구독 Offer 타입 */
type Offer = FreeTrial | NewSubscription | Returning;

// 1. 무료 체험
interface FreeTrial {
  type: 'FREE_TRIAL';
  offerId: string;
  period: string;
}

// 2. 신규 구독 사용자
interface NewSubscription {
  type: 'NEW_SUBSCRIPTION';
  offerId: string;
  period: string;
  displayAmount: string;
}

// 3. 복귀 사용자
interface Returning {
  type: 'RETURNING';
  offerId: string;
  period: string;
  displayAmount: string;
}
```

| 필드            | 타입     | 설명                 |
| ------------- | ------ | ------------------ |
| type          | string | 상품 유형이에요           |
| sku           | string | 상품의 고유 ID예요        |
| displayAmount | string | 통화 단위가 포함된 가격 정보예요 |
| displayName   | string | 화면에 표시할 상품 이름이에요   |
| iconUrl       | string | 상품 아이콘 이미지 URL이에요  |
| description   | string | 상품 설명이에요           |

**상품 타입 구분**

`getProductItemList`는 다음 세 가지 상품 타입을 반환할 수 있어요.

```tsx
type IapProductType = 'CONSUMABLE' | 'NON_CONSUMABLE' | 'SUBSCRIPTION';
```

각 타입의 의미는 다음과 같아요.

**1. 소모성 상품 (CONSUMABLE)**

한 번 사용하면 소멸되는 상품이에요. 예: 코인, 재화, 하트 등

```json
{
  type: 'CONSUMABLE';
  sku: string;
  displayAmount: string;
  displayName: string;
  iconUrl: string;
  description: string;
  hint?: Record<string, string>;
}
```

* 구매 후 여러 번 재구매할 수 있어요.
* 결제 성공 후 서버에서 상품을 지급하고 completeProductGrant를 호출해야 해요.
* 자동 갱신 개념은 없어요.

**2. 비소모성 상품 (NON\_CONSUMABLE)**

한 번 구매하면 영구적으로 소유하는 상품이에요. 예: 광고 제거, 영구 업그레이드

```json
{
  type: 'NON_CONSUMABLE';
  sku: string;
  displayAmount: string;
  displayName: string;
  iconUrl: string;
  description: string;
  hint?: Record<string, string>;
}
```

* 동일 계정에서는 재구매하지 않아요.
* 기기 변경 시 복원 로직이 필요할 수 있어요.
* 자동 갱신되지 않아요.

**3. 구독 상품 (SUBSCRIPTION)**

일정 주기로 자동 갱신되는 상품이에요. 예: 월간/연간 멤버십

```json
{
  type: 'SUBSCRIPTION';
  sku: string;
  displayAmount: string;
  displayName: string;
  iconUrl: string;
  description: string;
  hint?: Record<string, string>;
  renewalCycle: 'WEEKLY' | 'MONTHLY' | 'YEARLY';
  offers?: Offer[];
}
```

| 필드           | 타입       | 설명                        |
| ------------ | -------- | ------------------------- |
| renewalCycle | string   | 구독 갱신 주기예요                |
| offers       | Offer\[] | 사용자가 받을 수 있는 구독 혜택 목록이에요. |

* 자동 갱신돼요.
* 무료 체험, 신규 할인, 복귀 할인 등의 offers를 가질 수 있어요.
* 주문은 createSubscriptionPurchaseOrder로 생성해야 해요.
* 서버에서 구독 상태 동기화(갱신/취소/환불 처리)가 필요해요.

**타입별 주문 생성 함수 정리**

| 타입               | 주문 생성 함수                          |
| ---------------- | --------------------------------- |
| `CONSUMABLE`     | `createOneTimePurchaseOrder`      |
| `NON_CONSUMABLE` | `createOneTimePurchaseOrder`      |
| `SUBSCRIPTION`   | `createSubscriptionPurchaseOrder` |

***

### 구독 주문 생성하기 <a href="#createsubscriptionpurchaseorder" id="createsubscriptionpurchaseorder"></a>

**SDK 함수:** `createSubscriptionPurchaseOrder`

구독 상품 전용 주문을 생성하고, 구독 결제 페이지로 이동하는 함수예요. 사용자가 구독 상품 구매 버튼을 누르는 상황에서 사용할 수 있어요.

**시그니처**

```tsx
function createSubscriptionPurchaseOrder(params: CreateSubscriptionPurchaseOrderOptions): () => void;
```

**프로퍼티**

```tsx
interface CreateSubscriptionPurchaseOrderOptions {
  options: {
    sku: string; // 필수: 구매할 구독 SKU
    offerId?: string | null; // 선택: 적용할 offer ID (없으면 기본 가격)
    processProductGrant: (params: { orderId: string; subscriptionId?: string }) => boolean | Promise<boolean>;
  };
  onEvent: (event: SubscriptionSuccessEvent) => void | Promise<void>;
  onError: (error: unknown) => void | Promise<void>;
}
```

**사용 예시**

```tsx
import { IAP } from '@apps-in-toss/web-framework';
import { useCallback } from 'react';

interface Props {
  sku: string;
  offerId?: string;
}

function SubscriptionPurchaseButton({ sku, offerId }: Props) {
  const handleClick = useCallback(async () => {
    const cleanup = IAP.createSubscriptionPurchaseOrder({
      options: {
        sku,
        offerId,
        processProductGrant: ({ orderId, subscriptionId }) => {
          // 상품 지급 로직 작성
          console.log(orderId, subscriptionId);
          return true; // 상품 지급 여부
        },
      },
      onEvent: (event) => {
        console.log(event);
        cleanup();
      },
      onError: (error) => {
        console.error(error);
        cleanup();
      },
    });
  }, [sku, offerId]);

  return <button onClick={handleClick}>구독하기</button>;
}
```

***

### 구독 상태 조회하기 <a href="#getsubscriptioninfo" id="getsubscriptioninfo"></a>

**SDK 함수:** `getSubscriptionInfo`

구독 주문의 현재 상태 정보를 가져오는 함수예요.

{% hint style="info" %}
**최소 지원 버전**

* 토스앱 최소 지원 버전은 안드로이드 `5.253.0`, iOS `5.250.0` 이상 이에요. 해당 버전 미만에서는 `undefined`를 반환할 수 있어요.
  {% endhint %}

**시그니처**

```tsx
function getSubscriptionInfo(params: {
  params: { orderId: string };
}): Promise<{ subscription: IapSubscriptionInfoResult } | undefined>;
```

**파라미터**

* params object

  조회할 구독 주문 정보를 담은 객체예요.

  * **params.orderId**
  * `string`

    주문의 고유 ID예요.

**반환값**

* `Promise<{ subscription: IapSubscriptionInfoResult } | undefined>`

  구독 상태 정보를 담은 객체를 반환해요. 앱 버전이 최소 지원 버전(안드로이드 `5.253.0`, iOS `5.250.0`)보다 낮으면 `undefined`를 반환해요.

**프로퍼티**

```tsx
interface IapSubscriptionInfoResult {
  catalogId: number;
  status: 'ACTIVE' | 'EXPIRED' | 'IN_GRACE_PERIOD' | 'ON_HOLD' | 'PAUSED' | 'REVOKED';
  expiresAt: string | null;
  isAutoRenew: boolean;
  gracePeriodExpiresAt: string | null;
  isAccessible: boolean;
}
```

| 필드                   | 타입                                                                                 | 설명                                       |
| -------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------- |
| catalogId            | number                                                                             | 구독 상품의 식별자예요.                            |
| status               | `'ACTIVE' \| 'EXPIRED' \| 'IN_GRACE_PERIOD' \| 'ON_HOLD' \| 'PAUSED' \| 'REVOKED'` | 구독 상태를 나타내는 값이에요.                        |
| expiresAt            | string \| null                                                                     | 구독 만료 예정 시각이에요. 만료 정보가 없으면 `null`이에요.    |
| isAutoRenew          | boolean                                                                            | 구독 자동 갱신 여부예요.                           |
| gracePeriodExpiresAt | string \| null                                                                     | 결제 유예 기간 만료 시각이에요. 유예 기간이 없으면 `null`이에요. |
| isAccessible         | boolean                                                                            | 현재 구독 상품을 이용할 수 있는지 여부예요.                |

**사용 예시**

{% tabs %}
{% tab title="React" %}

```tsx
import { IAP } from '@apps-in-toss/web-framework';

async function fetchSubscriptionInfo(orderId: string) {
  try {
    const response = await IAP.getSubscriptionInfo({ params: { orderId } });
    return response?.subscription;
  } catch (error) {
    console.error(error);
  }
}
```

{% endtab %}

{% tab title="React Native" %}

```tsx
import { IAP } from '@apps-in-toss/framework';

async function fetchSubscriptionInfo(orderId: string) {
  try {
    const response = await IAP.getSubscriptionInfo({ params: { orderId } });
    return response?.subscription;
  } catch (error) {
    console.error(error);
  }
}
```

{% endtab %}
{% endtabs %}

***

### 웹훅으로 구독 상태 변경 받기 <a href="#webhook" id="webhook"></a>

구독 갱신, 해지, 일시정지 등 구독 상태가 변경되면 서버로 웹훅 이벤트가 발송돼요. 콘솔에서 콜백 URL을 등록하면 이벤트를 수신할 수 있어요.

* 시간 값(`occurredAt`, `expiresAt` 등)은 timezone 없는 ISO-8601 문자열이에요. 예: `"2026-05-06T00:00:00"`
* `orderId`는 직접 사용자 식별자는 아니지만, 주문과 사용자를 매핑하고 있다면 상관관계 식별자로 활용할 수 있어요.

**이벤트 종류**

| `eventType`                          | 설명                |
| ------------------------------------ | ----------------- |
| `callback.registration_verification` | 콜백 URL 등록·변경 시 발송 |
| `subscription.status_changed`        | 구독 상태 변경 시 발송     |

***

**`callback.registration_verification`**

콜백 URL을 등록하거나 변경하면 발송돼요. 이 이벤트를 정상 수신해야 콜백 URL이 활성화돼요.

```json
{
  "eventType": "callback.registration_verification",
  "occurredAt": "2026-05-06T00:00:00"
}
```

***

**`subscription.status_changed`**

구독 상태가 확정된 뒤 발송돼요.

```json
{
  "eventType": "subscription.status_changed",
  "eventVersion": "1.0",
  "occurredAt": "2026-05-06T00:00:00",
  "orderId": "order-1",
  "sku": "premium.monthly",
  "changeReason": "RENEWED",
  "subscription": {
    "previous": {
      "status": "ACTIVE",
      "accessGranted": true,
      "expiresAt": "2026-05-06T00:00:00",
      "autoRenew": true
    },
    "current": {
      "status": "ACTIVE",
      "accessGranted": true,
      "expiresAt": "2026-06-06T00:00:00",
      "autoRenew": true
    }
  }
}
```

`CREATED`처럼 이전 상태가 없는 경우 `subscription.previous`가 생략될 수 있어요.

```json
{
  "eventType": "subscription.status_changed",
  "eventVersion": "1.0",
  "occurredAt": "2026-05-06T00:00:00",
  "orderId": "order-1",
  "sku": "premium.monthly",
  "changeReason": "CREATED",
  "subscription": {
    "current": {
      "status": "ACTIVE",
      "accessGranted": true,
      "expiresAt": null,
      "autoRenew": true
    }
  }
}
```

**필드**

| 필드                      | 타입      | 설명                                 |
| ----------------------- | ------- | ---------------------------------- |
| `eventType`             | string  | 고정값: `subscription.status_changed` |
| `eventVersion`          | string  | 고정값: `1.0`                         |
| `occurredAt`            | string  | 통지 발생 시각이에요                        |
| `orderId`               | string  | 주문 식별자예요                           |
| `sku`                   | string  | 상품 SKU예요                           |
| `changeReason`          | string  | 구독 상태 변경 사유예요                      |
| `subscription.previous` | object? | 변경 전 구독 상태예요. 생성 이벤트에서는 생략될 수 있어요  |
| `subscription.current`  | object  | 변경 후 구독 상태예요                       |

**Snapshot 필드**

`subscription.previous`와 `subscription.current`는 동일한 구조예요.

| 필드              | 타입             | 설명                    |
| --------------- | -------------- | --------------------- |
| `status`        | string         | 구독 상태예요               |
| `accessGranted` | boolean        | 현재 접근 권한 부여 여부예요      |
| `expiresAt`     | string \| null | 구독 만료 시각이에요. 없을 수 있어요 |
| `autoRenew`     | boolean        | 자동 갱신 여부예요            |

**`changeReason` 값**

| 값                      | 의미             |
| ---------------------- | -------------- |
| `CREATED`              | 구독 생성          |
| `RENEWED`              | 구독 갱신          |
| `RECOVERED`            | 결제 실패 상태에서 복구  |
| `RESTARTED`            | 구독 재시작         |
| `ENTERED_GRACE_PERIOD` | 유예 기간 진입       |
| `ON_HOLD`              | 결제 보류          |
| `PAUSED`               | 구독 일시정지        |
| `AUTO_RENEW_ENABLED`   | 자동 갱신 활성화      |
| `AUTO_RENEW_DISABLED`  | 자동 갱신 비활성화     |
| `EXTENDED`             | 구독 기간 연장       |
| `EXPIRED`              | 구독 만료          |
| `REVOKED`              | 구독 회수 또는 환불 처리 |

**`status` 값**

| 값                 | 의미    |
| ----------------- | ----- |
| `ACTIVE`          | 활성    |
| `EXPIRED`         | 만료    |
| `IN_GRACE_PERIOD` | 유예 기간 |
| `ON_HOLD`         | 보류    |
| `PAUSED`          | 일시정지  |
| `REVOKED`         | 회수됨   |

***

### 구매 복구하기 <a href="#purchase-recovery" id="purchase-recovery"></a>

결제가 완료되었더라도 네트워크 오류나 서버 오류로 상품 지급이 실패할 수 있어요. 지급 오류 발생 시 사용자가 상품을 정상적으로 받을 수 있도록 구매 복구 로직을 반드시 추가해 주세요.

{% hint style="info" %}
**권장 흐름**

구매 복구 로직이 없으면, 결제는 완료되었지만 사용자가 구독 혜택을 받지 못하는 상황이 발생할 수 있어요. 앱 초기화 시점에 `getPendingOrders`를 호출해 미결 주문을 처리하는 것을 권장해요.
{% endhint %}

**복구 흐름**

1. `getPendingOrders` — 결제는 완료되었지만 아직 지급되지 않은 구독 주문 목록 조회
2. 상품 지급 처리 — 서버에서 실제 구독 상품 지급
3. `completeProductGrant` — 지급 완료 처리

**사용 예시**

{% tabs %}
{% tab title="React" %}

```tsx
import { IAP } from '@apps-in-toss/web-framework';

async function recoverPendingOrders() {
  const result = await IAP.getPendingOrders();

  if (!result?.orders?.length) return;

  for (const order of result.orders) {
    // 서버에 구독 상품 지급 요청
    const granted = await grantSubscriptionProduct(order.orderId);

    if (granted) {
      await IAP.completeProductGrant({ params: { orderId: order.orderId } });
    }
  }
}
```

{% endtab %}

{% tab title="React Native" %}

```tsx
import { IAP } from '@apps-in-toss/framework';

async function recoverPendingOrders() {
  const result = await IAP.getPendingOrders();

  if (!result?.orders?.length) return;

  for (const order of result.orders) {
    const granted = await grantSubscriptionProduct(order.orderId);

    if (granted) {
      await IAP.completeProductGrant({ params: { orderId: order.orderId } });
    }
  }
}
```

{% endtab %}
{% endtabs %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developers-apps-in-toss.toss.im/documentation/common/monetization/iap/in-app-subscription.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
