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

API 사용 패턴

SDK API를 C#에서 호출할 때 반복해서 마주치는 패턴을 다룹니다. 개별 API가 무엇을 하는지가 아니라, 어떤 API를 부르든 똑같이 적용되는 규칙을 모았습니다.

API 원문은 어디에 있나

Runtime/SDK/의 C# 표면은 클라이언트 SDK(@apps-in-toss/web-framework)의 타입 정의에서 자동 생성됩니다. 현재 24개 카테고리에 85개 API가 있습니다.

어디서
무엇을

Unity IntelliSense

개별 API의 설명, 파라미터, 반환값. 상위 SDK의 JSDoc이 C# XML 주석으로 옮겨져 있습니다

플랫폼 정책, 콘솔 설정, 서버 연동 등 클라이언트 SDK 공식 문서

이 문서 모음

위 둘에 없는 Unity 고유 사정

이 저장소의 문서에 API 레퍼런스를 따로 두지 않는 이유는, 그것이 상위 문서의 수기 사본이 되기 때문입니다. C# 표면은 SDK를 업데이트할 때마다 재생성되지만 손으로 쓴 마크다운은 그렇지 않아서, 시간이 지나면 반드시 어긋납니다. 대신 IntelliSense가 항상 최신이고, 이 문서는 상위 문서가 다루지 않는 것만 씁니다 — async/await, AwaitableTask의 분기, timeoutMs, AITException.ErrorCode, Mock(Editor mock, devtools), IL2CPP 스트리핑.

SDK 버전에 따라 C# 표면이 어떻게 달라졌는지는 API 변경 이력에서 확인할 수 있습니다.

기본 패턴

SDK API는 비동기입니다. await로 결과를 기다리면 Unity 메인 스레드를 막지 않습니다.

using AppsInToss;
using UnityEngine;

public class Example : MonoBehaviour
{
    async void Start()
    {
        // await 키워드로 비동기 결과를 대기
        string deviceId = await AIT.GetDeviceId();
        Debug.Log($"Device ID: {deviceId}");
    }
}

중요: 예외가 하나 있습니다. 인앱결제의 ProcessProductGrant 콜백만은 동기 bool을 반환합니다. 이유와 올바른 구조는 아래 인앱결제: 지급 승인과 서버 검증 절을 참고하세요.

Awaitable 과 Task

같은 API라도 Unity 버전에 따라 반환형이 다릅니다.

Unity 버전
반환형

6000.0 이상

Awaitable, Awaitable<T>

그 이하

Task, Task<T>

await로 소비하는 코드는 양쪽에서 그대로 동작하므로 대부분은 신경 쓸 필요가 없습니다. 반환형을 명시적으로 적을 때만 갈라집니다.

두 버전을 모두 지원해야 하는데 반환형이 필요하다면 조건부 컴파일로 나눕니다.

참고: Task.WhenAllTask에만 있고 Awaitable에는 없습니다. Unity 6 이상에서 여러 API를 동시에 진행시키려면 아래 방식을 쓰세요.

여러 API 호출하기

순차 호출은 그냥 이어서 await 합니다.

서로 독립적인 호출이라면 먼저 전부 시작해 두고 나중에 각각 기다리면 왕복이 겹칩니다. 이 방식은 AwaitableTask 양쪽에서 동일하게 동작합니다.

타임아웃

모든 비동기 API는 마지막 인자로 timeoutMs를 받습니다. 기본값 0무제한 대기입니다.

이 타임아웃은 C# 쪽 대기만 포기합니다. 브릿지 너머의 JavaScript와 플랫폼 작업은 계속 진행될 수 있고, 뒤늦게 도착한 결과는 버려집니다. 따라서 부수 효과가 있는 API(결제, 공유, 권한 요청 등)에 타임아웃을 걸 때는 "타임아웃 = 실행되지 않음"으로 단정하면 안 됩니다.

AITClientTimeoutExceptionAITException을 상속하므로 기존 catch (AITException) 블록이 그대로 받아냅니다. 타임아웃만 따로 다루고 싶을 때만 먼저 잡으세요. ErrorCodeTIMEOUT입니다.

인앱결제: 지급 승인과 서버 검증

