> 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;
}
```

| 字段            | 类型     | 说明            |
| ------------- | ------ | ------------- |
| 类型            | 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`  | 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/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.
