> 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 patterns you repeatedly encounter when calling SDK APIs from C#. Rather than what each individual API does, **rules that apply no matter which API you call**are collected here.

### Where is the original API text?

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

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

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

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

### Basic patterns

SDK APIs are asynchronous. `await`Waiting for the result with

```csharp
using AppsInToss;
using UnityEngine;

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

> **Important**: There is one exception. Only the in-app purchase `ProcessProductGrant` callback returns synchronously `bool`. The reason and the correct structure are below. **In-app payments: delivery 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 or later | `Awaitable`, `Awaitable<T>` |
| and below       | `Task`, `Task<T>`           |

`await`Code that consumes it with await works unchanged on both sides, so in most cases you don't need to worry about it. Only when you **explicitly write the return type** does it diverge.

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

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

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

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

> **Note**: `Task.WhenAll`is `Task`is only in `Awaitable`and not in Awaitable. To run multiple APIs concurrently in Unity 6 or later, use the approach below.

#### Calling multiple APIs

Sequential calls are just chained `await` together.

```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}, Language: {locale}");
}
```

If the calls are independent, start them all first and then await each one later so the round trips overlap. This works `Awaitable`and `Task` the same on both sides.

```csharp
async void InitializeGameParallel()
{
    // Start everything first — do not 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}, Language: {locale}");
}
```

### Timeout

All asynchronous APIs take `timeoutMs`as the last argument. Default value `0`is **wait indefinitely**.

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

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

`AITClientTimeoutException`is `AITException`inherits from, so existing `catch (AITException)` catch blocks handle it as-is. Catch it first only when you want to handle only timeouts separately. `ErrorCode`is `TIMEOUT`.

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

`IAPCreateOneTimePurchaseOrder` / `IAPCreateSubscriptionPurchaseOrder`passed to `ProcessProductGrant` callback returns whether to grant the item `bool`as **synchronous return**. The key is not to validate in this callback — approve it immediately here, and do server verification and actual granting after the overlay closes **afterward** `onEvent`in onEvent.

#### This callback is not optional

`ProcessProductGrant`is a nullable field, so it compiles even if you do not specify it, but **if you don't specify it, every payment is treated as a grant failure.**

```csharp
// ❌ It compiles and the payment window opens, but the item will not be granted
var options = new IapCreateOneTimePurchaseOrderOptionsOptions { Sku = sku };
```

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

```
[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.
```

If you're hooking up the payment flow, fill in this field first.

#### 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 too. So a continuation inside the callback `await`waits for the frame that comes only after the overlay closes, and the overlay waits for the callback response, creating a deadlock. In real-device measurements, this loop held for **115 seconds** before remaining `"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 a successful payment), and the payment that was approved immediately closed the overlay after `true` 30 seconds **1.5 seconds**and completed normally. The reason the return type was fixed to `bool`was to prevent this shape at compile time. `await` There are two ledgers

#### The callback's return value and my server's grant records are

two separate ledgers **What is recorded?**.

|                                    | Ownership                          | Closure   | Deadline               |
| ---------------------------------- | ---------------------------------- | --------- | ---------------------- |
| `ProcessProductGrant` Return value | **Has the payment been consumed?** | Toss      | 30 seconds (no frame)  |
| My server's grant records          | **Was the item delivered?**        | Developer | No deadline, retryable |

Verification does not block the first ledger; **it blocks the second ledger.** The callback is the place to answer, "I received payment consumption," and verification and granting are done afterward at leisure.

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

#### Step 1 callback approves immediately

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

The fact that this callback was called already means the app has determined the payment was successful. The only information this callback carries is `OrderId` so there is nothing new to verify here.

#### Step 2 verification and granting in onEvent

Server verification **There are only two times you can call it**.

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

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

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

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

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

        // Hand verification and granting over to the server and don't wait
        _ = DeliverAsync(e.Data.OrderId);
    },
    options: options,
    onError: err => Debug.LogError(err.Message)
);

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

> **Note**: `SuccessEvent.Data`contains `Sku`isn't there. To know which product it is, either capture the `sku`passed when starting the purchase in a closure, or the server must look it up via `OrderId`.

#### What does the server verify?

What the client sent `OrderId`must not be trusted as-is. The developer server directly checks with Toss using **Order status lookup API**POST <https://apps-in-toss-api.toss.im/api-partner/v1/apps-in-toss/order/get-order-status>

```
{ "orderId": "..." }
mTLS certificate is required
```