IAPCreateOneTimePurchaseOrder / IAPCreateSubscriptionPurchaseOrder에 넘기는 ProcessProductGrant 콜백은 지급 여부를 bool동기 반환합니다. 핵심은 이 콜백에서 검증하지 않는 것입니다 — 콜백은 즉시 승인하고, 서버 검증과 실제 지급은 오버레이가 닫힌 onEvent에서 합니다.

이 콜백은 선택이 아닙니다

ProcessProductGrant는 nullable 필드라 지정하지 않아도 컴파일되지만, 지정하지 않으면 모든 결제가 지급 실패로 처리됩니다.

JS 브릿지는 이 콜백을 항상 플랫폼에 넘기므로, C#에 등록된 핸들러가 없으면 SDK가 결제 완료 시마다 자동으로 false를 응답합니다. 이때 Console에 다음 에러가 남습니다.

결제 흐름을 붙일 때 이 필드부터 채우세요.

왜 동기여야 하나

결제 오버레이가 떠 있는 동안은 visibilityState = hidden이라 requestAnimationFrame이 멈추고, 그것으로만 도는 Unity WebGL player loop도 함께 멈춥니다. 그래서 콜백 안에서 await한 continuation은 오버레이가 닫혀야 오는 프레임을 기다리고, 오버레이는 그 콜백의 응답을 기다리는 교착이 됩니다. 실기기 실측에서 이 고리가 115초 유지된 뒤 "{앱 이름}에 문제가 생겼어요. 환불을 신청해주세요" 페이지가 떴고(결제 성공 후 30초 내 true 응답이 없으면 노출될 수 있음), 즉시 승인한 결제는 오버레이가 1.5초에 닫히고 정상 완료됐습니다. 반환형을 bool로 고정한 것은 이 await 형태를 컴파일 단계에서 막기 위해서입니다.

장부가 두 개입니다

콜백의 반환값과 내 서버의 지급 기록은 서로 다른 두 장부입니다.

무엇을 기록하나
소유
마감

ProcessProductGrant 반환값

결제가 소비됐는가

Toss

30초 (프레임 없음)

내 서버의 지급 기록

아이템을 배달했는가

개발사

마감 없음, 재시도 가능

검증은 첫 번째 장부를 막는 게 아니라 두 번째 장부를 막습니다. 콜백은 "결제 소비를 접수했다"고 답하는 자리고, 검증과 지급은 그 뒤에 여유롭게 합니다.

그래서 이 콜백에 넣을 코드는 사실상 한 줄로 정해져 있습니다.

1단계 콜백은 즉시 승인한다

이 콜백이 호출됐다는 것 자체가 이미 앱이 결제 성공을 판정했다는 뜻입니다. 콜백이 들고 오는 정보는 OrderId 하나뿐이라, 여기서 새로 검증할 수 있는 것도 없습니다.

2단계 검증과 지급은 onEvent 에서

서버 검증을 호출할 수 있는 시점은 둘뿐입니다.

  1. 정상 흐름이면 onEvent — 오버레이가 닫힌 직후.

  2. 그마저 놓쳤으면 앱 시작 시 대사(3단계).

onEvent가 첫 번째 유효 시점인 이유는, 그때가 OrderId와 살아 있는 player loop를 동시에 갖는 가장 이른 순간이기 때문입니다. 아래는 실기기에서 측정한 한 결제의 타임라인입니다.

onEvent부터는 프레임이 정상 속도로 돌므로 await를 마음껏 써도 됩니다 (WaitForSecondsRealtime(0.2f)가 202ms에 완료).

주의: SuccessEvent.Data에는 Sku가 없습니다. 어떤 상품인지는 구매를 시작할 때 넘긴 sku를 클로저로 잡아두거나, 서버가 OrderId로 조회해야 합니다.

서버는 무엇을 검증하나

클라이언트가 보낸 OrderId를 그대로 믿으면 안 됩니다. 개발사 서버는 주문 상태 조회 API로 Toss에 직접 확인합니다.

  • mTLS 인증서가 필수입니다 (서버 간 통신). 인증서와 사용자 인증 헤더 안내는 인증 문서를 참고하세요.

  • x-toss-user-key 헤더에 토스 로그인으로 얻은 userKey를 넣으면 그 유저의 주문만 응답합니다. 넣지 않으면 모든 주문이 조회되므로, 다른 유저의 OrderId를 가로채 재사용하는 것을 막으려면 이 헤더를 함께 보내야 합니다.

  • 응답의 sku로 실제 결제된 상품을 확인할 수 있습니다. 클라이언트가 알려준 SKU를 신뢰하지 마세요.

