> 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-en/common/monetization/iap/in-app-subscription.md).

# IAP subscriptions

It is used for automatically renewing subscription products. It is billed automatically at set intervals and can be used until canceled. Service introduction and console setup instructions are [In-app Purchase introduction document](https://developers-apps-in-toss.toss.im/guide/monetization/in-app-payment)please refer to.

`getProductItemList`where it explains how subscription products are retrieved and how to create subscription orders `createSubscriptionPurchaseOrder`It also covers how to receive webhooks on the server when subscription status changes, such as renewals and cancellations.

{% hint style="info" %}
**The current sandbox app does not support testing subscription features.**

Support is planned for a future release.
{% endhint %}

Follow the sequence below for the integration flow.

1. [Fetch subscription product list](#getproductitemlist) — `getProductItemList`
2. [Create subscription order](#createsubscriptionpurchaseorder) — `createSubscriptionPurchaseOrder`
3. [Check subscription status](#getsubscriptioninfo) — `getSubscriptionInfo`
4. [Receive subscription status changes via webhook](#webhook) — server callback
5. [Restore purchases](#purchase-recovery) — `getPendingOrders`, `completeProductGrant`

***

### In-app purchase object

**SDK object:** `IAP`

The existing IAP object has the following features added/expanded.

**Signature**

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

`createSubscriptionPurchaseOrder`is a subscription-only order creation function. It is similar to the existing one-time order flow, but it handles subscription-only parameters (such as offerId and exposure of renewalCycle). The returned cleanup function is used to release App Bridge resources, as before.

### View product list <a href="#getproductitemlist" id="getproductitemlist"></a>

**SDK function:** `getProductItemList`

`getProductItemList()`can now return product lists that include subscription products (type: 'SUBSCRIPTION'). Subscription products have additional fields.

**Signature**

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

**Return value**

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

  It returns an object containing the product list. If the app version is lower than the minimum supported version (Android `5.248.0`, iOS `5.250.0`) is lower than the minimum supported version, `undefined`is returned.

**Properties**

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

/** subscription-only extended return **/
interface IapSubscriptionProduct extends IapProductListItemBase {
  type: 'SUBSCRIPTION';
  renewalCycle: 'WEEKLY' | 'MONTHLY' | 'YEARLY';
  offers?: Offer[];
}

/** subscription Offer type */
type Offer = FreeTrial | NewSubscription | Returning;

// 1. Free trial
interface FreeTrial {
  type: 'FREE_TRIAL';
  offerId: string;
  period: string;
}

// 2. New subscription user
interface NewSubscription {
  type: 'NEW_SUBSCRIPTION';
  offerId: string;
  period: string;
  displayAmount: string;
}

// 3. Returning user
interface Returning {
  type: 'RETURNING';
  offerId: string;
  period: string;
  displayAmount: string;
}
```

| Field         | Type   | Description                               |
| ------------- | ------ | ----------------------------------------- |
| type          | string | Product type                              |
| sku           | string | Unique ID of the product                  |
| displayAmount | string | Price information including currency unit |
| displayName   | string | Product name displayed on screen          |
| iconUrl       | string | URL of the product icon image             |
| description   | string | Product description                       |

**Product type classification**

`getProductItemList`can return the following three product types.

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

The meaning of each type is as follows.

**1. Consumable product (CONSUMABLE)**

A product that disappears after one use. Example: coins, currency, hearts, etc.

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

* You can repurchase it multiple times after purchase.
* After successful payment, the server must grant the product and call completeProductGrant.
* There is no auto-renewal concept.

**2. Non-consumable product (NON\_CONSUMABLE)**

A product you own permanently after one purchase. Example: remove ads, permanent upgrade

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

* It is not repurchased for the same account.
* A restore flow may be needed when changing devices.
* It does not auto-renew.

**3. Subscription product (SUBSCRIPTION)**

A product that automatically renews on a regular cycle. Example: monthly/annual membership

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

| Field        | Type     | Description                                         |
| ------------ | -------- | --------------------------------------------------- |
| renewalCycle | string   | Subscription renewal cycle                          |
| offers       | Offer\[] | List of subscription benefits the user can receive. |

* It renews automatically.
* It can have offers such as free trials, new user discounts, and returning user discounts.
* Orders must be created with createSubscriptionPurchaseOrder.
* Server-side synchronization of subscription status (renewal/cancellation/refund processing) is required.

**Order creation function summary by type**

| Type             | Order creation function           |
| ---------------- | --------------------------------- |
| `CONSUMABLE`     | `createOneTimePurchaseOrder`      |
| `NON_CONSUMABLE` | `createOneTimePurchaseOrder`      |
| `SUBSCRIPTION`   | `createSubscriptionPurchaseOrder` |

***

### Create subscription order <a href="#createsubscriptionpurchaseorder" id="createsubscriptionpurchaseorder"></a>

**SDK function:** `createSubscriptionPurchaseOrder`

A function that creates subscription-only orders and takes the user to the subscription payment page. It can be used when the user presses a subscription product purchase button.

**Signature**

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

**Properties**

```tsx
interface CreateSubscriptionPurchaseOrderOptions {
  options: {
    sku: string; // required: subscription SKU to purchase
    offerId?: string | null; // optional: offer ID to apply (default price if omitted)
    processProductGrant: (params: { orderId: string; subscriptionId?: string }) => boolean | Promise<boolean>;
  };
  onEvent: (event: SubscriptionSuccessEvent) => void | Promise<void>;
  onError: (error: unknown) => void | Promise<void>;
}
```

**Example usage**

```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 }) => {
          // Write the product grant logic
          console.log(orderId, subscriptionId);
          return true; // whether the product is granted
        },
      },
      onEvent: (event) => {
        console.log(event);
        cleanup();
      },
      onError: (error) => {
        console.error(error);
        cleanup();
      },
    });
  }, [sku, offerId]);

  return <button onClick={handleClick}>Subscribe</button>;
}
```

***

### Check subscription status <a href="#getsubscriptioninfo" id="getsubscriptioninfo"></a>

**SDK function:** `getSubscriptionInfo`

A function that gets the current status information of a subscription order.

{% hint style="info" %}
**Minimum supported version**

* The Toss app minimum supported version is Android `5.253.0`, iOS `5.250.0` or later. Below that version, `undefined`may be returned.
  {% endhint %}

**Signature**

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

**Parameters**

* params object

  An object containing the subscription order information to look up.

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

    The unique ID of the order.

**Return value**

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

  Returns an object containing subscription status information. If the app version is lower than the minimum supported version (Android `5.253.0`, iOS `5.250.0`) is lower than the minimum supported version, `undefined`is returned.

**Properties**

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

| Field                | Type                                                                               | Description                                                                                       |
| -------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| catalogId            | number                                                                             | The identifier of the subscription product.                                                       |
| status               | `'ACTIVE' \| 'EXPIRED' \| 'IN_GRACE_PERIOD' \| 'ON_HOLD' \| 'PAUSED' \| 'REVOKED'` | A value indicating the subscription status.                                                       |
| expiresAt            | string \| null                                                                     | The scheduled expiration time of the subscription. If there is no expiration information, `null`. |
| isAutoRenew          | boolean                                                                            | Whether the subscription auto-renews.                                                             |
| gracePeriodExpiresAt | string \| null                                                                     | The expiration time of the payment grace period. If there is no grace period, `null`.             |
| isAccessible         | boolean                                                                            | Whether the current subscription product can be used.                                             |

**Example usage**

{% 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 %}

***

### Receive subscription status changes via webhook <a href="#webhook" id="webhook"></a>

When the subscription status changes, such as renewal, cancellation, or suspension, a webhook event is sent to the server. You can receive the event by registering a callback URL in the console.

* Time values (`occurredAt`, `expiresAt` etc.) are ISO-8601 strings without a timezone. Example: `"2026-05-06T00:00:00"`
* `orderId`is not a direct user identifier, but if you are mapping orders to users, it can be used as a correlation identifier.

**Event type**

| `eventType`                          | Description                                      |
| ------------------------------------ | ------------------------------------------------ |
| `callback.registration_verification` | Sent when registering or changing a callback URL |
| `subscription.status_changed`        | Sent when subscription status changes            |

***

**`callback.registration_verification`**

It is sent when a callback URL is registered or changed. The callback URL is activated only after this event is received successfully.

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

***

**`subscription.status_changed`**

Sent after the subscription status has been finalized.

```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`like when there is no previous state `subscription.previous`may be omitted.

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

**Field**

| Field                   | Type    | Description                                                                  |
| ----------------------- | ------- | ---------------------------------------------------------------------------- |
| `eventType`             | string  | Fixed values: `subscription.status_changed`                                  |
| `eventVersion`          | string  | Fixed values: `1.0`                                                          |
| `occurredAt`            | string  | The time when the notification occurred                                      |
| `orderId`               | string  | Order identifier                                                             |
| `sku`                   | string  | Product SKU                                                                  |
| `changeReason`          | string  | Reason for the subscription status change                                    |
| `subscription.previous` | object? | The subscription status before the change. May be omitted in creation events |
| `subscription.current`  | object  | The subscription status after the change                                     |

**Snapshot fields**

`subscription.previous`and `subscription.current`has the same structure.

| Field           | Type           | Description                                    |
| --------------- | -------------- | ---------------------------------------------- |
| `status`        | string         | Subscription status                            |
| `accessGranted` | boolean        | Whether access is currently granted            |
| `expiresAt`     | string \| null | Subscription expiration time. It may be absent |
| `autoRenew`     | boolean        | Whether auto-renewal is enabled                |

**`changeReason` Value**

| Value                  | Meaning                          |
| ---------------------- | -------------------------------- |
| `CREATED`              | Subscription created             |
| `RENEWED`              | Subscription renewed             |
| `RECOVERED`            | Recovered from payment failure   |
| `RESTARTED`            | Subscription restarted           |
| `ENTERED_GRACE_PERIOD` | Entered grace period             |
| `ON_HOLD`              | Payment on hold                  |
| `PAUSED`               | Subscription paused              |
| `AUTO_RENEW_ENABLED`   | Auto-renewal enabled             |
| `AUTO_RENEW_DISABLED`  | Auto-renewal disabled            |
| `EXTENDED`             | Subscription period extended     |
| `EXPIRED`              | Subscription expired             |
| `REVOKED`              | Subscription revoked or refunded |

**`status` Value**

| Value             | Meaning      |
| ----------------- | ------------ |
| `ACTIVE`          | Active       |
| `EXPIRED`         | Expired      |
| `IN_GRACE_PERIOD` | Grace period |
| `ON_HOLD`         | On hold      |
| `PAUSED`          | Paused       |
| `REVOKED`         | Revoked      |

***

### Restore purchases <a href="#purchase-recovery" id="purchase-recovery"></a>

Even if payment is completed, product granting may fail due to network or server errors. If a granting error occurs, please be sure to add purchase recovery logic so users can receive the product properly.

{% hint style="info" %}
**Recommended flow**

Without purchase recovery logic, payment may complete but the user may not receive the subscription benefits. At app initialization, `getPendingOrders`It is recommended to call to process pending orders.
{% endhint %}

**Recovery flow**

1. `getPendingOrders` — Check the list of subscription orders that have been paid for but not yet granted
2. Grant product — actually grant the subscription product on the server
3. `completeProductGrant` — Mark grant as complete

**Example usage**

{% 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) {
    // Request subscription product grant from the server
    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-en/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.
