> 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

Used for auto-renewing subscription products. It is billed automatically at a fixed interval and can be used continuously until canceled. For an introduction to the service and how to configure the console, [In-app Purchase Introduction](https://developers-apps-in-toss.toss.im/guide/monetization/in-app-payment).

`getProductItemList`explains how subscription products are delivered and how to create subscription orders, `createSubscriptionPurchaseOrder`It also explains 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 will be added later.
{% endhint %}

Please follow the sequence below for the integration flow.

1. [Get 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 following features have been added to or extended in the existing IAP object.

**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-specific parameters (such as offerId and exposing renewalCycle). The returned cleanup function is, as before, for releasing app bridge resources.

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

**SDK function:** `getProductItemList`

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

**Signature**

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

**Return value**

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

  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`), `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 the currency unit |
| displayName   | string | Product name to display on screen             |
| iconUrl       | string | Product icon image URL                        |
| 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 payment succeeds, the server must grant the product and call completeProductGrant.
* There is no concept of auto-renewal.

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

A product that you own permanently after one purchase. Example: ad removal, permanent upgrade

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

* It will not be repurchased on the same account.
* You may need restore logic when changing devices.
* It does not auto-renew.

**3. Subscription product (SUBSCRIPTION)**

A product that automatically renews on a regular cycle. Example: monthly/yearly 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   | The subscription renewal cycle                        |
| offers       | Offer\[] | A 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 subscription status synchronization is required (renewal/cancellation/refund handling).

**Summary of order creation functions 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`

This function creates an order for subscription products and navigates to the subscription payment page. It can be used when the user taps the purchase button for a subscription product.

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

**Usage example**

```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 product grant logic
          console.log(orderId, subscriptionId);
          return true; // whether the product was 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`

This function retrieves the current status information for 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. On versions below that, `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 query.

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

    The order's unique ID.

**Return value**

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

  Returns an object containing the subscription status information. If the app version is lower than the minimum supported version (Android `5.253.0`, iOS `5.250.0`), `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 expected 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 is accessible.                                          |

**Usage example**

{% 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, webhook events are sent to the server. You can receive the events 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 map orders to users, it can be used as a correlation identifier.

**Event type**

| `eventType`                          | Description                                       |
| ------------------------------------ | ------------------------------------------------- |
| `callback.registration_verification` | Sent when a callback URL is registered or changed |
| `subscription.status_changed`        | Sent when the subscription status changes         |

***

**`callback.registration_verification`**

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 is 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`When there is no previous state, such as `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 value: `subscription.status_changed`                                   |
| `eventVersion`          | string  | Fixed value: `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? | Subscription status before the change. It may be omitted in creation events. |
| `subscription.current`  | object  | Subscription status after the change                                         |

**Snapshot field**

`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-renew 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-renew enabled               |
| `AUTO_RENEW_DISABLED`  | Auto-renew 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 grant error occurs, please make 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. When the app initializes, `getPendingOrders`is recommended for handling pending orders.
{% endhint %}

**Recovery flow**

1. `getPendingOrders` — Query 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

**Usage example**

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