응답 status가 이 API의 핵심입니다.

status
의미

PURCHASED

결제와 상품 지급이 모두 완료

PAYMENT_COMPLETED

결제는 완료됐으나 상품 지급 실패

REFUNDED

환불 완료

FAILED / ORDER_IN_PROGRESS / NOT_FOUND

결제 실패 / 진행 중 / 주문 없음

앞의 두 값이 곧 ProcessProductGrant 반환값의 결과입니다. true를 반환한 주문은 PURCHASED, 그렇지 않은 주문은 PAYMENT_COMPLETED로 남습니다.

자세한 명세는 공식 IAP 문서를 참고하세요.

3단계 앱 시작 시 미배달 대사

2단계가 항상 실행된다는 보장은 없습니다. 콜백이 true를 보낸 직후 앱이 종료되면 onEvent를 받지 못하고, 그 주문은 이미 결제 소비가 확정돼 IAPGetPendingOrders에도 나타나지 않습니다.

이 경우를 회수하는 것이 IAPGetCompletedOrRefundedOrders입니다. 앱 시작이나 포그라운드 복귀 시 한 번 훑어, 내 서버가 배달하지 않은 주문을 찾습니다.

이 3단계가 없으면 1단계의 즉시 승인이 위험해집니다. 셋은 한 묶음입니다.

중요: 환불은 폴링으로만 알 수 있습니다. 결제나 환불이 발생했을 때 개발사 서버로 알려주는 웹훅은 제공되지 않습니다. 사용자가 환불을 받아도 앱이 다시 실행되어 이 대사가 돌기 전까지는 개발사가 알 수 없습니다. 환불된 주문의 상품을 회수해야 한다면, 지급한 주문의 OrderId를 서버에 보관해두고 주문 상태 조회 API로 주기적으로 확인해야 합니다.

false 는 언제 반환하나

공식 문서는 true가 아닌 응답에 대해 환불 안내 페이지가 노출될 수 있다고 안내합니다. (직접 측정한 것은 무응답 경로이며, 명시적 false에서도 같은 화면이 나오는지는 확인하지 않았습니다.) 따라서 false정말로 이 상품을 줄 수 없을 때만 씁니다 — 예를 들어 이미 보유한 비소모품을 결제 도중 다른 기기에서 획득한 경우처럼, 지급이 불가능하다고 지금 단정할 수 있을 때입니다.

"확신이 없으니 일단 false"는 성립하지 않습니다. 매 결제마다 환불 안내가 뜨는 앱이 되기 때문입니다. 확신은 1~3단계로 확보하는 것이지 false로 확보하는 것이 아닙니다.

참고: 구버전 토스앱에서는 반환값이 무시됩니다. processProductGrant를 지원하지 않는 버전(Android 5.231.1 미만 / iOS 5.230.0 미만)에서는 브릿지가 구 결제 경로로 폴백하며, 이때 콜백의 반환값은 플랫폼에 전달되지 않고 버려집니다. 반환값에 의존하는 로직을 짤 때 이 구간을 염두에 두세요.

에러 처리

API 호출이 실패하면 AITException이 throw됩니다.

속성
타입
설명

Message

string

사람이 읽을 수 있는 오류 메시지

ErrorCode

string

오류 코드. 플랫폼이 주지 않으면 빈 문자열

APIName

string

실패한 API 이름. 알 수 없으면 빈 문자열

IsPlatformUnavailable

bool

플랫폼 브릿지 부재로 인한 오류인지 여부

ErrorCode로 분기하려면 값이 비어 있을 수 있다는 점을 감안하세요.

IsPlatformUnavailable

이 플래그는 별도 필드로 전달되는 것이 아니라 에러 메시지를 보고 판정합니다. 아래 문자열 중 하나라도 들어 있으면 true가 됩니다.

판정 문자열
언제