* **(server-to-server communication). Instructions for certificates and user-auth headers are in the**authentication docs [If you put the userKey obtained from Toss login in the header](https://developers-apps-in-toss.toss.im/documentation/api/auth).
* `x-toss-user-key` only that user's orders **are returned.** If you don't include it, all orders are queried, so to prevent intercepting and reusing another user's `OrderId`you should send this header as well.
* In the response, `sku`you can check the actually purchased item via . Don't trust the SKU the client told you.

Response `status`is the key part of this API.

| status                                       | Meaning                                         |
| -------------------------------------------- | ----------------------------------------------- |
| `PURCHASED`                                  | Both payment and item delivery completed        |
| `PAYMENT_COMPLETED`                          | Payment completed, but **item delivery failed** |
| `REFUNDED`                                   | Refund completed                                |
| `FAILED` / `ORDER_IN_PROGRESS` / `NOT_FOUND` | Payment failed / in progress / no order         |

The first two values are the result of `ProcessProductGrant` the return value. `true`Orders for which it returns are `PURCHASED`, while orders that do not remain `PAYMENT_COMPLETED`as

For detailed specifications, [the official IAP documentation](https://developers-apps-in-toss.toss.im/documentation/sdk/domains-api/iap).

#### Step 3: Unfulfilled-delivery sweep at app start

There is no guarantee that Step 2 will always run. If the app quits right after the callback sends `true`you will not receive `onEvent`and that order has already been confirmed as payment consumed, so it will not appear in `IAPGetPendingOrders`either.

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

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

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

    // The criterion for delivery is server records. Local records such as PlayerPrefs
    // disappear on reinstall or device change, so they cannot be the basis for this sweep.
    await MyServer.DeliverIfMissing(order.OrderId, order.Sku);
}
```

Without this Step 3, the immediate approval in Step 1 becomes risky. **These three form one package.**

> **Important**Refunds can only be detected 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 cannot know until the app runs again and this sweep is performed. If you need to reclaim items from refunded orders, keep the `OrderId`of the delivered orders on the server and periodically check them with the order status lookup API.

#### When does false return?

The official docs say `true`for responses that are not *a refund notice page may be shown*. (What I measured directly was the no-response path, and I have not verified whether the same screen appears for explicit `false`.) Therefore, `false`is **use it only when you truly cannot grant this item** — for example, when a non-consumable you already own is acquired on another device during checkout, and you can already conclude that granting is impossible.

"Since I'm not sure, let's just `false`" does not hold. That would make the app show a refund notice on every payment. Certainty is obtained through steps 1\~3, not through `false`.

> **Note**: In older Toss app versions, the return value is ignored. `processProductGrant`In versions that do not support it (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 in mind when writing logic that depends on the return value.

### Error Handling

If the API call fails, `AITException`is thrown.

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

| Property                | Type     | Description                                                   |
| ----------------------- | -------- | ------------------------------------------------------------- |
| `Message`               | `string` | Human-readable error message                                  |
| `ErrorCode`             | `string` | Error code. Empty string if the platform does not provide one |
| `APIName`               | `string` | Name of the failed API. Empty string if unknown               |
| `IsPlatformUnavailable` | `bool`   | Whether the error is due to a missing platform bridge         |

`ErrorCode`If you want to branch on it, keep in mind that the value may be empty.

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

#### IsPlatformUnavailable

This flag is not passed as a separate field, but **determined by looking at the error message**If it contains any of the strings below, `true`it becomes true.

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

`true`if so, it's not a code bug **an execution environment issue**It is. Since this commonly happens in regular browsers or development environments, when filing an error report it is better to downgrade this case to low severity or filter it out.

### Behavior by execution environment

| Environment                            | Behavior                                                               |
| -------------------------------------- | ---------------------------------------------------------------------- |
| WebGL build + Apps in Toss app         | Actual native API calls                                                |
| WebGL build + regular browser          | Mostly fails. If devtools are enabled (Dev Server), responds with mock |
| Unity Editor                           | Editor mock calls                                                      |
| Other platforms (Windows, macOS, etc.) | Editor mock calls                                                      |

Editor mock is unrelated to the build profile. `Runtime/SDK/`Each API of `#if UNITY_WEBGL && !UNITY_EDITOR`is split into, so if it's not a WebGL build **at compile time** only the mock path remains.

If needed, you can branch by execution environment.

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

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

### Mock

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

|                    | Editor mock                                              | devtools                                                                                                         |
| ------------------ | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| where              | `Runtime/SDK/`C# of                                      | `@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's not a WebGL build (determined at compile time) | When opening a build run with Dev Server in a regular browser                                                    |
| How to turn it off | Cannot be turned off                                     | `AIT > Configuration`the devtools settings, or the environment variable when running the server `AIT_DEVTOOLS=0` |

#### Editor mock

When calling APIs in the Unity Editor and on non-WebGL platforms, it logs and returns default values. It does not throw exceptions, so game logic does not stop in the Editor.

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

| 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 class type `null`comes in as. What matters is that in the Editor `result.SomeField`if you read it directly `NullReferenceException`occurs. If this is logic you want to run in the Editor too, add a null check. APIs that return arrays will return empty arrays, so `foreach`is safe.

#### devtools

`@apps-in-toss/devtools`is `@apps-in-toss/web-framework` **3.x only** This is a development tool. When you run Dev Server, the Vite plugin `@apps-in-toss/web-framework` aliases imports to mock implementations, 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 of devtools (or just the panel), `AIT > Configuration`change the devtools settings — since the build artifacts stay the same, **it takes effect just by restarting the server**If you want to disable it just once without touching settings, such as in CI or for a quick check, use the server run environment variable `AIT_DEVTOOLS=0`to override it.

If you call an SDK API while devtools are turned off (for example, when opened in a regular browser with devtools disabled), `IsPlatformUnavailable`If it is `true`is `AITException`triggered.

### Related documents

* [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 with Sentry
* [Build Profiles](https://developers-apps-in-toss.toss.im/documentation/unity/build/build-profiles) — devtools settings location
* [Troubleshooting](https://developers-apps-in-toss.toss.im/documentation/unity/first-steps/faq) — Common stumbling 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.
