- 마지막 업데이트
결제
이해하기
결제 지원 개요
스토브는 PC 온라인 결제와 모바일 마켓 결제, 두 가지 결제 종류를 지원해요.
결제 종류에 따라 지원 통화·결제 수단·한도·취소 방식이 달라요.
| 결제 종류 | 설명 |
|---|---|
| PC 온라인 결제 | PC 버전 게임의 웹상점·인게임 상점 결제 (스토브 제공 결제창 이용). |
| 모바일 마켓 결제 | 구글·애플 등 앱 마켓 출시 게임의 결제 (마켓 제공 IAP(인앱 결제) 결제창 이용). |
결제 계정 본인 확인
ㆍ 최소한의 본인 확인이 된 계정만 결제 가능
ㆍ 한국: 본인 인증 / 글로벌: 이메일 인증
스팀런처로 배포한 게임의 결제
스팀런처로 실행되는 게임의 결제도 PC 온라인 결제에 속하지만, 결제창이 스토브 결제 웹뷰가 아니라 스팀 결제 오버레이로 대체돼요. 자세한 동선은 개발하기 → PC IAP → 스팀 결제 를 참고하세요.
결제 지원 통화
| 분류 | 통화 | 설명 | 이 통화로 결제하는 계정 |
|---|---|---|---|
| PC 온라인 결제 | KRW | 대한민국 원 | 한국 가입 계정 |
| USD | 미국 달러 | 미국 가입 계정, 그리고 지원 통화가 없는 국가의 가입 계정 | |
| JPY | 일본 엔 | 일본 가입 계정 | |
| EUR | 유로 | 유로를 쓰는 지정 국가의 가입 계정 (하단 목록 참조) | |
| THB | 태국 바트 | 태국 가입 계정 | |
| PHP | 필리핀 페소 | 필리핀 가입 계정 | |
| TWD | 대만 달러 | 대만 가입 계정 | |
| 모바일 마켓 결제 | - | 마켓 결제 지원 통화 | 이용자의 각 마켓 계정에 설정된 지원 통화로 결제 |
결제 통화 유의사항
ㆍ 불가리아: USD 결제·포인트 적립 제공
ㆍ 같은 상품의 결제 통화를 상점과 빌링 시스템에 동일하게 등록해야 결제돼요 (예: 상점 EUR·빌링 USD처럼 다르게 등록하면 결제 불가)
지원 결제 수단
| 분류 | 국가 | 설명 |
|---|---|---|
| PC 온라인 결제 | 한국 | 스토브 페이(기본): 계좌이체·신용카드 간편결제, 1회 결제 한도가 크고 포인트 적립도 많음 그 외: 카카오페이·네이버페이·토스·페이코 등 간편결제, 휴대폰, 상품권류 |
| 글로벌 | 주요 결제수단: PayPal, Credit/Debit Card, ApplePay 등. 국가별 주력 결제수단 추가 제공 (예: 일본 PayPay) | |
| 모바일 마켓 결제 | 모든 국가 | 이용자가 마켓 계정에 등록한 결제 수단으로 결제 (수단 구성은 마켓이 결정하며 스토브·입점사가 관여할 수 없음) |
결제사(PG) 이중화
ㆍ 주요 결제수단은 2개 이상 PG 연동 → 장애 시 자동 전환
미성년자 결제
ㆍ 상품권류만 기본 이용, 전체 수단은 보호자 동의 필요
ㆍ 보호자 동의: 본인인증으로 20세 이상 나이 차 확인 (보호자는 회원이 아니어도 무방, 유효 12개월)
결제 한도
게임에서 결제할 수 있는 금액에는 매월 게임별 한도가 있으며, 국가·플랫폼에 따라 기준이 달라요.
| 분류 | 국가 | 설명 |
|---|---|---|
| PC 온라인 결제 | 한국 | 문화부 자율규제에 따라 PC 온라인 게임 한도 적용 ㆍ 성인: 10만원~3천만 원 자가 설정 (월 3회, 휴대폰 본인 인증 후 변경) ㆍ 설정한 한도는 명의자(주민번호) 기준으로 합산되며, 매월 게임별로 적용 ㆍ 미성년자: 월 7만원 한도, 변경 불가 |
| 글로벌 | 기본 한도 없음 (단, 결제수단 자체 한도는 있을 수 있음) ㆍ 게임별 1회·1일·1개월 한도를 통화별 설정 가능 |
|
| 모바일 마켓 결제 | 모든 국가 | 결제 한도 없음 (단, 결제수단 자체 한도는 있을 수 있음) |
| PC 온라인 + 마켓 결제 | 일본 | 연령 구간별 구매 한도 일본에 서비스하는 게임에 적용 (전자금융거래법에 따른 사실상의 규제) ㆍ 멀티플랫폼 게임은 PC·모바일마켓 구매 금액 합산 ㆍ 15세 이하: 월 5천엔 ㆍ 16세~17세: 월 3만엔 ㆍ 18세 이상: 제한 없음 ㆍ ※ 2022년 4월 일본 민법상 성인 연령 만 20세 → 만 18세 변경 |
일본 결제 한도 산정 방식
ㆍ 당월 1일~말일 누적 "엔화" 결제 금액 합산 (쿠폰·포인트 제외)
ㆍ 정회원 PC: 가입 국가가 일본인 경우 (일본 가입 = 엔화 결제)
ㆍ 정회원 모바일: 가입 국가가 일본이고 결제 통화가 엔화일 때만, 실제 엔화 결제액을 합산
ㆍ 게스트 PC: PC에는 게스트 회원이 없어 해당 없음
ㆍ 게스트 모바일: 가입 국가를 알 수 없어, 결제 통화가 엔화일 때만 실제 엔화 결제액을 합산
ㆍ 게임 내 모든 결제 금액을 합산 (재화성 상품뿐 아니라 전체 상품 포함)
ㆍ 구매하려는 상품 금액이 남은 한도를 넘으면 그 상품은 구매 불가 (남은 한도 안에서 살 수 있는 상품만 가능)
이용자별 특별 한도
ㆍ 특정 이용자(회원번호 기반)에 특별 한도 부여 가능 (마케팅·인플루언서 등)
ㆍ 게임 한도와 상충 시 이용자 특별 한도 우선 적용
결제 확인과 증빙
| 분류 | 설명 |
|---|---|
| PC 온라인 결제 | 스토브 플랫폼에서 확인 ㆍ 결제/구매 완료 시 이메일 안내 (정보 없으면 미발송) ㆍ 실시간: 본인 결제 내역 (보호자는 자녀 결제 내역도 확인) ㆍ 월간: 전월 본인 결제 내역 (보호자는 자녀 결제 내역도 확인) ㆍ '내정보'에서 결제 내역·적립 포인트 내역 확인 |
| 모바일 마켓 결제 | 각 마켓에서 확인 ㆍ 결제 통지 내역·인보이스·영수증으로 결제 정보 확인 |
현금 영수증 처리
ㆍ PC 온라인 결제: 결제사(PG) 처리, 국세청 홈페이지에서 확인 가능
ㆍ 모바일 마켓(구글 월렛): 스토브가 현금영수증 자동 발행
결제 취소
| 분류 | 설명 |
|---|---|
| PC 온라인 결제 | 상점·아이템 결제 (한국 결제 수단) 청약 철회 기간 경과·상품 가치 훼손 여부로 환불 판단 (기간은 게임별 상이) ㆍ 기간 이내 + 미사용: 원 결제 수단 승인 취소 ㆍ 기간 이내 + 사용: 부분 환불 ㆍ 기간 경과 + 미사용: 1000원과 10% 수수료 중 큰 금액 제하고 부분 환불 ㆍ 기간 경과 + 사용: 게임 정책에 따라 환불 제한 가능 상점·아이템 결제 (글로벌 결제 수단) ㆍ 승인 취소 기간 내: 원 결제 수단 승인 취소 ㆍ 승인 취소 기간 경과: 취소 불가 스토어 게임 ㆍ 구매 후 14일 이내 + 누적 플레이 2시간 미경과 시 구매 취소 가능 ㆍ 취소 시 스토브 캐시 환급, 환급 캐시는 별도 현금 환불 신청 가능 |
| 모바일 마켓 결제 | 청약 철회 기간 경과·상품 가치 훼손 여부로 환불 판단 |
빌링 시스템과 연동
스토브 SDK를 통해 게임에 결제(IAP) 기능을 연동하는 방법을 안내해요.
Google Play, Apple App Store 등 주요 마켓의 인앱 결제와 PC 온라인 결제를 통합 지원하며, 구독 상품 및 비정상 환불 처리까지 포함한 결제 흐름 전반을 제공해요.
빌링 시스템 구성
스토브 빌링 시스템은 클라이언트 SDK와 서버 미들웨어로 구성돼요.
| 시스템명 | 설명 |
|---|---|
| MultiPlatform-Middleware (크로스플랫폼 미들웨어) |
게임과 빌링(온라인/모바일) 통신을 담당하는 GATEWAY 서버. |
| IAP | 모바일 빌링 서버. |
| Bill-WEB API | 온라인 빌링 서버. |
| Game Server | 게임 서버. |

