> 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/api-and-sdk-zh/common/monetization/iap/in-app-subscription.md).

# IAP 定期订阅

用于自动续费订阅商品。会按固定周期自动扣费，可持续使用直到取消。服务介绍和控制台设置方法是 [应用内支付介绍文档](https://developers-apps-in-toss.toss.im/guide/monetization/in-app-payment)。

`getProductItemList`，其中包含订阅商品如何下架，以及创建订阅订单的 `createSubscriptionPurchaseOrder`的使用方法。也会说明在续订、解约等订阅状态变更时，如何通过 Webhook 接收来自服务器的通知。

{% hint style="info" %}
**当前沙盒应用不支持订阅功能测试。**

计划后续支持。
{% endhint %}

请按以下顺序进行对接流程。

1. [获取订阅商品列表](#getproductitemlist) — `getProductItemList`
2. [创建订阅订单](#createsubscriptionpurchaseorder) — `createSubscriptionPurchaseOrder`
3. [查询订阅状态](#getsubscriptioninfo) — `getSubscriptionInfo`
4. [通过 Webhook 接收订阅状态变更](#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 函数与以往相同，用于释放 App Bridge 资源。

### 查看商品列表 <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" %}
**最低支持版本**

* Toss App 最低支持版本为 Android `5.253.0`, iOS `5.250.0` 及以上。低于该版本时， `undefined`可以返回。
  {% endhint %}

**签名**

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

**参数**

* params 对象

  包含要查询的订阅订单信息的对象。

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

    订单的唯一 ID。

**返回值**

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

  返回包含订阅状态信息的对象。应用版本低于最低支持版本（Android `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 %}

***

### 通过 Webhook 接收订阅状态变更 <a href="#webhook" id="webhook"></a>

当订阅续订、解约、暂停等订阅状态发生变化时，Webhook 事件会发送到服务器。在控制台注册回调 URL 后即可接收事件。

* 时间值（`occurredAt`, `expiresAt` 等）是没有时区的 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`  | 对象      | 变更后的订阅状态。                          |

**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` | 过期  |
| `宽限期`     | 挂起  |
| `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/api-and-sdk-zh/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.
