> 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/unity/first-steps/api-usage-patterns.md).

# API Usage Patterns

This covers the patterns you repeatedly run into when calling the SDK API from C#. Rather than what each individual API does, **rules that apply no matter which API you call**have been collected here.

### Where are the original API docs?

`Runtime/SDK/`The C# surface of Runtime/SDK/ is automatically generated from the type definitions of the client SDK (`@apps-in-toss/web-framework`). Currently, **there are 85 APIs in 24 categories**.

| Where                                                                     | What                                                                                                                                   |
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Unity IntelliSense                                                        | Descriptions, parameters, and return values for individual APIs. JSDoc from the parent SDK has been carried over into C# XML comments. |
| [Apps in Toss Developer Center](https://developers-apps-in-toss.toss.im/) | Official client SDK documentation for platform policies, console settings, server integration, and more                                |
| This collection of documents                                              | Unity-specific details not covered by the two above                                                                                    |

The reason this repository does not keep a separate API reference in its documentation is that it would become a manual copy of the upstream docs. The C# surface is regenerated every time the SDK is updated, but handwritten Markdown is not, so over time they will inevitably drift apart. Instead, **IntelliSense is always up to date**, and this document only covers what the upstream docs do not — async/await, `Awaitable`and `Task`branching, `timeoutMs`, `AITException.ErrorCode`, Mock(Editor mock, devtools), IL2CPP stripping.

How the C# surface changed depending on the SDK version can be found in [API change history](https://toss.github.io/apps-in-toss-unity-sdk/docs/changelog/index.html).

### Basic pattern

SDK APIs are asynchronous. `await`waiting for the result with does not block Unity's main thread.

{% code collapsedlinecount="10" %}

```csharp
using AppsInToss;
using UnityEngine;

public class Example : MonoBehaviour
{
    async void Start()
    {
        // wait for the asynchronous result with the await keyword
        string deviceId = await AIT.GetDeviceId();
        Debug.Log($"Device ID: {deviceId}");
    }
}
```

{% endcode %}

> **Important**: there is one exception. In-app payment's `ProcessProductGrant` callback alone is synchronous `bool`is returned. See the section below for the reason and correct structure. **In-app payments: grant approval and server verification** section.

#### Awaitable and Task

Even for the same API, the return type differs depending on the Unity version.

| Unity version    | Return type                 |
| ---------------- | --------------------------- |
| 6000.0 and above | `Awaitable`, `Awaitable<T>` |
| and below        | `Task`, `Task<T>`           |

`await`Code that consumes it works unchanged on both sides, so most of the time you don't need to worry about it. It only branches when you **explicitly write the return type** .

{% code collapsedlinecount="10" %}

```csharp
// ❌ Compiles only on Unity 6 or later
public async Awaitable<bool> ProcessPayment(string orderId) { ... }

// ✅ Compiles on either side — no return type written
async void ProcessPayment(string orderId) { ... }
```

{% endcode %}

If you need to support both versions and also need a return type, split them with conditional compilation.

{% code collapsedlinecount="10" %}

```csharp
#if UNITY_6000_0_OR_NEWER
    public async Awaitable<bool> ProcessPayment(string orderId)
#else
    public async Task<bool> ProcessPayment(string orderId)
#endif
    {
        try
        {
            var result = await AIT.CheckoutPayment(options);
            return result != null;
        }
        catch (AITException)
        {
            return false;
        }
    }
```

{% endcode %}

> **Note**: `Task.WhenAll`is `Task`available only in `Awaitable`and not in . If you want to run multiple APIs at the same time on Unity 6 or later, use the method below.

#### Calling multiple APIs

For sequential calls, just chain them `await` .

{% code collapsedlinecount="10" %}

```csharp
async void InitializeGame()
{
    string deviceId = await AIT.GetDeviceId();
    string platform = await AIT.GetPlatformOS();
    string locale = await AIT.GetLocale();

    Debug.Log($"Device: {deviceId}, platform: {platform}, locale: {locale}");
}
```

{% endcode %}

If the calls are independent of each other, starting them all first and then waiting for each later overlaps the round trips. This approach `Awaitable`and `Task` works the same on both sides.

{% code collapsedlinecount="10" %}

```csharp
async void InitializeGameParallel()
{
    // Start them all first — don't await here
    var deviceIdOp = AIT.GetDeviceId();
    var platformOp = AIT.GetPlatformOS();
    var localeOp = AIT.GetLocale();

    // Then collect each one
    string deviceId = await deviceIdOp;
    string platform = await platformOp;
    string locale = await localeOp;

    Debug.Log($"Device: {deviceId}, platform: {platform}, locale: {locale}");
}
```

{% endcode %}

### Timeout

All asynchronous APIs take `timeoutMs`as their last argument. The default value is `0`is **wait indefinitely**.

{% code collapsedlinecount="10" %}

```csharp
try
{
    string deviceId = await AIT.GetDeviceId(timeoutMs: 3000);
}
catch (AITClientTimeoutException ex)
{
    Debug.LogWarning($"No response arrived within {ex.TimeoutMs}ms");
}
```

{% endcode %}

This timeout **only gives up waiting on the C# side.** JavaScript and platform work across the bridge may still continue, and late-arriving results are discarded. So when you set a timeout for APIs with side effects (payments, sharing, permission requests, etc.), you must not assume that "timeout = not executed".

`AITClientTimeoutException`is `inherits from AITException, so the existing`block catches it as-is. Only catch it first if you want to handle timeout separately. `catch (AITException)` ErrorCode `is`TIMEOUT `IAPCreateOneTimePurchaseOrder`.

### In-app payments: grant approval and server verification

`IAPCreateSubscriptionPurchaseOrder` / `The callback passed to`synchronously returns whether to grant the payment `ProcessProductGrant` as `bool`a synchronous return **. The key point is not to verify inside this callback — approve immediately in the callback, and do server verification and actual delivery after the overlay closes**. **afterward** `onEvent`.

#### This callback is not optional

`ProcessProductGrant`is a nullable field, so the code compiles even if you do not specify it, **but if you don't specify it, all payments are treated as delivery failures.**

{% code collapsedlinecount="10" %}

```csharp
// ❌ It compiles and the payment window appears, but the product is not delivered
var options = new IapCreateOneTimePurchaseOrderOptionsOptions { Sku = sku };
```

{% endcode %}

The JS bridge always passes this callback **always** to the platform, so if there is no handler registered in C#, the SDK automatically responds `false`each time the payment completes. In this case, the following error is left in the Console.

{% code collapsedlinecount="10" %}

```
[AITCore] Nested callback 'processProductGrant' is not registered (id: ...); responding false.
The payment already succeeded, so the product will NOT be granted and the user may see a
refund notice. Set ProcessProductGrant on the order options and return the grant decision
(e.g. _ => true); verify and deliver later in onEvent.
```

{% endcode %}

Fill in this field first when wiring up the payment flow.

#### Why must it be synchronous?

While the payment overlay is open, `visibilityState = hidden`so `requestAnimationFrame`stops, and the Unity WebGL player loop that runs only on it stops as well. So a continuation inside the callback `await`ends up waiting for the frame that will arrive only after the overlay closes, while the overlay waits for the callback's response — a deadlock. In actual device measurements, this loop lasted **115 seconds** before `"There was a problem with {app name}. Please request a refund"` page appeared (it may be shown if there is no response within 30 seconds after payment success), and the payment that was approved immediately closed the overlay in `true` 1.5 seconds **and completed normally. Fixing the return type to**was to prevent this `bool`pattern at compile time. `await` Two ledgers

#### The callback's return value and my server's delivery record are

two different ledgers **.**.

|                                    | What is recorded?             | Ownership | Deadline                    |
| ---------------------------------- | ----------------------------- | --------- | --------------------------- |
| `ProcessProductGrant` Return value | **Was the payment consumed?** | Toss      | 30 seconds (no frame)       |
| My server's delivery record        | **Was the item delivered?**   | Developer | No deadline, retry possible |

Verification does not block the first ledger, **it blocks the second ledger.** The callback is the place that says "payment consumption received," and verification and delivery happen afterward at leisure.

So the code to put in this callback is effectively reduced to one line.

#### Stage 1 callback: approve immediately

{% code collapsedlinecount="10" %}

```csharp
var options = new IapCreateOneTimePurchaseOrderOptionsOptions
{
    Sku = sku,
    ProcessProductGrant = _ => true
};
```

{% endcode %}

The very fact that this callback was called already means the app has determined the payment succeeded. The only information the callback carries is `OrderId` , so there is nothing new you can verify here.

#### Stage 2 verification and delivery happen in onEvent

The only two times you can call server verification **are**.

1. in the normal flow **`onEvent`** — immediately after the overlay closes.
2. If you missed even that **App-start line**(Stage 3).

`onEvent`The reason this is the first valid moment is that it is the earliest moment when you have both **`OrderId`and a live player loop**at the same time. Below is the timeline of a payment measured on a real device.

{% code collapsedlinecount="10" %}

```
00:35:48.563  Payment overlay covers the screen      player loop stops ─┐
                                                                │ In this section, await does
                 ⋮  (user interacting with the payment UI)              │ not resume.
                                                                │ Calling verification causes a deadlock.
00:36:01.413  ProcessProductGrant → immediately true   [Stage 1]         │
00:36:02.725  Overlay closes                     loop resumes ──────┘
00:36:02.796  onEvent arrives              (+71ms)  [Stage 2] ← server verification is called here
00:36:02.998  Verification complete        (+202ms)          await resumes normally
```

{% endcode %}

`onEvent`From here on, frames run at normal speed, so you can use `await`freely (`WaitForSecondsRealtime(0.2f)`completed in 202ms).

{% code collapsedlinecount="10" %}

```csharp
_disposer = AIT.IAPCreateOneTimePurchaseOrder(
    onEvent: e =>
    {
        // Since the payment is already confirmed, you can reflect it in the UI immediately
        ShowPurchaseSuccess(e.Data.DisplayAmount);

        // Leave verification and delivery to the server; do not wait for them
        _ = DeliverAsync(e.Data.OrderId);
    },
    options: options,
    onError: err => Debug.LogError(err.Message)
);

async Task DeliverAsync(string orderId)
{
    // Here the frame loop is running normally, so await is safe
    await MyServer.VerifyAndDeliver(orderId);
}
```

{% endcode %}

> **Note**: `SuccessEvent.Data`does not contain `Sku`. Which product it was must be taken from the `sku`you passed when starting the purchase, or the server must look it up `OrderId`using it.

#### What does the server verify?

You must not trust `OrderId`sent by the client as-is. The developer server checks directly with Toss via the **order status lookup API**.

{% code collapsedlinecount="10" %}

```
POST https://apps-in-toss-api.toss.im/api-partner/v1/apps-in-toss/order/get-order-status
{ "orderId": "..." }
```

{% endcode %}

* **mTLS certificates are required**(server-to-server communication). See the [authentication docs](https://developers-apps-in-toss.toss.im/api/auth)for certificate and user-auth-header guidance.
* `x-toss-user-key` If you put the userKey obtained from Toss login in the header, **only that user's orders** are returned. If you do not include it, all orders are queried, so to prevent intercepting and reusing another user's `OrderId`you must send this header as well.
* In the response, `sku`you can check the actually purchased product via. Do not trust the SKU told to you by the client.

Response `status`is the core of this API.

| status                                       | Meaning                                            |
| -------------------------------------------- | -------------------------------------------------- |
| `PURCHASED`                                  | Both payment and product delivery are complete     |
| `PAYMENT_COMPLETED`                          | Payment completed, but **product delivery failed** |
| `REFUNDED`                                   | Refund completed                                   |
| `FAILED` / `ORDER_IN_PROGRESS` / `NOT_FOUND` | Payment failed / in progress / order not found     |

The first two values are exactly `ProcessProductGrant` the result of the return value. `true`Orders that returned `PURCHASED`remain `PAYMENT_COMPLETED`, while others remain

For the full specification, see the [official IAP docs](https://developers-apps-in-toss.toss.im/documentation/sdk/domains-api/iap)for certificate and user-auth-header guidance.

#### Stage 3 app-start undelivered-orders sweep

There is no guarantee that Stage 2 always runs. If the app exits right after the callback sends `true`, `onEvent`you will not receive `IAPGetPendingOrders`, and that order already has payment consumption confirmed, so it will not appear there either.

The way to recover this case is `IAPGetCompletedOrRefundedOrders`. Sweep once at app start or when returning to the foreground to find orders that my server has not delivered.

{% code collapsedlinecount="10" %}

```csharp
var completed = await AIT.IAPGetCompletedOrRefundedOrders();
if (completed.Orders == null) return;   // If the platform is unsupported, the reason is stored in the error field

foreach (var order in completed.Orders)
{
    if (order.Status != CompletedOrRefundedOrdersResultOrderStatus.COMPLETED) continue;

    // Whether it was delivered is determined by the server record. Local records such as PlayerPrefs
    // disappear on reinstall or device change, so they cannot be the basis for judging this routine.
    await MyServer.DeliverIfMissing(order.OrderId, order.Sku);
}
```

{% endcode %}

Without this Stage 3, the Stage 1 immediate approval becomes risky. **The three are a single bundle.**

> **Important**: Refunds can only be known by polling. No webhook is provided to notify the developer server when a payment or refund occurs. Even if the user gets a refund, the developer won't know until the app runs again and this routine executes. If you need to reclaim the product for a refunded order, you must store the `OrderId`of the delivered order on the server and periodically check it with the order status lookup API.

#### When is false returned?

The official docs say that for a response that is not `true`the refund guidance page may be *shown*. (What I measured directly was the no-response path, and I did not verify whether the same screen appears for an explicit `false`response.) Therefore, `false`TIMEOUT **use it only when you really cannot give this product** — for example, when a non-consumable you already own was acquired on another device during payment, and you can already conclude that delivery is impossible.

"I don't have confidence, so I'll just `false`" does not hold. That would make the app show the refund guidance page on every payment. Confidence is gained through stages 1–3, not `false`by using false.

> **Note**: In older Toss apps, the return value is ignored. `processProductGrant`In versions that do not support (below Android 5.231.1 / below iOS 5.230.0), the bridge falls back to the old payment path, and at that time the callback's return value is not passed to the platform and is discarded. Keep this range in mind when writing logic that depends on the return value.

### Error handling

If the API call fails `inherits from AITException, so the existing`this is thrown.

{% code collapsedlinecount="10" %}

```csharp
using AppsInToss;
using UnityEngine;

public class ErrorHandling : MonoBehaviour
{
    async void CallAPI()
    {
        try
        {
            var result = await AIT.GetDeviceId();
            Debug.Log($"Success: {result}");
        }
        catch (AITException ex)
        {
            Debug.LogError($"API error: {ex.Message}");
            Debug.LogError($"Error code: {ex.ErrorCode}");
        }
        catch (System.Exception ex)
        {
            Debug.LogError($"Unexpected error: {ex.Message}");
        }
    }
}
```

{% endcode %}

| Property                | Type     | Description                                                      |
| ----------------------- | -------- | ---------------------------------------------------------------- |
| `Message`               | `string` | A human-readable error message                                   |
| `is`                    | `string` | Error code. Empty string if the platform does not provide one    |
| `APIName`               | `string` | The name of the failed API. Empty string if unknown              |
| `IsPlatformUnavailable` | `bool`   | Whether the error was caused by the absence of a platform bridge |

`is`Keep in mind that the value may be empty if you want to branch on it.

{% code collapsedlinecount="10" %}

```csharp
catch (AITException ex)
{
    switch (ex.ErrorCode)
    {
        case "PAYMENT_CANCELLED":
            Debug.Log("The user cancelled the payment.");
            break;
        case "PAYMENT_FAILED":
            Debug.LogError("An error occurred while processing the payment.");
            break;
        case "NETWORK_ERROR":
            Debug.LogError("Please check your network connection.");
            break;
        default:
            Debug.LogError($"Unknown error: {ex.Message}");
            break;
    }
}
```

{% endcode %}

#### IsPlatformUnavailable

This flag is not passed as a separate field, but **is determined by looking at the error message.**&#x49;f any of the strings below appear, `true`is determined.

| Determination string                  | When                                            |
| ------------------------------------- | ----------------------------------------------- |
| `__GRANITE_NATIVE_EMITTER`            | No native emitter                               |
| `ReactNativeWebView`                  | Running outside the Toss app WebView            |
| `is not a constant handler`           | There is no bridge handler for that API         |
| `Cannot read properties of undefined` | `window.AppsInToss`has not been initialized yet |

`true`If so, it is not a code bug, but **an execution environment issue**Because this often happens in a regular browser or development environment, when reporting errors it is better to lower the severity of this case or filter it out.

### Behavior by execution environment

| Environment                            | Behavior                                                               |
| -------------------------------------- | ---------------------------------------------------------------------- |
| WebGL build + Apps in Toss app         | Actual native API call                                                 |
| WebGL build + regular browser          | Usually fails. If devtools is on (Dev Server), it responds with a mock |
| Unity Editor                           | Editor mock call                                                       |
| Other platforms (Windows, macOS, etc.) | Editor mock call                                                       |

Editor mock is independent of the build profile. `Runtime/SDK/`of each API `#if UNITY_WEBGL && !UNITY_EDITOR`is split by this, so if it is not a WebGL build, **at compile time,** only the mock path remains.

If needed, you can branch by execution environment.

{% code collapsedlinecount="10" %}

```csharp
void Start()
{
#if UNITY_WEBGL && !UNITY_EDITOR
    // WebGL-only logic
#else
    // Logic for development and testing
#endif
}
```

{% endcode %}

To check actual native behavior, you must build for WebGL and run it in the Apps in Toss app. In the Editor, it is mock no matter what you do.

### Mock

What is called "Mock" **are two things**and they behave differently.

|                    | Editor mock                                               | devtools                                                                                                           |
| ------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Where              | `Runtime/SDK/`of C#                                       | `@apps-in-toss/devtools`(npm package, works when opening build artifacts in a browser)                             |
| What               | All SDK APIs                                              | 60+ SDK APIs + a floating panel for manipulating state                                                             |
| When               | When it is not a WebGL build (determined at compile time) | When opening a build run with the Dev Server in a regular browser                                                  |
| How to turn it off | Cannot be turned off                                      | `AIT > Configuration`the devtools setting in, or the environment variable when running the server `AIT_DEVTOOLS=0` |

#### Editor mock

When you call an API from the Unity Editor or on a non-WebGL platform, it logs and returns a default value. Because it does not throw exceptions, game logic does not stop in the Editor.

{% code collapsedlinecount="10" %}

```
[AIT Mock] GetDeviceId called
[AIT Mock] GetPlatformOS called
```

{% endcode %}

| Return type          | Mock return value                                               |
| -------------------- | --------------------------------------------------------------- |
| `string`             | Empty string `""`                                               |
| `bool`               | `false`                                                         |
| Array                | Empty array                                                     |
| Class type           | `default`, that is, `null`                                      |
| Unsubscribe `Action` | A function that only logs. `SafeAreaInsetsSubscribe`only `null` |

The important thing is that the class type `null`comes as. In the Editor, `result.SomeField`if you read it directly `NullReferenceException`is thrown. If it is logic you want to run in the Editor too, add a null check. APIs that return arrays will return an empty array, so `foreach`is safe.

#### devtools

`@apps-in-toss/devtools`TIMEOUT `@apps-in-toss/web-framework` **3.x only** development tool. When you run the Dev Server, the vite plugin `@apps-in-toss/web-framework` aliases imports to the mock implementation, so more than 60 SDK APIs work as mocks in a regular browser without the Toss app. At the same time, a floating panel appears on the screen, allowing you to directly manipulate mock state such as login status, ad results, and storage values.

The panel is enabled by default. To turn off all devtools (or only the panel), `AIT > Configuration`change the devtools setting in AIT > Configuration — since the build artifacts stay the same, **it is reflected just by restarting the server**If you want to turn it off just once without changing the settings, such as for CI or a quick check, you can override it with the server environment variable `AIT_DEVTOOLS=0`or override it with.

If you call an SDK API while devtools is turned off (for example, when opening it in a regular browser with devtools disabled), `IsPlatformUnavailable`an error `true`is `inherits from AITException, so the existing`thrown.

### Related docs

* [Getting started](https://developers-apps-in-toss.toss.im/documentation/unity/first-steps/getting-started) — Installation and basic setup
* [Ad integration](https://developers-apps-in-toss.toss.im/documentation/unity/add-features/advertising) — How to use the ad API
* [Sentry integration](https://developers-apps-in-toss.toss.im/documentation/unity/add-features/sentry-integration) — Collecting errors in Sentry
* [Build profile](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles) — Where to find the devtools settings
* [Troubleshooting](https://developers-apps-in-toss.toss.im/documentation/unity/first-steps/faq) — Common sticking points


---

# 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/unity/first-steps/api-usage-patterns.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.