지원 기능
| 기능 | 설명 |
|---|---|
| 결제 완료 알림 | 결제(온라인/모바일) 완료 시 스토브 빌링이 게임 서버로 알림 전달. 미들웨어를 통해 유효성 확인 후 완료 응답 필요. |
| 결제 유효성 확인 | 수신된 결제 알림의 출처를 스토브 빌링에서 검증. BULK(리스트) 처리 지원. |
| 게임 캐시 잔액 확인 | 게임에서 관리하는 유료 캐시 잔액 조회. |
| 비정상 환불 처리 | 이용자가 마켓을 통해 직접 환불한 내역을 수집해 게임 서버로 전달. Google, Apple 마켓 지원. |
| 구독 상품 | 스토브 SDK v2.6.0 이상에서 Google Play, Apple App Store의 자동 갱신형 구독 상품 지원. |
사전 준비
정상적인 빌링 동작을 위해 파트너스에 아래 정보를 모두 등록해야 해요.
하나라도 누락되면 결제가 정상적으로 이루어지지 않아요.
| 구분 | 등록 내용 |
|---|---|
| 빌링 연동 정보 | 빌링 버전(V4)·서비스 타입·GUID 사용여부·결제 완료 알림 URL·캐시 조회 URL |
| 마켓별 IAP 정보 | Google·Apple 마켓 인증 정보 |
| 마켓 상품 정보 | 마켓별 상품 ID |
| 월드 정보 | Noti URL용 기본 월드(world_id) |
마켓 연동 정보 주의
ㆍ Apple IAP Key: 최초 다운로드 후 재다운로드 불가 (분실 시 재발급)
ㆍ Apple 공유 암호 재생성 시 사용 중인 모든 앱 정보 업데이트 필요
비정상 환불 처리
이용자가 아이템을 받은 뒤 운영자를 거치지 않고 마켓에서 직접 환불(취소)하면, 받은 아이템을 계속 쓰지 못하도록 막는 기능이에요.
스토브 빌링은 이를 위해 환불 확정 전 개입해 어뷰징을 막는 단계와 환불이 확정된 뒤 손실을 복구하는 단계, 2단계로 대응해요.
| 단계 | 목적 | 적용 마켓 |
|---|---|---|
| 1단계 환불 검토 |
마켓이 환불을 확정하기 전에 개입해, 이미 소비·판매한 상품의 환불(환불 어뷰징)을 방지 | Google Play, Apple App Store (Steam은 사전 피드백 절차가 없어 미지원) |
| 2단계 환불 재결제 |
1단계에서 거부 의견을 냈음에도 마켓이 환불을 강행 처리한 경우, 이용자가 다음 접속 시 재결제하도록 하여 손실을 복구 | Google Play, Apple App Store, Steam |
1단계에서 막지 못한 건이 2단계로 이어져요
마켓이 개발자(스토브) 의견을 반영하지 않고 환불을 강행 처리하면, 해당 건은 2단계(환불 재결제) 대상으로 넘어가요. 두 기능은 별개로 동작하지만 순차적으로 연결돼요.
비정상 환불 수집 대상
ㆍ 1단계(환불 검토): Google Play·Apple App Store 일반 상품 환불 요청 건
ㆍ 2단계(환불 재결제): Google Play·Apple App Store·Steam 환불 확정 건
ㆍ 모바일 구독 상품: 비정상 환불 판정 불가로 제외
ㆍ 게임 탈퇴 사용자도 환불 발생 시 정보 전달
개발하기
파트너스 및 콘솔 설정
- Google Console API 설정
- Google Play Console API 액세스 권한 얻기
- Google Cloud 프로젝트 생성 (프로젝트 만들기 / API 추가 / OAuth 동의 화면 설정 및 클라이언트 ID 생성)
- OAuth 2.0 Playground Refresh Token 생성
- 스토브 파트너스 빌링 설정 정보 입력
- 빌링 정보 입력 가이드
- 구글 플레이 빌링 정보 등록 (마켓 검증 키 / OAuth Client ID / OAuth Client Secret / Refresh Token / Bucket Name / Credential json)
- 애플 앱 스토어 빌링 정보 등록 (Issuer ID / Key ID / IAP Key / 공유 암호)
- 비 승인 환불 상품 내역 수신 설정
- iOS App Store 알림 설정 (QA·프로덕션 서버 URL / 버전 2 알림 변경)
- Android 알림 설정
SDK 연동
모바일 IAP
Android는 PlayBilling, iOS는 StoreKit을 사용해 결제 흐름을 처리해요. SDK가 마켓 호출과 스토브 빌링 서버 검증까지 연결해 주지만, 게임 서버 측 결제 알림(Noti) 수신·재검증·지급 처리는 별도로 구현해야 해요.
사전 준비
- 모듈 의존성
- Android (
build.gradle): 아래 저장소·의존성을 추가하세요. (릴리즈 최신 버전을 확인해서 적용하세요.)groovyrepositories { google() jcenter() maven { // SDK 2.6.1 부터 저장소 위치가 변경됐어요. url "https://externalnexus.iam0.com/repository/mvp" // Android Gradle Plugin 7.0 이상 사용 시 아래 옵션 추가 allowInsecureProtocol = true } mavenCentral() } dependencies { implementation 'com.stove:iap-google:2.6.1' } - iOS (Xcode Capabilities): Build Settings → Capabilities에서 In-App Purchase를 검색해 추가하고 활성 상태인지 확인하세요.
- Android (
- 파트너스 빌링 정보 등록: Google Play / Apple App Store 빌링 키·시크릿이 파트너스에 등록돼 있어야 해요. (
2. 연동 가이드 > Google Play 연동/Apple App Store 연동참고) - 결제 알림(Notification) URL 등록: 게임 서버 측 결제 알림 수신용 URL을 파트너스에 등록하세요. SDK 결제 완료 후 이 URL로 지급 요청이 전달돼요. (모바일은 최대 100회 재시도)
- 이용자 로그인 완료: 모든 IAP 호출은
Auth.login완료 이후에만 동작해요. 미로그인 상태에서 호출하면Auth.UnauthorizedError(30002)가 반환돼요. - 멀티캐릭터(다중 재화) 사용 시: 파트너스 SDK Config에서
iap_delivery_method(NOTI또는ALL),use_custom_billing_guid(true)를 설정하세요. 클라이언트는 IAP 초기화 직후 + 월드/캐릭터 선택 완료 시점에setCustomBillingGUID를 반드시 호출해야 해요. (코드는 아래 샘플 코드의 필수 멀티캐릭터(다중 재화) 관리 설정 단락 참고)
개발 흐름
- 초기화: 로그인 완료 이후
IAP.initialize를 호출하세요. 실패 시Auth.UnauthorizedError(30002)는 로그인 흐름으로 유도하고,IAPError.InitializeError(40003)면userInfo의responseCode(BillingClient.BillingResponseCode)와debugMessage로 마켓 환경 점검을 안내하세요. - 결제 리스너 등록: 초기화 성공 콜백 안에서
IAP.setListener로 결제 결과 콜백을 등록하세요. 정상 구매·취소·계정 불일치·검증 대기·지급 대기·마켓 오류가 모두 이 리스너로 전달돼요. - 상점 구성:
IAP.fetchProducts로 상품 목록을 조회하고, 응답IAPProduct의ProductState(Available/Waiting/Purchased) 값에 따라 구매 버튼 활성/비활성을 분기 처리하세요. - 구매 진행:
Available상품에 한해IAP.startPurchase를 호출하세요.Waiting은 진행 중이므로 버튼 비활성화,Purchased는 미지급 상태이므로IAP.flush로 재지급 처리하세요. - flush: 결제됐지만 미지급 상품이 있을 때 또는 앱 재실행 시
IAP.flush를 호출해 컨슘·재지급 요청을 진행하세요. 결과는setListener콜백으로 돌아와요. - (옵션) 부가 기능: developer payload 전달은
serviceOrderId인자로, 환불 내역 조회는IAP.handleVoidedPurchases, iOS App Store 프로모션 상품 결제는purchasePromotion을 사용하세요.
결제 검증은 게임 서버에서 수행하세요
SDK 콜백의 isSuccessful만으로 아이템을 지급하지 마세요. 클라이언트는 위변조 가능하므로 게임 서버가 결제 알림(Noti) 수신 후 스토브 결제 유효성 체크 API로 2차 검증한 뒤 재화/아이템을 지급하세요.
트러블슈팅
결제 검증 대기·지급 대기는 정상 흐름의 일부예요IAPError.PurchasedError(40006)와 IAPError.WaitingError(40001)는 결제·지급이 빌링 서버에서 비동기로 처리되는 정상 상태예요. 이용자에게 오류로 안내하지 말고 "결제가 처리 중이에요, 잠시 후 푸시로 알려드려요" 같은 진행 안내 화면으로 띄우세요. 일정 시간 후 빌링 서버에서 자동 처리되며 결과는 푸시로 전달돼요.
계정 불일치(SuccessButUserMissMatch, 40000) 처리
결제 검증이 끝나기 전 이용자가 계정을 변경하거나 월드를 이동했을 때 발생해요. 현재 로그인된 계정 기준으로 재로그인 흐름을 유도하고, 이전 결제 결과는 게임 서버 측 알림(Noti)으로 추후 처리됨을 안내하세요.
| 상황 | 원인 | 조치 |
|---|---|---|
Auth.UnauthorizedError (30002) | 로그인 미완료 또는 토큰 만료 | AuthUI.login 또는 Auth.login으로 재로그인 흐름을 진행한 뒤 IAP 호출을 다시 시도하세요. |
IAPError.InitializeError (40003) | Android 마켓(PlayBilling) 초기화 실패 | userInfo의 responseCode(BillingClient.BillingResponseCode)와 debugMessage를 확인해 스토어 앱 설치·로그인 상태·지원 마켓 여부를 점검하도록 이용자에게 안내하세요. |
IAPError.InvalidProduct (40004) | 임의로 만든 product 객체 또는 fetchProducts 결과 외 상품으로 결제 시도 | 반드시 IAP.fetchProducts로 받은 객체를 그대로 startPurchase에 전달하세요. 직접 인스턴스화한 product는 사용하지 마세요. |
IAPError.MarketError (40002) | 결제 시도 중 마켓 클라이언트 오류 | Android는 userInfo.responseCode(BillingResponseCode), iOS는 SKError.code로 분기해 이용자에게 안내하세요. 일시 오류면 재시도, 환경 문제면 마켓 앱·계정 점검 안내. |
iOS pay country code nil (40010) | Apple 계정의 결제 국가 정보 누락 | App Store 설정에서 국가/지역과 결제 정보를 등록하도록 안내하세요. |
서버 빌링 검증 실패 (30000 verify fail / 30015 sandbox transaction) | 결제 영수증 검증 실패 또는 LIVE 환경에서 테스트 계정 미등록 | LIVE 환경이면 파트너스에 테스트 계정 등록 여부를 확인하세요. 일반 케이스는 게임 서버의 영수증 재검증 로직을 점검하세요. |
이미 구독 중인 상품 (93106) | 동일 구독 상품을 이용자가 이미 보유 | 이용자에게 이미 구독 중임을 안내하고 구매 버튼을 비활성화하세요. |
| 결제 후 지급 누락 | 게임 서버 알림(Noti) URL 응답 실패 또는 처리 누락 | 파트너스에 등록된 알림 URL이 항상 200 응답을 주는지 확인하세요. 모바일은 최대 100회 재지급을 시도하므로 게임 서버 측 멱등(idempotent) 처리도 함께 구현하세요. |
| 멀티캐릭터에서 잘못된 캐릭터로 지급 | setCustomBillingGUID 미호출 또는 잘못된 시점에 호출 | IAP 초기화 후 + 월드/캐릭터 선택 완료 시점에 setCustomBillingGUID("{STOVE_guid}_{STOVE_character_no}")를 호출하세요. |
환불 내역 조회 UI에서 40303 반환 | 인증 토큰 만료 | OperationUI.HandleResult에 위임 후 Auth.logout으로 강제 로그아웃을 수행해 재로그인을 유도하세요. |
각 함수별 세부 ErrorCode 표는 아래 샘플 코드 안에 함께 포함돼 있어요.
샘플 코드
초기화
Market SDK(PlayBilling/StoreKit)를 초기화해요. 초기화가 완료돼야 상품 조회·구매 호출이 가능해요.
- 추천 시점: 로그인 성공 이후

#if UNITY_ANDROID
using Stove.StoveSDK.IAP.Google;
#endif
public void Initialize()
{
IAP.Initialize((Result result) =>
{
if (result.IsSuccessful)
{
SetListener(); // setListener 호출
}
else if (result.ErrorCode == Auth.UnauthorizedError)
{
//TODO : login
}
else
{
//InitializeError. Android Only
// see https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode
}
});
}
ErrorCodes
| Domain | ErrorCode | Description |
|---|---|---|
| com.stove.success | 0 | Success |
| com.stove.auth | 30002 | UnauthorizedErrorResult |
| com.stove.iap | 40003 | InitializeError. Android PlayBilling의 경우 userInfo의 'responseCode'와 'debugMessage' 참조 |
| com.stove.iap | 40010 | iOS에 한해서만 pay country code가 nil일 경우 |
| com.stove.server | 20001 | initialize fail on precheck |
| com.stove.server | 20002 | initialize fail on afterCheck |
| com.stove.server | 20003 | initialize fail on send tid |
| com.stove.server | 20004 | initialize fail on send tid : error parsing response |
setListener
IAP 초기화 성공 이후 결제 결과를 받을 리스너를 설정해요. flush의 처리 결과도 이 리스너로 전달돼요.
- 추천 시점: 초기화 완료 직후
product값이 있으면 구매 시도한 상품 상태를 응답받은 상태로 변경해요. 특정 상황에서Available이외의 상태가 올 수 있어요.
public void SetListener()
{
IAP.SetListener((Result result, IAPProduct product, IAPPurchaseDetail purchaseDetail) =>
{
if (result.IsSuccessful)
{
//구매 성공
}
else if (result.IsCanceled)
{
//구매 취소
}
else if (result.ErrorCode == IAPError.SuccessButUserMissMatchError)
{
//구매 성공 (계정 불일치)
//결제 검증이 완료되지 않은 상태에서 계정 변경 또는 월드 이동 시 발생 가능
}
else if (result.ErrorCode == IAPError.PurchasedError)
{
//결제 검증 대기 중 - 추후 빌링 서버에서 자동 처리, 푸시로 알림
}
else if (result.ErrorCode == IAPError.WaitingError)
{
//지급 대기 중 - 추후 자동 처리, 푸시로 알림
}
else if (result.ErrorCode == IAPError.MarketError)
{
//Android: BillingClient.BillingResponseCode
//iOS: SKError.Code
}
});
}
ErrorCodes
| Domain | ErrorCode | Description |
|---|---|---|
| com.stove.success | 0 | Success |
| com.stove.cancel | 10 | Canceled |
| com.stove.iap | 40000 | SuccessButUserMissMatch |
| com.stove.iap | 40001 | Waiting |
| com.stove.iap | 40002 | MarketError (Android: BillingResponseCode / iOS: 결제 실패) |
| com.stove.iap | 40006 | Purchased |
| com.stove.server | 10001 | invalid parameters |
| com.stove.server | 10004 | not exists noti-api |
| com.stove.server | 10007 | is not supported environment |
| com.stove.server | 30000 | verify fail |
| com.stove.server | 30006 | canceled transaction - verify fail |
| com.stove.server | 30007 | revoked transaction - verify fail |
| com.stove.server | 30011 | completed transaction |
| com.stove.server | 30012 | duplicate store receipt |
| com.stove.server | 30014 | failed transaction |
| com.stove.server | 30015 | sandbox transaction (LIVE에서 테스트 계정 미등록 시 발생) |
| com.stove.server | 40001 | supply server response is 'fail' |
| com.stove.server | 40005 | noti api call fail |
| com.stove.server | 40006 | cash api call fail |
| com.stove.server | 40007 | itembox api call fail |
상점 구성하기
마켓에서 상품을 조회해 상점을 구성해요. 조회 응답인 IAPProduct의 주요 필드는 다음과 같아요.

IAPProduct 필드
| 필드명 | 설명 | 예시 |
|---|---|---|
| ProductType | 상품 종류 (inapp/subs) | inapp |
| productIdentifier | 마켓 등록 상품ID (구매 요청 시 전달) | google_item01 |
| stoveProductId | 스토브 파트너스에 등록된 상품ID | stove_item01 |
| localizedTitle | 마켓 상품명 | 전투 방패 |
| localizedDescription | 마켓 상품 설명 | 상품 설명... |
| price | 가격 (노출용 금액) | ₩1,100 |
| priceCurrencyCode | 통화 코드 | KRW, JPY, USD |
| priceAmountMicros | 가격을 마이크로 단위 (÷1,000,000 = 실제 금액) | 1100000000 |
| ProductState | 상품 상태 | Available / Waiting / Purchased |
| isLimit | 구매 제한 여부 | false |
| subscriptionPeriod | 구독 기간 (ISO 8601) | P1M |
| freeTrialPeriods | 무료 체험 기간 (ISO 8601) | P7D |
권장 사항: 상품 상태별로 구매 버튼을 다르게 표시해요.
- Available: 구매 가능 상태
- Waiting: 결제 진행 대기 상태 (구매 시도 중)
- Purchased: 구매 완료했으나 지급이 안 된 상태 (flush로 재지급 처리)
#if UNITY_ANDROID
using Stove.StoveSDK.IAP.Google;
#endif
public void FetchProducts()
{
IAP.FetchProducts((Result result, List<IAPProduct> products) =>
{
if (result.IsSuccessful)
{
foreach (IAPProduct product in products)
{
IAPProduct.ProductState state = product.State;
if (state == IAPProduct.ProductState.Available) { /* 구매 가능 */ }
else if (state == IAPProduct.ProductState.Waiting) { /* 결제 중 */ }
else if (state == IAPProduct.ProductState.Purchased) { /* flush 필요 */ }
string localizedTitle = product.LocalizedTitle;
string price = product.Price;
}
}
else if (result.IsServerError) { /* 빌링 서버 오류 */ }
else { /* 네트워크 오류 */ }
});
}
ErrorCodes
| Domain | ErrorCode | Description |
|---|---|---|
| com.stove.success | 0 | Success |
| com.stove.server | 20000 | not exists value |
| com.stove.server | 21000 | No content |
| com.stove.server | 21001 | not exists game |
| com.stove.server | 21002 | Not exists marketCode |
| com.stove.base.network | 10001 | NoConnectionError |
| com.stove.base.network | 10002 | TimeoutError |
구매하기
상품의 ProductState에 따라 다르게 처리해요. Available인 경우만 startPurchase를 호출하고, Purchased는 flush로 재지급 처리해요.
#if UNITY_ANDROID
using Stove.StoveSDK.IAP.Google;
#endif
public void StartPurchase(IAPProduct product)
{
IAPProduct.ProductState state = product.State;
if (state == IAPProduct.ProductState.Waiting) { /* 결제 중 - 버튼 비활성화 */ }
else if (state == IAPProduct.ProductState.Available)
{
IAP.StartPurchase(product, (Result result) =>
{
if (result.IsSuccessful) { /* 결제 시작됨 */ }
else if (result.IsServerError)
{
if (result.ErrorCode == 93106) { /* 이미 구독 중인 상품 */ }
}
else if (result.ErrorCode == Auth.UnauthorizedError) { /* AuthUI.login */ }
else if (result.ErrorCode == IAPError.InvalidProductError) { /* fetchProducts 결과 사용 */ }
else { /* 네트워크 오류 */ }
});
}
else if (state == IAPProduct.ProductState.Purchased)
{
IAP.Flush(); //재지급 처리
}
}
ErrorCodes
| Domain | ErrorCode | Description |
|---|---|---|
| com.stove.success | 0 | Success |
| com.stove.auth | 30002 | UnauthorizedError |
| com.stove.iap | 40003 | InitializeError |
| com.stove.iap | 40004 | InvalidProduct |
| com.stove.iap | 40006 | PurchasedNeedFlush |
| com.stove.iap | 40008 | ListenerError |
| com.stove.iap | 40009 | InvalidUser |
| com.stove.iap | 40010 | iOS pay country code nil |
| com.stove.server | 20005 | initialize fail on already purchased split product |
| com.stove.server | 93102 | invalid product info |
| com.stove.server | 93103 | is not sales product |
| com.stove.server | 93104 | limit purchase |
| com.stove.server | 93106 | exists purchased subscript product |
| com.stove.server | 93107 | not subscript product |
| com.stove.server | 93108 | product is subscript and limit |
| com.stove.server | 93110 | over limit on purchase of payment amount |
| com.stove.base.network | 10001 | NoConnectionError |
| com.stove.base.network | 10002 | TimeoutError |
flush
결제는 됐지만 아직 지급 전인 상품의 컨슘 처리 및 상품 재지급 요청 용도예요. 처리 결과는 IAP.setListener에 설정된 리스너로 전달돼요.
public void Flush()
{
IAP.Flush();
// 결과는 setListener의 콜백으로 전달됨
}
결제 시 developer payload 설정
결제 호출 시 serviceOrderId를 설정하면 결제 응답에서 확인할 수 있어요.
⚠️ 주의
payload 값은 이용자의 일부 재지급 케이스(앱 삭제 등)에서 100% 전달이 보장되지 않아요.
지급에 영향을 주는 주요 정보로는 사용하지 않도록 주의하세요.
string serviceOrderId = "set your developer payload";
IAP.StartPurchase(product, serviceOrderId, null, (Result result) => { });
환불 내역 조회 UI
이용자의 마켓 환불 내역을 화면에 노출해요. 환불 결제가 필요한 경우 이 화면에서 처리할 수 있어요.
#if UNITY_ANDROID
using Stove.StoveSDK.IAP.Google;
#endif
public void HandleVoidedPurchases()
{
IAP.HandleVoidedPurchases((Result result) =>
{
if (result.IsSuccessful)
{
/** 게임 진입 가능 **/
}
else if (result.IsCanceled)
{
if (result.UserInfo != null && result.UserInfo.TryGetValue("userAction", out string userAction))
{
if (!string.IsNullOrEmpty(userAction) && userAction.Equals("customerSupport"))
{
ViewUI.CustomerSupport((Result result, Dictionary<string, string> dictionary) =>
{
HandleVoidedPurchases();
});
return;
}
}
HandleVoidedPurchases();
}
else
{
OperationUI.HandleResult(result, (Result operationResult) =>
{
if (result.ErrorCode == 40303)
{
Auth.Logout();
}
});
}
});
}
필수 멀티캐릭터(다중 재화) 관리 설정
필수 적용 대상
다음 중 하나라도 해당하면 멀티캐릭터 사용 설정이 필수예요.
- 다중 월드로 서비스되는 게임 (월드별 캐릭터·아이템 지급)
- 단일 서버지만 내부 캐릭터가 있고 캐릭터별 결제·지급이 필요한 경우
- 파트너스 월드 설정이 2개 이상인 경우
- 스토브 2차 재화 시스템과 결제(IAP) 기능을 함께 사용하는 경우
파트너스(SDK Config) 설정
파트너스 SDK Config > IAP에서 아래 항목을 설정해야 해요.
iap_delivery_method=NOTI또는ALL- ALL: 노티를 통해 게임 서버로 결제 결과 통보 + 스토브 CASH 시스템으로 재화 관리
- NOTI: 게임 자체 CASH 시스템 + 노티로 결제 결과 전달받아 직접 재화 지급
use_custom_billing_guid=true
클라이언트 작업 - 이용자 유니크 식별자 설정
- 설정 시점: SDK IAP 객체 생성(초기화) 직후, 상품 지급받을 이용자 설정(
setCustomBillingGUID)을 반드시 해야 해요. 단, 월드 또는 캐릭터가 선택된 이후에 설정하도록 주의하세요. - 설정 방식 (스토브 게임 캐릭터 기능 사용 시, 표준 권장 방식):
{STOVE_guid}_{STOVE_character_no}조합 형태로 설정해요. 이 방식으로 설정하더라도 결제 완료 후 빌링 서버 지급 요청 시character_no필드는 정상적으로 전달돼요.
안내사항 (2025.05.15)
신규 게임의 경우 '계정 선물하기' 등 다양한 기능에 유연하게 대응하기 위해, {STOVE_guid}_{STOVE_character_no} 조합 형태로 가이드가 변경됐어요. 본 설정은 모바일 환경에서만 적용돼요.
IAP.SetCustomBillingGUID("{STOVE_guid}_{STOVE_character_no}");
iOS Promoted
App Store 내 프로모션 상품 결제 연동이에요.
- iOS 11 이상부터 사용 가능
purchasePromotion호출 시점은 초기화·로그인 완료 이후
코드 예시 (iOS)
#import <SGSIAP/SGSIAP.h>
#import <StoreKit/StoreKit.h>
@interface AppDelegate () <SKPaymentTransactionObserver>
@property (nonatomic) NSString *promotionProductID;
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
return YES;
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
{
}
- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product {
//프로모션 아이템 정보를 저장
self.promotionProductID = product.productIdentifier;
return NO;
}
//프로모션 상품 저장 여부 체크
- (BOOL)isExistPromotion {
return (self.promotionProductID != nil);
}
//프로모션 상품 결제
- (void)purchasePromotion {
[SGSIAP fetchProductsWithCompletionHandler:^(SGSResult * _Nonnull result, NSArray<SGSIAPProduct *> * _Nullable products) {
if ([result isSuccessful]) {
for (SGSIAPProduct *product in products) {
if ([product.productIdentifier isEqualToString:self.promotionProductID]) {
[SGSIAP startPurchaseWithProduct:product completionHandler:^(SGSResult * _Nonnull result) {
if (result.isSuccessful) { /* 결제 성공 */ }
else { /* 결제 실패 */ }
}];
break;
}
}
} else if ([result isServerError]) {
//빌링 서버 오류
} else {
//네트워크 오류
}
}];
}
@end
PC IAP
사전 준비
- BaseSDK 초기화(
Base_Initialize)와 이용자 로그인 완료가 선행돼야 해요. 이어서IAP_Initialize(shopKey)로 IAPSDK를 초기화해요. - 게임 루프에서 콜백 러너(
Base_RunCallback())가 주기적으로 호출돼야 비동기 콜백이 동작해요. - 부모 창 지정이 필요하면
IAP_Initialize대신IAP_InitializeWithWndInfo를 사용해요. - 파트너스에 결제 알림(Notification) 수신 URL과 마켓별 빌링 정보(Google/Apple/Steam)가 등록돼 있어야 해요.
- 멀티캐릭터(다중 재화) 사용 시 게임 프로필 설정 API로 이용자 식별자를 먼저 설정해요.
- 스팀런처로 배포하는 게임은 외부플랫폼연동모듈(APIModule)로 스토브 인증을 마치고 PCSDK3가 기동된 상태여야 하며, 게임이 Steamworks SDK를 직접 연동·초기화해야 해요. 자세한 동선은 아래 스팀 결제 를 참고하세요.
개발 흐름
- 초기화:
Base_Initialize완료 후IAP_Initialize(shopKey)(부모 창이 필요하면IAP_InitializeWithWndInfo)로 IAP 모듈을 초기화해요. - 약관 동의 확인: 약관 동의 조회 API로 필수 약관 동의 여부를 조회해요. (
IAP_FetchTermsAgreement) 미동의 시 약관 동의 흐름을 진행해요. - 상점 구성: 카테고리 조회(
IAP_FetchShopCategories)와 상품 목록 조회(IAP_FetchProducts)로 상품 목록을 가져와 UI를 구성해요. - 구매 진행: 이용자 선택 시 구매 시작 API를 호출해요. (
IAP_StartPurchase/IAP_StartPurchaseEx—StovePCPurchaseOption.operation으로 결제 UI/종료 동작을 제어) - 구매 확정/검증: 결제 완료 후 게임 서버로 알림이 전달되면 구매 확정 API(
IAP_ConfirmPurchase)로 클라이언트 측 확정을 호출하고, 게임 서버에서는 결제 유효성 체크 API로 2차 검증을 수행해요. - 보관함/환불: 필요 시 인벤토리 조회(
IAP_FetchInventory)를 활용해요. 환불 내역 조회(IAP_FetchVoidedPurchases)도 필요 시 활용할 수 있어요. - 부가 동작: 게임해지(약관철회) UI는
IAP_WithdrawGame, 열린 결제 창 일괄 닫기는IAP_CloseAllPopups를 사용해요. - 정리: 게임 종료 직전에
IAP_UnInitialize()로 IAP 모듈을 정리한 뒤Base_UnInitialize()를 호출해요.
스팀런처 배포 게임은 4~7단계가 달라요
ㆍ 4~5단계: 결제창을 SDK가 띄우지 않고 스팀런처가 게임에 주입한 스팀 결제 오버레이로 처리되며, 결제 완료 신호도 SDK가 아니라 Steamworks 콜백으로 도착해요. 자동 구매 확정 동선이 없어 IAP_ConfirmPurchase 호출이 필수예요.
ㆍ 6~7단계: 환불 내역 조회(IAP_FetchVoidedPurchases)와 게임해지(IAP_WithdrawGame)는 스팀 동선에서 지원하지 않아요. 보관함 조회(IAP_FetchInventory)는 그대로 사용해요.
자세한 내용은 아래 스팀 결제 를 참고하세요.
스팀 결제
스팀런처로 배포·실행되는 게임의 결제 동선이에요. 스토브런처로만 배포한다면 이 단락은 건너뛰고 위 동선을 그대로 따르면 돼요.
이 동선은 게임이 외부플랫폼연동모듈(APIModule)로 스토브 인증을 마치고 PCSDK3가 기동된 상태를 전제로 해요. 모듈 연동과 스팀 게임 약관 동의는 스팀 연동 가이드에서 다루고, 이 단락은 그 이후의 결제 부분만 설명해요.
기본 PC 결제와 다른 점
| 구분 | 기본 PC 결제 | 스팀 결제 |
|---|---|---|
| 결제창 | SDK가 스토브 결제 웹뷰를 표시해요. | 스팀런처가 게임에 주입하는 스팀 결제 오버레이로 처리돼요. |
| 결제 옵션 | StovePCPurchaseOption.operation으로 결제 UI 표시 방식과 자동 확정 여부를 제어해요. |
옵션 값이 적용되지 않아요. 어떤 옵션값을 넣어도 자동을 DEFAULT 옵션값으로 처리되어요. |
| 결제 완료 신호 | IAP_StartPurchase 콜백으로 전달돼요. |
Steamworks의 MicroTxnAuthorizationResponse_t 콜백으로 도착해요. |
| 구매 확정 | WITH_WEBVIEW_AND_CONFIRM_RESULT면 SDK가 자동으로 확정해요. |
자동 확정이 없어 IAP_ConfirmPurchase 호출이 필수예요. |
| 콜백 펌프 | Base_RunCallback() 하나면 돼요. |
Base_RunCallback()과 SteamAPI_RunCallbacks()를 함께 돌려야 해요. |
동선
- 상점 구성:
IAP_FetchShopCategories로 카테고리를,IAP_FetchProducts로 상품 목록을 조회해 상점 UI를 구성해요. 여기까지는 기본 동선과 같아요. - 구매 시작: 이용자가 상품을 고르면
IAP_StartPurchase를 호출해요. 상품 정보(productId·salePrice·quantity)만 채우고,StovePCPurchaseOption은 기본값 그대로 두세요. - 두 가지가 동시에 일어나요: ①
IAP_StartPurchase콜백으로 거래 마스터 번호(transactionMasterNumber) 가 도착하고, ② 스팀런처가 게임 위에 스팀 결제 오버레이를 띄워요. 두 동작의 순서는 보장되지 않으니, 콜백에서 받은 거래 마스터 번호를 변수에 보관해 두세요. - 이용자 결제: 이용자가 오버레이에서 구매를 승인하거나 취소해요. 이 화면은 스팀이 그리므로 개발사가 만들 UI는 없어요.
- 결과 수신: 승인·취소 결과는 SDK가 아니라 Steamworks의
MicroTxnAuthorizationResponse_t콜백으로 도착해요. 콜백 등록과 처리는 게임이 연동한 Steamworks SDK 영역이에요. - 구매 확정: 승인된 경우 보관해 둔 거래 마스터 번호로
IAP_ConfirmPurchase를 호출해요. 스팀 동선에는 자동 확정이 없어서 이 호출을 건너뛰면 아이템이 지급되지 않아요. - 지급 확인:
IAP_FetchInventory로 지급된 아이템을 확인해요.
실제 구현 코드는 아래 샘플 코드 → 스팀 결제(스팀런처 배포 게임) 를 참고하세요. 같은 샘플 코드 안의 (1)~(3) operation 예제는 스토브 결제창 동선이므로 스팀 동선에는 해당하지 않아요.
결제 오버레이 화면
이용자가 구매를 시작하면 스팀런처가 게임 위에 아래와 같은 구매 승인 화면을 띄워요. 상품명·수량·금액은 스토브에 등록된 상품 정보를 백엔드가 스팀에 전달해 구성돼요.

스팀 결제 동선에서 사용하지 않는 기능
| 기능 | 스팀 결제 동선에서의 처리 |
|---|---|
약관 동의IAP_FetchTermsAgreement |
사용하지 않아요. 스토브 플랫폼 약관 동의용 기능이고, 스팀 게임 약관 동의는 외부플랫폼연동모듈 동선에서 이미 처리돼요. |
게임 탈퇴(약관 철회)IAP_WithdrawGame |
지원하지 않아요. |
환불 내역 조회IAP_FetchVoidedPurchases |
지원하지 않아요. 환불 내역은 개발사 서버에서 서버 간(Server to Server) API로 연동할 예정이에요. |
구매 확정(IAP_ConfirmPurchase) 호출이 필수예요
스팀 결제 동선에는 SDK 자동 확정(WITH_WEBVIEW_AND_CONFIRM_RESULT)에 해당하는 경로가 없어요. 스팀 오버레이에서 결제가 승인돼도 IAP_ConfirmPurchase를 호출하지 않으면 구매가 확정되지 않아 아이템이 지급되지 않아요.
콜백 값은 거래 마스터 번호만 사용해요
결제 API 콜백이 돌려주는 값들은 스토브 자체 결제에 필요한 정보라, 스팀 결제 동선에서는 거래 마스터 번호(transactionMasterNumber)만 유효하고 나머지는 기본값(false·0·빈 배열·빈 문자열)으로 내려와요. 결제 URL·구매 진행 상태(purchaseProgress)·구매 상품 배열·과금 정보·구매 여부 플래그는 참조하지 마세요. 샘플 코드의 DEFAULT 예제에 나오는 NOT_NEED_PAYMENT_WINDOW 분기도 스팀 동선에서는 필요 없어요. 성공·실패 판정은 CallbackResult만 보고 하세요.
콜백 펌프는 두 개를 함께 돌려요
SDK 콜백 러너(Base_RunCallback())와 Steamworks 콜백 펌프(SteamAPI_RunCallbacks())는 서로 별개예요. 둘 중 하나라도 호출되지 않으면 결제 동선이 중간에 멈춰요.
트러블슈팅
| 상황 | 원인 | 해결 방법 |
|---|---|---|
| 게임 진입 직후 IAP 기능이 동작하지 않아요 | BaseSDK 초기화(Base_Initialize)나 이용자 로그인이 끝나지 않은 상태에서 IAP API를 호출했어요. | Base_Initialize 완료 후 IAP_Initialize로 IAPSDK를 초기화하고 로그인을 마친 뒤 IAP API를 호출해야 해요. |
| 결제를 시작했는데 약관 미동의 응답이 와요 | 스토브 결제 약관에 동의하지 않은 이용자라 결제가 차단된 상태예요. 신규 이용자나 약관 개정 직후에 자주 발생해요. | 약관 동의 조회 API(IAP_FetchTermsAgreement)로 약관 미동의 여부를 먼저 확인하고, 필수 약관에 미동의면 약관 동의 팝업을 띄워야 해요. 동의 완료 후 결제 흐름으로 되돌아오게 만들면 문제없어요. |
| 상품 조회가 빈 상품 목록을 반환해요 | 파트너스에 상품이 아직 등록되지 않았거나, 등록은 됐지만 마켓 키(상품 ID)가 게임에서 사용 중인 값과 일치하지 않아요. 빌드별로 상품 ID를 다르게 운영하다 발생하는 경우가 많아요. | 파트너스 빌링 콘솔에서 상품 등록 상태와 마켓 키를 확인하고, 게임 클라이언트가 동일한 상품 ID를 보내는지 확인해야 해요. 환경(개발/스테이지/라이브)별 상품 ID가 분리돼 있다면 빌드 환경과 매칭되는지 점검하면 문제없어요. |
| 결제 진행 중에 결제 창이 갑자기 닫혀요 | IAP API를 새로 호출하면 기존에 떠 있던 결제·약관 창이 일괄 종료돼요. 두 개의 결제 흐름이 동시에 노출되는 것을 막기 위한 의도된 동작이에요. | 한 번에 하나의 IAP 흐름만 진행하도록 결제 시작 시 게임 측에서 추가 호출을 막아야 해요. 결제 종료 콜백을 받기 전까지 결제 버튼을 비활성화하면 문제없어요. |
| 결제 결과 콜백이 호출되지 않아요 | 게임 메인 루프에서 콜백 러너(Base_RunCallback())를 호출하지 않으면 결제 결과 이벤트가 게임에 전달되지 않아요. | 메인 루프에서 매 프레임 또는 일정 주기로 콜백 러너를 호출해야 해요. 결제 진행 중에는 게임 일반 루프와 동일하게 호출되고 있는지 확인하면 문제없어요. |
결제는 성공했는데 게임 내에서 아이템이 지급되지 않고 스토브에 50052(아이템 발송 실패) 오류가 기록돼요 | 결제는 스토브 → 게임 서버 결제 알림(웹훅) 전송 → 게임 서버 응답 수신 → 지급 처리로 완료돼요. 게임 서버 알림 URL 미등록·잘못된 경로, 게임 서버 응답 누락이 있으면 스토브가 통지 전송 실패로 판단해 50052 오류로 기록하고 지급이 누락돼요. | 파트너스 빌링 설정에서 결제 알림 URL이 정확히 등록돼 있는지 확인하고, 게임 서버는 알림 수신 직후 동기적으로 정상 응답을 회신하도록 구현해야 해요. 알림 URL과 서버 응답을 우선 점검하면 50052 오류 발생을 막을 수 있어요. |
| 멀티캐릭터 게임에서 다른 캐릭터로 아이템이 지급돼요 | 결제 진입 전에 게임 프로필 설정 API로 현재 캐릭터의 식별자(nickname_no)를 설정하지 않으면 스토브 서버가 어떤 캐릭터에게 지급해야 할지 알 수 없어요. | 캐릭터 선택 직후마다 게임 프로필 설정 API를 호출해 현재 캐릭터를 SDK에 갱신해야 해요. 캐릭터 변경 흐름이 있는 게임이면 변경 시점마다 다시 호출하면 문제없어요. |
| 스팀 결제 오버레이가 뜨지 않아요 | Steamworks SDK가 초기화되지 않았거나, 스팀 클라이언트에서 게임 내 오버레이가 비활성화돼 있거나, 파트너스에 등록된 스팀 마켓 정보가 실제 앱과 달라요. | Steamworks SDK 초기화와 스팀 클라이언트의 오버레이 설정을 먼저 확인하고, 파트너스 빌링에 등록된 스팀 마켓 정보가 실제 앱과 일치하는지 대조하면 문제없어요. |
| 스팀 오버레이에서 결제를 승인했는데 게임이 아무 반응이 없어요 | Steamworks 콜백 펌프(SteamAPI_RunCallbacks())가 돌지 않아 MicroTxnAuthorizationResponse_t가 게임에 전달되지 않았어요. | 게임 루프에서 SteamAPI_RunCallbacks()를 호출해야 해요. SDK 콜백 러너(Base_RunCallback())와는 별개라 둘 다 돌고 있는지 확인하면 문제없어요. |
| 스팀 결제는 성공했는데 아이템이 지급되지 않아요 | 스팀 동선에는 SDK 자동 구매 확정이 없는데 IAP_ConfirmPurchase를 호출하지 않았어요. | MicroTxnAuthorizationResponse_t에서 승인 결과를 받은 뒤 반드시 IAP_ConfirmPurchase를 호출해야 해요. |
| 구매를 확정하려는데 거래 마스터 번호가 없어요 | IAP_StartPurchase 콜백과 스팀 오버레이 노출은 순서가 보장되지 않는데, 콜백에서 받은 거래 마스터 번호를 보관하지 않았어요. | IAP_StartPurchase 콜백에서 거래 마스터 번호를 변수에 보관해 두고, Steamworks 콜백 시점에 꺼내 쓰면 문제없어요. |
결제 동작은 Operation으로 제어해요IAP_StartPurchase는 StovePCPurchaseOption.operation 값으로 결제 UI 표시 방식을 정해요. DEFAULT는 SDK가 결제 UI를 띄우지 않고 1회용 결제 URL(oneTimePaymentUrl)만 전달하므로 게임이 직접 열어 결제 후 IAP_ConfirmPurchase를 호출하고, WITH_WEBVIEW는 SDK 웹뷰로 결제 페이지를 열어요. WITH_WEBVIEW_AND_CONFIRM_RESULT는 결제 성공 시 SDK가 자동으로 확정하므로 별도 확정 호출이 필요 없어요. 팝업 닫힘 시점이 필요하면 onDestroy 콜백을 받는 Ex 버전을 사용해요.
결제 종료 이벤트가 필요하면 Ex 버전을 사용해요IAP_StartPurchase, IAP_StartPayment 등은 결제 시작 결과만 전달해요. 팝업 닫힘 시점에 게임 흐름을 재개해야 한다면 Ex 버전(IAP_StartPurchaseEx, IAP_StartPaymentEx, IAP_FetchTermsAgreementEx, IAP_FetchProductsEx, IAP_FetchVoidedPurchasesEx)을 호출해 종료 이벤트를 받으세요.
결제 유효성 검증은 게임 서버에서 수행해요
클라이언트의 구매 확정(IAP_ConfirmPurchase)만으로는 위변조 방지가 보장되지 않아요. 게임 서버는 결제 알림 수신 후 스토브 결제 유효성 체크 API로 2차 검증한 뒤 재화/아이템을 지급해야 해요.
샘플 코드
각 기능을 함수 단위로 나눠 예제를 제공해요. 전체 결제 동선은 초기화·약관 → 상점 구성 → 구매 → 검증 → 보관함 순서로 진행해요.
초기화와 종료
Base_Initialize 완료 후 IAP_Initialize(shopKey)(부모 창이 필요하면 IAP_InitializeWithWndInfo)로 IAPSDK를 초기화해요. 종료 시에는 IAP_UnInitialize를 호출한 뒤 Base_UnInitialize로 정리해요. BaseSDK 초기화/종료에 대한 자세한 내용은 기본 연동 가이드를 참고하세요.
#include "IAPSDK.h"
using namespace Stove::PCSDK;
using namespace Stove::PCSDK::Base;
using namespace Stove::PCSDK::IAP;
// 1) 초기화 — Base_Initialize 완료 이후 호출합니다.
std::wstring shopKey = L"YOUR_SHOP_KEY";
auto initResult = IAP_Initialize(shopKey.c_str());
// 부모 창 지정이 필요하면: IAP_InitializeWithWndInfo(shopKey.c_str(), mainWindowHandle);
if (!initResult.IsSuccessful())
{
// 초기화 실패 시 로직을 구현해 주세요.
return;
}
// 2) 종료 — IAP_UnInitialize 후 Base_UnInitialize 순서로 정리합니다.
IAP_UnInitialize();
// 이후 Base_UnInitialize() 호출
약관 동의
초기화 후에는 IAP_FetchTermsAgreement로 필수 약관 동의 여부를 먼저 확인해요. 미동의 시 약관 동의 흐름(전달된 URL 또는 SDK 웹뷰)을 노출해요.
#include "IAPSDK.h"
using namespace Stove::PCSDK;
using namespace Stove::PCSDK::Base;
using namespace Stove::PCSDK::IAP;
// 필수 약관 동의 여부 조회 (IAP_Initialize 완료 이후)
StovePCTermsOption termsOption;
termsOption.SetOperation(StovePCTermsOperation::WITH_WEBVIEW);
termsOption.SetWebviewMode(Base::WebViewMode::INTERNAL);
termsOption.SetWebviewRect(0, 0, 800, 600);
IAP_FetchTermsAgreement(&termsOption,
[](CallbackResult callbackResult, bool agreed, const wchar_t* url)
{
if (callbackResult.GetResult().IsSuccessful() && !agreed)
{
// 약관 동의 흐름을 노출해 주세요.
}
});
상점 구성
카테고리 조회(IAP_FetchShopCategories)로 상점 탭을, 상품 목록 조회(IAP_FetchProducts)로 각 카테고리의 아이템 목록을 가져와 UI를 구성해요. 상품 목록은 카테고리 ID, 페이지 번호, 페이지 크기로 조회하며, 카테고리 ID를 빈 값으로 전달하면 전체 범위에서 조회돼요.
#include "IAPSDK.h"
using namespace Stove::PCSDK;
using namespace Stove::PCSDK::Base;
using namespace Stove::PCSDK::IAP;
// 1) 카테고리 조회
IAP_FetchShopCategories(
[](CallbackResult callbackResult, StovePCShopCategory* categories, uint32_t categoriesSize)
{
if (callbackResult.GetResult().IsSuccessful())
{
// categories[0..categoriesSize) 로 상점 탭 UI 구성
}
});
// 2) 상품 목록 조회
StovePCFetchProductParam fetchParams;
fetchParams.SetCategoryId(L""); // 빈 값이면 전체 카테고리
fetchParams.SetPageNumber(1);
fetchParams.SetPageSize(20);
IAP_FetchProducts(&fetchParams,
[](CallbackResult callbackResult, StovePCProduct* products, uint32_t productsSize)
{
if (callbackResult.GetResult().IsSuccessful())
{
// products[0..productsSize) 로 상점 UI 구성
}
});
인게임 아이템 구매
구매는 IAP_StartPurchase/IAP_StartPurchaseEx로 시작해요. StovePCPurchaseOption.operation 값으로 결제 UI 표시 방식과 구매 검증 자동화 여부를 정해요. 아래는 operation별 예제이며, 권장 순서대로 안내해요.
(1) WITH_WEBVIEW_AND_CONFIRM_RESULT — 결제창 표시 + 구매 검증 자동 (권장)
SDK가 결제창(Webview) 표시, 결제 완료 감지, 구매 검증(ConfirmPurchase)까지 모두 자동으로 처리합니다. 별도의 ConfirmPurchase 호출이 필요하지 않으며(직접 호출하면 중복 확정), 구매 완료 결과는 StartPurchase 의 onFinished 콜백으로 전달됩니다. 0원 아이템(NOT_NEED_PAYMENT_WINDOW) 케이스도 SDK가 내부에서 자동 처리해 동일하게 onFinished 로 전달합니다.
#include "IAPSDK.h"
using namespace Stove::PCSDK;
using namespace Stove::PCSDK::Base;
using namespace Stove::PCSDK::IAP;
void IAP_StartPurchase_WITH_WEBVIEW_AND_CONFIRM_RESULT_Example()
{
// (1) 상품/파라미터 구성 (실제로는 사용자가 선택한 인게임 아이템 정보를 전달)
StovePCOrderProduct product1; product1.SetProductId(123); product1.SetSalePrice(1000); product1.SetQuantity(3);
StovePCOrderProduct product2; product2.SetProductId(456); product2.SetSalePrice(5000); product2.SetQuantity(2);
StovePCOrderProduct product3; product3.SetProductId(789); product3.SetSalePrice(39800); product3.SetQuantity(1);
StovePCStartPurchaseParam params;
params.CreateOrderProduct(3); // 주문 상품 개수만큼 내부 배열 생성
params.SetOrderProduct(0, &product1);
params.SetOrderProduct(1, &product2);
params.SetOrderProduct(2, &product3);
// (2) 옵션: SDK가 결제창 표시 + 결제 완료 감지 + ConfirmPurchase(서버 확정)까지 자동 처리
StovePCPurchaseOption option;
option.SetOperation(StovePCPurchaseOperation::WITH_WEBVIEW_AND_CONFIRM_RESULT);
option.SetWebviewMode(Base::WebViewMode::INTERNAL);
// SetWebviewRect 인자: (x, y, width, height). 정중앙은 (-1, -1).
option.SetWebviewRect(0, 0, 420, 700);
params.SetPurchaseOption(option);
Game::ShopUI::SetPurchaseButtonEnabled(false); // (개발사 Psudo 함수) 결제 진행 중 중복 클릭 방지
// (3) 결제 시작 — 결제창이 닫히므로 onDestroy 를 받는 Ex API 사용
// 이 동선은 0원 아이템 제품 (NOT_NEED_PAYMENT_WINDOW) 케이스까지 SDK가 내부에서 ConfirmPurchase 를
// 자동 처리해 그 결과를 onFinished 로 전달한다. 따라서 GetPurchaseProgress() 분기나
// IAP_ConfirmPurchase 수동 호출이 필요 없다(직접 호출하면 중복 확정이 된다).
IAP_StartPurchaseEx(¶ms,
// onFinished: 실제 구매 완료 처리 지점 (SDK가 ConfirmPurchase까지 끝낸 뒤 1회 호출)
[](CallbackResult callbackResult, StovePCPurchaseResult purchaseResult)
{
// (3-1) 결제 실패/취소 처리. 흐름이 정상 종료되지 못한 모든 경우가 여기로 온다(purchased 는 항상 false).
// 원인은 ResultCode 로 구분한다.
if (!callbackResult.GetResult().IsSuccessful())
{
if (callbackResult.GetResult().GetResultCode() == (uint32_t)SDKResultCode::WEBVIEW_CLOSED_BEFORE_PURCHASE)
{
// 사용자가 결제를 완료하지 않고 결제창을 닫음(결제 취소). 에러가 아닌 의도적 취소로 처리한다.
}
else
{
// 그 외 오류: 결제창 진입 실패(VIEWUI_NOT_INITIALIZED / WEBVIEW_CREATE_FAIL / WEBVIEW_LOAD_URL_FAIL),
// 파라미터 오류, REST 네트워크/서버 오류, ConfirmPurchase 서버 API 실패 등.
}
return; // 어느 경우든 아이템 지급 없음
}
// (3-2) 여기가 실제 구매 완료 시점.
// SDK가 ConfirmPurchase 까지 끝낸 결과가 purchaseResult 에 담겨 있으므로,
// IsPurchased()==true 이면 여기서 바로(그리고 여기서만) 아이템을 지급한다.
// 0원 아이템 제품 (NOT_NEED_PAYMENT_WINDOW) 케이스도 purchased=true 로 동일하게 들어와 함께 처리된다.
if (purchaseResult.IsPurchased())
{
// 구매 완료 지급 시점. 여기에서 purchasedProducts[0..purchasedProductSize) 를 인벤토리/재화에 반영하는 코드를 적용한다.
// transactionMasterNumber 는 거래마다 고유(Unique)한 값이다. 게임 서버를 운영하는 개발사는
// 이 값을 기준으로 서버 측 중복 지급을 방지할 수 있고, UI 에서도 중복 처리(중복 지급 노출 등)를
// 방지하는 키로 사용할 수 있다.
for (uint32_t i = 0; i < purchaseResult.GetPurchasedProductCount(); ++i)
{
const StovePCPurchasedProduct* product = purchaseResult.GetPurchasedProduct(i);
int64_t detailNo = product->GetTransactionDetailNumber(); // 구매 상세 번호(구매 상품 TID)
int64_t productId = product->GetProductId(); // 플랫폼 상품 아이디
const wchar_t* categoryId = product->GetCategoryId(); // 카테고리 아이디
int32_t totalQty = product->GetTotalQuantity(); // 상품 총 판매수량
int32_t memberQty = product->GetMemberQuantity(); // 회원 구매 수량
int32_t guidQty = product->GetGuidQuantity(); // guid 구매 수량
// 구매한 아이템 별로 처리 구현
}
// 결제에 사용된 재화/결제수단 정보(StovePCChargeInfo). 영수증 표시·정산 로깅 등에 활용.
for (uint32_t i = 0; i < purchaseResult.GetChargeInfoCount(); ++i)
{
const StovePCChargeInfo* charge = purchaseResult.GetChargeInfo(i);
int32_t type = charge->GetChargeType(); // 98:STOVE 캐시, 99:포인트, 그 외:PG 결제 수단
const wchar_t* typeName = charge->GetChargeTypeName(); // 결제 수단 이름
double deductVal = charge->GetChargeDeductVal(); // 결제 가격
double displayVal = charge->GetChargeDisplayDeductVal(); // 결제 가격의 캐시 전환 가격
// 결제 수단별 처리 구현
}
}
else
{
// 흐름은 정상(IsSuccessful()==true)이나 서버가 결제 완료를 확인하지 않은 상태.
// (ConfirmPurchase 가 HTTP 200 이지만 서버 응답 status=false 인 드문 케이스 = 결제 상태 미확정)
// 아이템을 지급하지 않고, 서버 측 구매 상태 재조회 또는 고객지원 처리로 넘긴다.
// (사용자 취소·결제 실패는 여기가 아니라 위 (3-1) IsSuccessful()==false 로 처리된다.)
}
},
// onDestroy: 결제창 팝업이 닫혔을 때 호출.
[](CallbackResult callbackResult)
{
// UI 정리 전용 콜백. (완료 판정/지급은 위 onFinished에서 처리)
Game::ShopUI::SetPurchaseButtonEnabled(true); // (개발사 Psudo 함수) 잠가둔 구매 버튼 재활성화
Game::ShopUI::HideLoadingSpinner(); // (개발사 Psudo 함수) 로딩/오버레이 해제
});
}
(2) DEFAULT — 결제창 미표시 (개발사가 직접 결제 팝업 구현)
SDK가 결제 웹뷰를 띄우지 않습니다. 게임이 StartPurchase 결과로 받은 결제 URL로 자체 결제 페이지를 직접 표시하고, 결제 완료를 감지한 시점에 ConfirmPurchase 를 수동 호출해 구매를 검증합니다. 구매 완료 결과는 ConfirmPurchase 콜백으로 전달됩니다.
#include "IAPSDK.h"
using namespace Stove::PCSDK;
using namespace Stove::PCSDK::Base;
using namespace Stove::PCSDK::IAP;
// IAP_ConfirmPurchase 결과 콜백 (DEFAULT / NOT_NEED_PAYMENT_WINDOW 공용)
// status == true 인 이 시점이 실제 구매 완료 시점이다.
static void OnConfirmPurchase(CallbackResult callbackResult, bool status,
StovePCPurchasedProduct* purchasedProducts, uint32_t purchasedProductSize,
StovePCChargeInfo* chargeInfos, uint32_t chargeInfoSize)
{
if (callbackResult.GetResult().IsSuccessful() && status)
{
// 구매 완료 지급 시점. 여기에서 purchasedProducts[0..purchasedProductSize) 를 인벤토리/재화에 반영하는 코드를 적용한다.
// transactionMasterNumber 는 거래마다 고유(Unique)한 값이다. 게임 서버를 운영하는 개발사는
// 이 값을 기준으로 서버 측 중복 지급을 방지할 수 있고, UI 에서도 중복 처리(중복 지급 노출 등)를
// 방지하는 키로 사용할 수 있다.
for (uint32_t i = 0; i < purchasedProductSize; ++i)
{
const StovePCPurchasedProduct* product = &purchasedProducts[i];
int64_t detailNo = product->GetTransactionDetailNumber(); // 구매 상세 번호(구매 상품 TID)
int64_t productId = product->GetProductId(); // 플랫폼 상품 아이디
const wchar_t* categoryId = product->GetCategoryId(); // 카테고리 아이디
int32_t totalQty = product->GetTotalQuantity(); // 상품 총 판매수량
int32_t memberQty = product->GetMemberQuantity(); // 회원 구매 수량
int32_t guidQty = product->GetGuidQuantity(); // guid 구매 수량
// 구매한 아이템 별로 처리 구현
}
// 결제에 사용된 재화/결제수단 정보(StovePCChargeInfo). 영수증 표시·정산 로깅 등에 활용.
for (uint32_t i = 0; i < chargeInfoSize; ++i)
{
const StovePCChargeInfo* charge = &chargeInfos[i];
int32_t type = charge->GetChargeType(); // 98:STOVE 캐시, 99:포인트, 그 외:PG 결제 수단
const wchar_t* typeName = charge->GetChargeTypeName(); // 결제 수단 이름
double deductVal = charge->GetChargeDeductVal(); // 결제 가격
double displayVal = charge->GetChargeDisplayDeductVal(); // 결제 가격의 캐시 전환 가격
// 결제 수단별 처리 구현
}
}
else if (!callbackResult.GetResult().IsSuccessful())
{
// ConfirmPurchase 호출 자체가 실패(네트워크/서버 오류, 파라미터 오류 등). 아이템 지급 금지 + 실패 로그/재시도/문의 안내.
}
else
{
// 호출은 성공(IsSuccessful()==true)했으나 서버가 결제 완료를 확인하지 않음(status==false).
// 사용자가 결제를 완료하지 않고 결제창을 닫은 취소, 또는 결제 미확정 상태가 여기에 해당한다.
// 아이템 지급 금지. 필요 시 서버 측 구매 상태 재조회/안내.
}
}
void IAP_StartPurchase_DEFAULT_Example()
{
// (1) 상품/파라미터 구성 (실제로는 사용자가 선택한 인게임 아이템 정보를 전달)
StovePCOrderProduct product1; product1.SetProductId(123); product1.SetSalePrice(1000); product1.SetQuantity(3);
StovePCOrderProduct product2; product2.SetProductId(456); product2.SetSalePrice(5000); product2.SetQuantity(2);
StovePCOrderProduct product3; product3.SetProductId(789); product3.SetSalePrice(39800); product3.SetQuantity(1);
StovePCStartPurchaseParam params;
params.CreateOrderProduct(3); // 주문 상품 개수만큼 내부 배열 생성
params.SetOrderProduct(0, &product1);
params.SetOrderProduct(1, &product2);
params.SetOrderProduct(2, &product3);
// (2) 옵션: DEFAULT — SDK가 결제 웹뷰를 띄우지 않는다(게임이 직접 결제 URL을 표시).
// SDK 결제창이 없으므로 SetWebviewMode / SetWebviewRect 설정은 무의미하다(생략).
StovePCPurchaseOption option;
option.SetOperation(StovePCPurchaseOperation::DEFAULT);
params.SetPurchaseOption(option);
// (3) 결제 시작.
// DEFAULT 는 SDK 결제창이 없어 onDestroy 콜백이 호출되지 않지만(내부적으로 POPUP_NOT_CREATED 가
// 발생하나 Cpp 래퍼가 가로채 사용자 onDestroy 를 호출하지 않음), API 통일을 위해 IAP_StartPurchaseEx 를 사용한다.
IAP_StartPurchaseEx(¶ms,
// onFinished: "결제 URL 발급" 시점 (구매 완료 아님)
[](CallbackResult callbackResult, StovePCPurchaseResult purchaseResult)
{
// (3-1) 결제 시작 성공 여부. 실패면 URL 발급도 안 된 것. 사용자 실패 안내 후 종료.
if (!callbackResult.GetResult().IsSuccessful())
return;
// (3-2) 결제창이 필요 없는 케이스(0원 결제 등). 이미 결제 완료 상태.
// 확정 지급 목록을 받기 위해 ConfirmPurchase 로 마무리한다. (완료 결과는 OnConfirmPurchase)
if (purchaseResult.GetPurchaseProgress() == PurchaseProgress::NOT_NEED_PAYMENT_WINDOW)
{
IAP_ConfirmPurchase(purchaseResult.GetTransactionMasterNumber(), OnConfirmPurchase);
return;
}
// (3-3) 이 onFinished 는 "결제 URL이 발급됐다"는 신호일 뿐 구매 완료가 아니다.
// 게임이 이 URL로 자체 결제 페이지를 띄워 사용자 결제를 진행시키고,
// ConfirmPurchase 호출까지 거래번호(txnNo)를 보관한다.
const wchar_t* paymentUrl = purchaseResult.GetOneTimePaymentUrl();
int64_t txnNo = purchaseResult.GetTransactionMasterNumber();
Game::SavePendingTransaction(txnNo); // (개발사 Psudo 함수) 거래번호 보관
Game::OpenOwnPaymentPage(paymentUrl, txnNo); // (개발사 Psudo 함수) 자체 결제 페이지 표시
},
// onDestroy: DEFAULT 는 SDK 결제창이 없어 이 콜백이 호출되지 않는다.
[](CallbackResult callbackResult)
{
});
}
// (4) 게임이 자체 결제 페이지에서 결제 완료를 감지한 시점에 호출. 구매 완료 결과는 OnConfirmPurchase 로 들어온다.
// 주의: onFinished(URL 발급) 시점에 호출하지 않는다. 그 시점은 아직 사용자가 결제하기 전이다.
void Game_ConfirmOwnPurchase()
{
int64_t txnNo = Game::TakePendingTransaction(); // (개발사 Psudo 함수) 보관한 거래번호 회수
if (txnNo != 0)
IAP_ConfirmPurchase(txnNo, OnConfirmPurchase);
}
(3) WITH_WEBVIEW — 결제창 표시 + 구매 검증 수동
SDK가 결제창(Webview)은 띄우지만 ConfirmPurchase 는 자동 호출하지 않습니다. 게임이 결제 완료를 확신할 수 있는 명시적 시점에 ConfirmPurchase 를 직접 호출해 구매를 검증해야 합니다. 구매 완료 결과는 ConfirmPurchase 콜백으로 전달됩니다.
onDestroy(결제창 닫힘)는 화면 정리 이벤트일 뿐 결제 완료 신호가 아닙니다. 구매 검증을 onDestroy 에 묶지 마세요.
#include "IAPSDK.h"
using namespace Stove::PCSDK;
using namespace Stove::PCSDK::Base;
using namespace Stove::PCSDK::IAP;
// IAP_ConfirmPurchase 결과 콜백 (WITH_WEBVIEW / NOT_NEED_PAYMENT_WINDOW 공용)
// status == true 인 이 시점이 실제 구매 완료 시점이다.
static void OnConfirmPurchase(CallbackResult callbackResult, bool status,
StovePCPurchasedProduct* purchasedProducts, uint32_t purchasedProductSize,
StovePCChargeInfo* chargeInfos, uint32_t chargeInfoSize)
{
if (callbackResult.GetResult().IsSuccessful() && status)
{
// 구매 완료 지급 시점. 여기에서 purchasedProducts[0..purchasedProductSize) 를 인벤토리/재화에 반영하는 코드를 적용한다.
// transactionMasterNumber 는 거래마다 고유(Unique)한 값이다. 게임 서버를 운영하는 개발사는
// 이 값을 기준으로 서버 측 중복 지급을 방지할 수 있고, UI 에서도 중복 처리(중복 지급 노출 등)를
// 방지하는 키로 사용할 수 있다.
for (uint32_t i = 0; i < purchasedProductSize; ++i)
{
const StovePCPurchasedProduct* product = &purchasedProducts[i];
int64_t detailNo = product->GetTransactionDetailNumber(); // 구매 상세 번호(구매 상품 TID)
int64_t productId = product->GetProductId(); // 플랫폼 상품 아이디
const wchar_t* categoryId = product->GetCategoryId(); // 카테고리 아이디
int32_t totalQty = product->GetTotalQuantity(); // 상품 총 판매수량
int32_t memberQty = product->GetMemberQuantity(); // 회원 구매 수량
int32_t guidQty = product->GetGuidQuantity(); // guid 구매 수량
// 구매한 아이템 별로 처리 구현
}
// 결제에 사용된 재화/결제수단 정보(StovePCChargeInfo). 영수증 표시·정산 로깅 등에 활용.
for (uint32_t i = 0; i < chargeInfoSize; ++i)
{
const StovePCChargeInfo* charge = &chargeInfos[i];
int32_t type = charge->GetChargeType(); // 98:STOVE 캐시, 99:포인트, 그 외:PG 결제 수단
const wchar_t* typeName = charge->GetChargeTypeName(); // 결제 수단 이름
double deductVal = charge->GetChargeDeductVal(); // 결제 가격
double displayVal = charge->GetChargeDisplayDeductVal(); // 결제 가격의 캐시 전환 가격
// 결제 수단별 처리 구현
}
}
else if (!callbackResult.GetResult().IsSuccessful())
{
// ConfirmPurchase 호출 자체가 실패(네트워크/서버 오류, 파라미터 오류 등). 아이템 지급 금지 + 실패 로그/재시도/문의 안내.
}
else
{
// 호출은 성공(IsSuccessful()==true)했으나 서버가 결제 완료를 확인하지 않음(status==false).
// 사용자가 결제를 완료하지 않고 결제창을 닫은 취소, 또는 결제 미확정 상태가 여기에 해당한다.
// 아이템 지급 금지. 필요 시 서버 측 구매 상태 재조회/안내.
}
}
void IAP_StartPurchase_WITH_WEBVIEW_Example()
{
// (1) 상품/파라미터 구성 (실제로는 사용자가 선택한 인게임 아이템 정보를 전달)
StovePCOrderProduct product1; product1.SetProductId(123); product1.SetSalePrice(1000); product1.SetQuantity(3);
StovePCOrderProduct product2; product2.SetProductId(456); product2.SetSalePrice(5000); product2.SetQuantity(2);
StovePCOrderProduct product3; product3.SetProductId(789); product3.SetSalePrice(39800); product3.SetQuantity(1);
StovePCStartPurchaseParam params;
params.CreateOrderProduct(3); // 주문 상품 개수만큼 내부 배열 생성
params.SetOrderProduct(0, &product1);
params.SetOrderProduct(1, &product2);
params.SetOrderProduct(2, &product3);
// (2) 옵션: WITH_WEBVIEW — SDK 결제 웹뷰는 띄우지만 ConfirmPurchase 는 자동 호출하지 않는다.
StovePCPurchaseOption option;
option.SetOperation(StovePCPurchaseOperation::WITH_WEBVIEW);
option.SetWebviewMode(Base::WebViewMode::INTERNAL); // 게임 창 내부에 표시
// SetWebviewRect 인자: (x, y, width, height). 정중앙은 (-1, -1).
option.SetWebviewRect(0, 0, 420, 700);
params.SetPurchaseOption(option);
// (3) 결제 시작 — 결제창이 닫히므로 onDestroy 를 받는 Ex API 사용
Game::ShopUI::SetPurchaseButtonEnabled(false); // (개발사 Psudo 함수) 결제 진행 중 중복 클릭 방지
IAP_StartPurchaseEx(¶ms,
// onFinished: "결제창 표시됨" 신호 (구매 완료 아님)
[](CallbackResult callbackResult, StovePCPurchaseResult purchaseResult)
{
// (3-1) 결제창 진입 성공 여부. 실패면 웹뷰가 안 뜬 것(UI 복구는 onDestroy에서).
if (!callbackResult.GetResult().IsSuccessful())
return;
// (3-2) 0원 아이템 구매와 같은 상황이므로 결제창이 열리지 않는다. ConfirmPurchase 로 확정 목록 수령.
if (purchaseResult.GetPurchaseProgress() == PurchaseProgress::NOT_NEED_PAYMENT_WINDOW)
{
IAP_ConfirmPurchase(purchaseResult.GetTransactionMasterNumber(), OnConfirmPurchase);
return;
}
// (3-3) 이 onFinished 는 웹뷰 URL 로드가 끝났다는 신호일 뿐 구매 완료가 아니다.
// SDK는 결제 완료를 통지하지 않으므로, 완료 결과를 받으려면 게임이 직접
// IAP_ConfirmPurchase 를 호출해야 한다. 거래번호를 보관해 둔다.
Game::SavePendingTransaction(purchaseResult.GetTransactionMasterNumber()); // (개발사 Psudo 함수) 거래번호 보관
},
// onDestroy: 결제창 팝업이 닫혔을 때. UI 정리 전용
[](CallbackResult callbackResult)
{
// (4) "웹뷰가 닫혔다"는 화면 이벤트(결제 성공/실패와 무관).
// 결제 판정, 지급, ConfirmPurchase 를 여기서 하지 않는다.
// (창 닫힘은 결제 완료가 아니고, 결제 후 창을 안 닫으면 호출되지도 않으므로)
Game::ShopUI::SetPurchaseButtonEnabled(true); // (개발사 Psudo 함수) 잠가둔 구매 버튼 재활성화
Game::ShopUI::HideLoadingSpinner(); // (개발사 Psudo 함수) 로딩/오버레이 해제
});
}
// (5) 게임이 결제 완료를 확신할 수 있는 명시적 시점(예: 결제 후 게임 복귀/구매내역 확인)에 호출.
// 주의: onDestroy(창 닫힘)에 묶지 않는다. 창 닫힘은 결제 완료 신호가 아니므로 위험.
void Game_ConfirmPendingPurchase()
{
int64_t txnNo = Game::TakePendingTransaction(); // (개발사 Psudo 함수) 보관한 거래번호 회수
if (txnNo != 0)
IAP_ConfirmPurchase(txnNo, OnConfirmPurchase); // 구매 완료 결과는 OnConfirmPurchase 로
}
스팀 결제 (스팀런처 배포 게임)
스팀런처로 배포한 게임의 결제 예제예요. StovePCPurchaseOption은 기본값 그대로 두고, 결제 완료 신호는 Steamworks의 MicroTxnAuthorizationResponse_t 콜백에서 받아 IAP_ConfirmPurchase로 확정해요. SDK가 결제창을 띄우지 않아 종료 이벤트가 없으므로 Ex 버전이 아닌 IAP_StartPurchase를 사용해요. Steamworks 콜백 등록과 SteamAPI_RunCallbacks() 호출은 게임이 연동한 Steamworks SDK 영역이라, 예제에서는 연결 지점만 표시했어요. 동선 전체 설명과 미지원 기능은 위 스팀 결제 를 참고하세요.
이 예제는 구매 확정·지급 확인까지 한 흐름으로 담았어요
스팀 동선은 IAP_ConfirmPurchase 호출이 필수이고 그 결과를 보관함으로 확인하는 것까지 이어지므로, 흐름이 끊기지 않도록 아래 구매 검증 과 보관함 조회 에서 따로 다루는 두 API를 이 예제에 함께 넣었어요. 각 API의 일반적인 사용법과 파라미터 설명은 해당 절을 참고하세요.
#include "IAPSDK.h"
using namespace Stove::PCSDK;
using namespace Stove::PCSDK::Base;
using namespace Stove::PCSDK::IAP;
// StartPurchase 콜백과 스팀 오버레이 노출은 순서가 보장되지 않는다.
// 콜백에서 받은 거래 마스터 번호를 보관해 두었다가 Steamworks 콜백 시점에 사용한다.
static int64_t g_PendingTxnNo = 0;
// (4) 구매 확정 결과 콜백
// 스팀 동선에서는 status / purchasedProducts / chargeInfos 가 기본값으로 내려온다.
// 따라서 성공 여부는 CallbackResult 로만 판정한다.
void OnConfirmPurchase(CallbackResult callbackResult, bool status,
StovePCPurchasedProduct* purchasedProducts, uint32_t purchasedProductsSize,
StovePCChargeInfo* chargeInfos, uint32_t chargeInfosSize)
{
if (!callbackResult.GetResult().IsSuccessful())
{
// 확정 실패 — 아이템 지급 금지. 실패 안내 또는 재시도 처리를 구현해 주세요.
return;
}
// 확정 완료. 지급 결과는 보관함 조회로 확인한다.
IAP_FetchInventory(
[](CallbackResult cb, StovePCInventoryItem* items, uint32_t itemsSize)
{
if (cb.GetResult().IsSuccessful())
{
// items[0..itemsSize) 로 보관함/재화 UI 갱신
}
});
}
// (1) 구매 시작 — PurchaseOption 은 설정하지 않는다(스팀 동선에서는 적용되지 않음).
void StartSteamPurchase()
{
StovePCOrderProduct product; product.SetProductId(123); product.SetSalePrice(1000); product.SetQuantity(1);
StovePCStartPurchaseParam params;
params.CreateOrderProduct(1);
params.SetOrderProduct(0, &product);
// 결제창은 스팀런처가 주입하므로 StovePCPurchaseOption 은 기본값 그대로 둔다.
Game::ShopUI::SetPurchaseButtonEnabled(false); // (개발사 Psudo 함수) 중복 클릭 방지
IAP_StartPurchase(¶ms,
[](CallbackResult callbackResult, StovePCPurchaseResult purchaseResult)
{
// 스팀 동선에서는 CallbackResult 로만 성공/실패를 판정한다.
if (!callbackResult.GetResult().IsSuccessful())
{
Game::ShopUI::SetPurchaseButtonEnabled(true); // (개발사 Psudo 함수)
return;
}
// (2) 거래 마스터 번호만 유효한 값이다. 결제 URL·구매 진행 상태 등 나머지는 기본값이므로
// DEFAULT 예제의 NOT_NEED_PAYMENT_WINDOW 분기는 여기서 필요 없다.
// 이 번호를 보관해 두었다가 스팀 결제 결과가 도착할 때 사용한다.
g_PendingTxnNo = purchaseResult.GetTransactionMasterNumber();
});
// 이 호출 전후로 스팀런처가 게임 위에 결제 오버레이를 띄운다(개발사 UI 구현 불필요).
}
// (3) 스팀 결제 결과 수신 — Steamworks 의 MicroTxnAuthorizationResponse_t 콜백에서 호출한다.
// 콜백 등록과 SteamAPI_RunCallbacks() 호출은 게임이 연동한 Steamworks SDK 영역이다.
void OnSteamMicroTxnAuthorized(bool authorized)
{
Game::ShopUI::SetPurchaseButtonEnabled(true); // (개발사 Psudo 함수)
if (!authorized || g_PendingTxnNo == 0)
{
// 이용자가 취소했거나 승인되지 않음 — 지급 없음
g_PendingTxnNo = 0;
return;
}
// 보관한 거래 마스터 번호로 구매를 확정한다. 스팀 동선에는 자동 확정이 없다.
IAP_ConfirmPurchase(g_PendingTxnNo, OnConfirmPurchase);
g_PendingTxnNo = 0;
}
구매 검증
WITH_WEBVIEW나 DEFAULT 모드, 그리고 스팀 결제 동선에서는 결제 완료 후 IAP_ConfirmPurchase로 클라이언트 측 구매를 확정해요. 거래 마스터 번호(transactionMasterNumber)를 전달하며, 게임 서버에서는 결제 유효성 체크 API로 2차 검증을 수행해야 해요.
#include "IAPSDK.h"
using namespace Stove::PCSDK;
using namespace Stove::PCSDK::Base;
using namespace Stove::PCSDK::IAP;
// StartPurchase 결과에서 얻은 transaction master number 로 구매를 확정합니다.
void IAP_ConfirmPurchase_Example(int64_t transactionMasterNumber)
{
IAP_ConfirmPurchase(
transactionMasterNumber,
[](CallbackResult callbackResult, bool isPurchased,
StovePCPurchasedProduct* purchasedProducts, uint32_t purchasedProductsSize,
StovePCChargeInfo* chargeInfos, uint32_t chargeInfosSize)
{
if (callbackResult.GetResult().IsSuccessful() && isPurchased)
{
// 게임 서버 2차 검증 후 아이템 지급 처리
}
else
{
// 구매 검증 실패 처리
}
});
}
보관함 조회
IAP_FetchInventory로 로그인한 이용자의 구매 이력을 조회해요. 별도 매개변수 없이 호출하며, 결과 배열로 보관함 UI를 구성해요.
#include "IAPSDK.h"
using namespace Stove::PCSDK;
using namespace Stove::PCSDK::Base;
using namespace Stove::PCSDK::IAP;
IAP_FetchInventory(
[](CallbackResult callbackResult, StovePCInventoryItem* inventoryItems, uint32_t inventoryItemSize)
{
if (callbackResult.GetResult().IsSuccessful())
{
// inventoryItems[0..inventoryItemSize) 로 보관함 UI 구성
}
});
게임 탈퇴와 팝업 정리
게임 탈퇴(약관 철회) UI는 IAP_WithdrawGame으로 표시해요. 열린 IAP 결제 창을 즉시 모두 닫아야 할 때는 IAP_CloseAllPopups를 호출해요.
#include "IAPSDK.h"
using namespace Stove::PCSDK;
using namespace Stove::PCSDK::Base;
using namespace Stove::PCSDK::IAP;
// 1) 게임 탈퇴(약관 철회) UI 표시
StovePCWithdrawGameOption withdrawOption;
withdrawOption.SetWebviewMode(Base::WebViewMode::INTERNAL);
withdrawOption.SetWebviewRect(0, 0, 800, 600);
IAP_WithdrawGame(&withdrawOption,
[](CallbackResult callbackResult, bool gameWithdraw)
{
if (callbackResult.GetResult().IsSuccessful() && gameWithdraw)
{
// 게임해지(약관철회) 성공 시 로직을 구현해 주세요.
}
},
[](CallbackResult callbackResult)
{
// 게임해지(약관철회) 팝업이 닫힌 후 로직을 구현해 주세요.
});
// 2) 열린 결제 창 일괄 닫기
IAP_CloseAllPopups();
서버 연동
사전 준비
- 정상적인 빌링 동작을 위해 파트너스에 아래 정보를 모두 등록해야 해요.
| 구분 | 파트너스 메뉴 위치 | 항목 | 내용 |
|---|---|---|---|
| 빌링 연동 정보 | [Billing] > [빌링 설정] > [빌링 연동 정보 관리] | 결제 완료 알림 URL | 결제 완료 후 결제 정보 전달 받을 게임서버 URL |
| 환불 내역 알림 URL | 마켓의 환불 정보 수신 후 전달 받을 게임서버 URL | ||
| 결제 상태 조회 URL | 마켓의 환불 요청을 승인 판단 시 아이템 회수 처리 할 게임서버 URL |
- 플랫폼 서버와 게임서버간 통신 확인
- 플랫폼 서버와 게임서버간 통신확인이 필요해요.
- 결제 완료 알림 수신 시 결제 유효성 체크 API 연동으로 결제 완료 유효성 체크가 필요해요.
개발 흐름
- SDK 연동으로 전달 받은 아이템 목록에서 결제 요청을 진행해요.
- 결제 요청 후 결제를 진행하면 SDK를 통해서 빌링 서버로 결제정보가 전달돼요.
- 빌링 서버에서는 결제 정보의 유효성을 판단한 후 결제 완료 알림 URL에 등록된 게임서버로 결제 완료 정보를 전달해요.
- 게임서버에서는 결제 정보 유효성 API를 확인한 후 아이템 지급 후 빌링 서버에 응답을 해줘요.
비정상 환불 대응
1단계: 환불 검토 및 아이템 회수
이용자가 마켓에 환불을 요청하면, 마켓이 환불을 확정하기 전에 스토브 빌링이 결제건의 소비·회수 상태를 마켓에 회신해 환불 어뷰징(이미 소비·판매한 상품의 환불)을 막아요. 이 단계의 마켓 회신(Apple Send Consumption Information / Google orders.reviewrefund)은 스토브 빌링이 자체적으로 처리하며, 게임 서버는 환불요청이 전달되어 호출되는 아이템 회수 요청 API만 구현하면 돼요.
마켓별 동작 방식
| 구분 | 애플 (App Store) | 구글 (Google Play) |
|---|---|---|
| 환불정보 수신 방식 | App Store Server Notifications V2 (SSN) | Real-time Developer Notifications (RTDN, Cloud Pub/Sub) |
| 수신 노티 타입 | CONSUMPTION_REQUEST | pendingRefundReviewNotification |
| 스토브 빌링의 응답 API | Send Consumption Information | orders.reviewrefund |
| 스토브 빌링의 응답 제한시간 | 12시간 | 48시간 |
응답 제한시간은 스토브 빌링 기준이에요
애플 12시간·구글 48시간은 스토브 빌링이 마켓에 소비 정보를 회신해야 하는 기한으로, 게임 서버가 별도로 대응할 부분은 없어요. 게임 서버는 아래 아이템 회수 요청 API만 정상 응답하면 돼요.
처리 흐름
아이템 회수 요청 API 응답 규격
게임 서버 구현 API
게임 서버는 아래 API를 구현해야 해요. (파트너스 > 빌링 설정에 등록하는 "아이템 회수 알림 URL"이 여기에 대응돼요.)
| 필드 | 값 | 설명 |
|---|---|---|
item_status | int | 아이템 회수 결과: 0 회수완료 / 1 회수못함 |
2단계: 환불 재결제
1단계(환불 검토)에서 거부 의견을 회신했음에도 마켓이 이를 반영하지 않고 환불을 강행 처리한 경우, 또는 애초에 1단계 대응이 불가능한 마켓(Steam)에서 환불이 발생한 경우에 적용돼요. 해당 거래는 비정상 환불로 간주되어, 이용자는 다음 게임 접속 시 해당 금액을 재결제해야 정상 이용이 가능해요.
마켓별 재결제 동작
| 환불이 발생한 마켓 | 구글 출시버전(모바일) | 애플 출시버전(모바일) | 스팀 클라이언트 | 스토브 PC 클라이언트 |
|---|---|---|---|---|
| 구글에서 환불 | 재결제 화면 노출 | 이용 제한 안내 | 이용 제한 안내 | 이용 제한 안내 |
| 애플에서 환불 | 이용 제한 안내 | 재결제 화면 노출 | 이용 제한 안내 | 이용 제한 안내 |
| 스팀에서 환불 | 이용 제한 안내 | 이용 제한 안내 | 재결제 화면 노출 | 이용 제한 안내 |
| 환불 내역 없음 | 정상 진입 | 정상 진입 | 정상 진입 | 정상 진입 |
재결제 화면·이용 제한 안내를 띄우는 시점은 게임마다 다를 수 있어요
로그인 성공 후, 월드 진입 후, 캐릭터 선택 시, 상점 진입 시, 구매 시도 시 등 운영 의도에 맞는 시점에 노출하면 돼요.
처리 흐름 — 접속 시 대상 여부 확인
처리 흐름 — 재결제 결제 진행 (모바일)
처리 흐름 — 재결제 결제 진행 (Steam)
이용 제한 안내 예시
| 게임 이용 제한 안내 마켓(Google, Apple, Steam 등)에서 환불하신 상품으로 인해 일시적으로 게임 이용이 제한되었어요. 환불하신 마켓의 게임에 접속해 해당 상품을 재결제하신 후 게임을 이용하실 수 있어요. 고객센터 문의 닫기 |
재결제 UI 구성 가이드
기본 재결제 화면은 개발사에 가이드로 제공되며, 실제 디자인·컨텐츠 구성은 각 개발사가 진행해요.
- 안내 문구: 각 게임에 맞추어 구성
- 재결제 버튼: SDK의
startPurchase함수 호출로 상품 결제 화면 노출 - 고객센터 문의 버튼: SDK의
customerSupport함수 호출로 문의하기 이동 - 새로고침 버튼: 재결제 대상 상품 목록 새로고침
트러블슈팅
| 상황 | 원인 | 해결 방법 |
|---|---|---|
| 결제 완료 후 아이템 지급이 실패해요 | 결제 완료 알림 URL로 빌링 서버에서 호출이 오지 않아요. | 게임서버와 빌링 서버간 네트워크 확인이 필요해요. |
| 결제 완료 후 마켓 검증이 실패해요. | 마켓별 설정 정보를 잘못 입력할 경우 발생해요. | 파트너스를 통해서 마켓 검증 정보를 수정한 후 최대 1시간 후에 다시 확인이 필요해요. |
샘플 코드
결제 완료 알림 처리
--header 'caller-id: {{caller-id}}'
온라인 일반상품
{
"bill_platform_type": "SHOP",
"noti_type": "ONLINE_PURCHASE",
"guid": "265265",
"txn_time" : 1644807685000,
"data": {
"tid": "1909091033503333452",
"product_id": "test_1",
"product_price": 5000.0,
"product_currency": "KRW",
"inservice_item_id": "test_1",
"service_order_id": "testtest_1234"
}
온라인 장바구니상품
{
"bill_platform_type": "SHOP",
"noti_type": "ONLINE_CART_PURCHASE",
"member_no": 11111
"guid": "265265",
"txn_time" : 1644807685000,
"data": {
"tid": "1909091033503333452",
"products":[
]
"product_id": "test_1",
"product_price": 5000.0,
"product_currency": "KRW",
"inservice_item_id": "test_1",
"service_order_id": "testtest_1234"
}
온라인 스팀 상품
{
"bill_platform_type": "SHOP",
"noti_type": "STEAM",
"guid": "265265",
"txn_time" : 1644807685000,
"data": {
"tid": "1909091033503333452",
"product_id": "test_1",
"product_price": 5000.0,
"product_currency": "KRW",
"inservice_item_id": "test_1",
"service_order_id": "testtest_1234"
}
모바일 일반상품 / OOAP 상품
{
"bill_platform_type": "MOBILE",
"noti_type": "IAP_PURCHASE",
"character_no": "67891",
"guid": "67891",
"world_id": "world_1",
"txn_time" : 1644807685000,
"data": {
"tid": "1909091033503333452",
"pay_type": "INAPP",
"market_code": "GOOGLE_PLAY",
"market_product_id": "google_test_1",
"product_id": "test_1",
"product_price": 5000.0,
"product_currency": "KRW",
"product_tier": 1,
"inservice_item_id": "test_1",
"service_order_id": "testtest_1234",
"supply_items": [
{
"service_item_code": "potion_h",
"total_amount": 2,
"item_desc": ""
}]
}
}
결제 유효성 체크
- 결제 유효성 체크
{
"code": 0,
"message": "OK",
"data": {
"tid": "T202202103125",
"products": [
{
"tid": 5128216388247371675,
"product_id": "123456",
"quantity": 1,
"product_price": 5000.00,
"product_currency": "KRW",
"txn_time": 1644489139000,
"inservice_item_id": "cp7892"
},
{
"tid": 5128216388247323435,
"product_id": "123457",
"quantity": 1,
"product_price": 55000.00,
"product_currency": "KRW",
"txn_time": 1644489139000,
"inservice_item_id": "cp7893"
}
]
}
}
...
결제 유효성 체크(BULK)
- 결제 유효성 체크 (BULK)
curl --location --request POST 'https://api.onstove.com/bill-cpm/v1.0/payment/{service_id}/details' \
--header 'Authorization: Bearer {{access_token}}'
--header 'caller-id: {{caller-id}}'
{
"details" : [
{
"tid": "T20220304125",
"noti_type": "IAP_PURCHASE ",
"bill_platform_type": "MOBILE",
"guid": "120552311123",
"member_no": 123456
},
{
"tid": "T20220304127",
"noti_type": "ONLINE_PURCHASE",
"bill_platform_type": "SHOP",
"guid": "120552311150",
"member_no": 123456
}
]
}
비정상 환불
- 비정상 환불 목록 수신
{
"data": {
"total_count": 2,
"list": [
{
"tid": 5675140483049940678,
"market_code": "STEAM",
"member_no": 100056315,
"guid": "100056315",
"character_no": 624462,
"inservice_item_id": "Super_Gem13000",
"market_item_id": "12345",
"market_tid": "123456778",
"market_user_id": "123455",
"purchase_dt": 1699845522000,
"voided_dt": 1623031862137
},
{
"tid": 3675140483049940678,
"market_code": "GOOGLE_PLAY",
"member_no": 100056315,
"guid": "100056315",
"character_no": 624462,
"inservice_item_id": "Super_Gem14000",
"market_item_id": "a_market_product_id_01",
"market_tid": null,
"market_user_id": null,
"purchase_dt": 1699845522000,
"voided_dt": 1623031862137
}
]
},
"code": 0,
"message": "OK"
}
아이템 회수 알림
- 모바일 (App Store / Google Play)
- Request
text
{ "tid" : 1609997628158350014, "guid": "200000006062", "market_code": "GOOGLE_PLAY", "product_id": "l_0001" } - Response
text
{ "code" : 0, "message":"성공", "data": { "decision":0, "item_status":0 } }
- Request
- 인게임 상점(PC), 스팀 상점
- Request
text
{ "tid": 1609997628158350014, "guid": "200000006062", "market_code": "ONLINE", "product_id": "l_0001" } - Response
text
{ "code": 0, "message": "성공", "data": { "decision": 0, "item_status": 0 } }
- Request
- 웹 상점
- Request
text
{ "tid": 1609997628158350014, "guid": "200000006062", "market_code": "ONLINE_CART", "items": [ { "tid": 1234567, "product_id": "l_0001" }, { "tid": 1234568, "product_id": "l_0002" } ] } - Response
text
{ "code": 0, "message": "성공", "data": { "decision": 0, "items": [ { "tid": 1234567, "item_status": 0 }, { "tid": 1234568, "item_status": 0 } ] } }
- Request