For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 callhave 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.

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, Awaitableand Taskbranching, 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.

Basic pattern

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

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

Important: there is one exception. In-app payment's ProcessProductGrant callback alone is synchronous boolis 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>

awaitCode 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 .

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

Note: Task.WhenAllis Taskavailable only in Awaitableand 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 .

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

Timeout

All asynchronous APIs take timeoutMsas their last argument. The default value is 0is wait indefinitely.

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".

AITClientTimeoutExceptionis inherits from AITException, so the existingblock catches it as-is. Only catch it first if you want to handle timeout separately. catch (AITException) ErrorCode isTIMEOUT IAPCreateOneTimePurchaseOrder.

In-app payments: grant approval and server verification

IAPCreateSubscriptionPurchaseOrder / The callback passed tosynchronously returns whether to grant the payment ProcessProductGrant as boola 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

ProcessProductGrantis 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.

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

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

Why must it be synchronous?

While the payment overlay is open, visibilityState = hiddenso requestAnimationFramestops, and the Unity WebGL player loop that runs only on it stops as well. So a continuation inside the callback awaitends 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 towas to prevent this boolpattern 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

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).

onEventThe reason this is the first valid moment is that it is the earliest moment when you have both OrderIdand a live player loopat the same time. Below is the timeline of a payment measured on a real device.

onEventFrom here on, frames run at normal speed, so you can use awaitfreely (WaitForSecondsRealtime(0.2f)completed in 202ms).

Note: SuccessEvent.Datadoes not contain Sku. Which product it was must be taken from the skuyou passed when starting the purchase, or the server must look it up OrderIdusing it.

What does the server verify?

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

  • mTLS certificates are required(server-to-server communication). See the authentication docsfor 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 OrderIdyou must send this header as well.

  • In the response, skuyou can check the actually purchased product via. Do not trust the SKU told to you by the client.

Response statusis 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. trueOrders that returned PURCHASEDremain PAYMENT_COMPLETED, while others remain

For the full specification, see the official IAP docsfor 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, onEventyou 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.

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 OrderIdof 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 truethe 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 falseresponse.) Therefore, falseTIMEOUT 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 falseby using false.

Note: In older Toss apps, the return value is ignored. processProductGrantIn 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 existingthis is thrown.

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

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

IsPlatformUnavailable

This flag is not passed as a separate field, but is determined by looking at the error message.If any of the strings below appear, trueis 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.AppsInTosshas not been initialized yet

trueIf so, it is not a code bug, but an execution environment issueBecause 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_EDITORis 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.

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 thingsand 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 > Configurationthe 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.

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. SafeAreaInsetsSubscribeonly null

The important thing is that the class type nullcomes as. In the Editor, result.SomeFieldif you read it directly NullReferenceExceptionis 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 foreachis safe.

devtools

@apps-in-toss/devtoolsTIMEOUT @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 > Configurationchange the devtools setting in AIT > Configuration — since the build artifacts stay the same, it is reflected just by restarting the serverIf 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=0or 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), IsPlatformUnavailablean error trueis inherits from AITException, so the existingthrown.

Was this helpful?