__GRANITE_NATIVE_EMITTER

네이티브 이미터가 없음

ReactNativeWebView

토스 앱 WebView 바깥에서 실행 중

is not a constant handler

그 API의 브릿지 핸들러가 없음

Cannot read properties of undefined

window.AppsInToss가 아직 초기화되지 않음

true라면 코드 버그가 아니라 실행 환경 문제입니다. 일반 브라우저나 개발 환경에서 흔히 발생하므로, 에러 리포팅에 올릴 때는 이 케이스를 낮은 심각도로 내리거나 걸러내는 편이 낫습니다.

실행 환경별 동작

환경
동작

WebGL 빌드 + Apps in Toss 앱

실제 네이티브 API 호출

WebGL 빌드 + 일반 브라우저

대부분 실패. devtools가 켜져 있으면(Dev Server) mock으로 응답

Unity Editor

Editor mock 호출

그 외 플랫폼 (Windows, macOS 등)

Editor mock 호출

Editor mock은 빌드 프로필과 무관합니다. Runtime/SDK/의 각 API가 #if UNITY_WEBGL && !UNITY_EDITOR로 갈라져 있어, WebGL 빌드가 아니면 컴파일 시점에 mock 경로만 남습니다.

필요하면 실행 환경으로 분기할 수 있습니다.

실제 네이티브 동작을 확인하려면 WebGL로 빌드해 Apps in Toss 앱에서 실행해야 합니다. Editor에서는 무엇을 해도 mock입니다.

Mock

"Mock"이라는 이름으로 불리는 것이 두 가지이고, 서로 다르게 동작합니다.

Editor mock
devtools

어디에

Runtime/SDK/의 C#

@apps-in-toss/devtools(npm 패키지, 빌드 산출물을 브라우저에서 열 때 동작)

무엇을

모든 SDK API

60개 이상의 SDK API + 상태를 조작하는 플로팅 패널

언제

WebGL 빌드가 아닐 때 (컴파일 시점 결정)

Dev Server로 실행한 빌드를 일반 브라우저에서 열 때

어떻게 끄나

끌 수 없음

AIT > Configuration의 devtools 설정, 또는 서버 실행 시 환경 변수 AIT_DEVTOOLS=0

Editor mock

Unity Editor와 비 WebGL 플랫폼에서 API를 호출하면 로그를 남기고 기본값을 돌려줍니다. 예외를 던지지 않으므로 Editor에서 게임 로직이 멈추지 않습니다.

반환 타입
Mock 반환값

string

빈 문자열 ""

bool

false

배열

빈 배열

클래스 타입

default, 즉 null

구독 취소 Action

로그만 남기는 함수. SafeAreaInsetsSubscribenull

클래스 타입이 null로 온다는 점이 중요합니다. Editor에서 result.SomeField를 바로 읽으면 NullReferenceException이 납니다. Editor에서도 돌려볼 로직이라면 null 체크를 넣으세요. 배열을 돌려주는 API는 빈 배열이 오므로 foreach가 안전합니다.

devtools

@apps-in-toss/devtools@apps-in-toss/web-framework 3.x 전용 개발 도구입니다. Dev Server를 실행하면 vite 플러그인이 @apps-in-toss/web-framework import를 mock 구현으로 alias해, 토스 앱 없이 일반 브라우저에서 60개 이상의 SDK API가 mock으로 동작합니다. 동시에 화면에 플로팅 패널이 떠서 로그인 상태·광고 결과·스토리지 값 같은 mock 상태를 직접 조작할 수 있습니다.

패널은 기본으로 켜져 있습니다. devtools 전체(또는 패널만)를 끄려면 AIT > Configuration의 devtools 설정을 바꾸세요 — 빌드 산출물은 그대로이므로 서버 재시작만으로 반영됩니다. CI나 임시 확인처럼 설정을 건드리지 않고 한 번만 끄고 싶다면 서버 실행 환경 변수 AIT_DEVTOOLS=0으로 오버라이드할 수 있습니다.

devtools가 꺼져 있는 상태(예: 일반 브라우저에서 열되 devtools를 비활성화한 경우)에서 SDK API를 부르면 IsPlatformUnavailabletrueAITException이 납니다.

관련 문서

도움이 되었나요?