Skip to content
스토브
마지막 업데이트

실제 적용 흐름이 궁금하신가요?

이용 시나리오 / 게임에서 구매하기

결제

이해하기


결제 지원 개요

스토브는 PC 온라인 결제와 모바일 마켓 결제, 두 가지 결제 종류를 지원해요.
결제 종류에 따라 지원 통화·결제 수단·한도·취소 방식이 달라요.

결제 종류 설명
PC 온라인 결제 PC 버전 게임의 웹상점·인게임 상점 결제 (스토브 제공 결제창 이용).
모바일 마켓 결제 구글·애플 등 앱 마켓 출시 게임의 결제 (마켓 제공 IAP(인앱 결제) 결제창 이용).

결제 계정 본인 확인
ㆍ 최소한의 본인 확인이 된 계정만 결제 가능
ㆍ 한국: 본인 인증 / 글로벌: 이메일 인증

스팀런처로 배포한 게임의 결제
스팀런처로 실행되는 게임의 결제도 PC 온라인 결제에 속하지만, 결제창이 스토브 결제 웹뷰가 아니라 스팀 결제 오버레이로 대체돼요. 자세한 동선은 개발하기 → PC IAP → 스팀 결제 를 참고하세요.

결제 지원 통화

분류 통화 설명 이 통화로 결제하는 계정
PC 온라인 결제KRW대한민국 원한국 가입 계정
USD미국 달러미국 가입 계정, 그리고 지원 통화가 없는 국가의 가입 계정
JPY일본 엔일본 가입 계정
EUR유로유로를 쓰는 지정 국가의 가입 계정 (하단 목록 참조)
THB태국 바트태국 가입 계정
PHP필리핀 페소필리핀 가입 계정
TWD대만 달러대만 가입 계정
모바일 마켓 결제-마켓 결제 지원 통화이용자의 각 마켓 계정에 설정된 지원 통화로 결제

※ 유로 결제 적용 국가 목록 (2026년 기준)37개
국가 명코드 국가 명코드 국가 명코드
독일 (Germany)DE에스토니아 (Estonia)EE올란드 제도AX
프랑스 (France)FR라트비아 (Latvia)LV생 바르텔레미BL
이탈리아 (Italy)IT리투아니아 (Lithuania)LT프랑스령 기아나GF
스페인 (Spain)ES룩셈부르크 (Luxembourg)LU과들루프GP
포르투갈 (Portugal)PT키프로스 (Cyprus)CY세인트 마틴MF
네덜란드 (Netherlands)NL몰타 (Malta)MT마르티니크MQ
벨기에 (Belgium)BE크로아티아 (Croatia)HR생피에르 미클롱PM
오스트리아 (Austria)AT안도라 (Andorra)AD레위니옹RE
핀란드 (Finland)FI모나코 (Monaco)MC남극 프랑스령TF
아일랜드 (Ireland)IE산마리노 (San Marino)SM마요트YT
그리스 (Greece)GR바티칸 시국 (Vatican City)VA불가리아*BG
슬로바키아 (Slovakia)SK코소보 (Kosovo)XK
슬로베니아 (Slovenia)SI몬테네그로 (Montenegro)ME

결제 통화 유의사항
ㆍ 불가리아: 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 설정
      1. Google Play Console API 액세스 권한 얻기
      1. Google Cloud 프로젝트 생성 (프로젝트 만들기 / API 추가 / OAuth 동의 화면 설정 및 클라이언트 ID 생성)
      1. OAuth 2.0 Playground Refresh Token 생성
      1. 스토브 파트너스 빌링 설정 정보 입력

  • 빌링 정보 입력 가이드
    • 구글 플레이 빌링 정보 등록 (마켓 검증 키 / 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): 아래 저장소·의존성을 추가하세요. (릴리즈 최신 버전을 확인해서 적용하세요.)
      groovy
      repositories {
          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를 검색해 추가하고 활성 상태인지 확인하세요.
  • 파트너스 빌링 정보 등록: 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반드시 호출해야 해요. (코드는 아래 샘플 코드의 필수 멀티캐릭터(다중 재화) 관리 설정 단락 참고)
개발 흐름
  1. 초기화: 로그인 완료 이후 IAP.initialize를 호출하세요. 실패 시 Auth.UnauthorizedError(30002)는 로그인 흐름으로 유도하고, IAPError.InitializeError(40003)면 userInforesponseCode(BillingClient.BillingResponseCode)와 debugMessage로 마켓 환경 점검을 안내하세요.
  2. 결제 리스너 등록: 초기화 성공 콜백 안에서 IAP.setListener로 결제 결과 콜백을 등록하세요. 정상 구매·취소·계정 불일치·검증 대기·지급 대기·마켓 오류가 모두 이 리스너로 전달돼요.
  3. 상점 구성: IAP.fetchProducts로 상품 목록을 조회하고, 응답 IAPProductProductState(Available / Waiting / Purchased) 값에 따라 구매 버튼 활성/비활성을 분기 처리하세요.
  4. 구매 진행: Available 상품에 한해 IAP.startPurchase를 호출하세요. Waiting은 진행 중이므로 버튼 비활성화, Purchased는 미지급 상태이므로 IAP.flush로 재지급 처리하세요.
  5. flush: 결제됐지만 미지급 상품이 있을 때 또는 앱 재실행 시 IAP.flush를 호출해 컨슘·재지급 요청을 진행하세요. 결과는 setListener 콜백으로 돌아와요.
  6. (옵션) 부가 기능: 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) 초기화 실패userInforesponseCode(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)를 초기화해요. 초기화가 완료돼야 상품 조회·구매 호출이 가능해요.

  • 추천 시점: 로그인 성공 이후

csharp
#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

DomainErrorCodeDescription
com.stove.success0Success
com.stove.auth30002UnauthorizedErrorResult
com.stove.iap40003InitializeError. Android PlayBilling의 경우 userInfo의 'responseCode'와 'debugMessage' 참조
com.stove.iap40010iOS에 한해서만 pay country code가 nil일 경우
com.stove.server20001initialize fail on precheck
com.stove.server20002initialize fail on afterCheck
com.stove.server20003initialize fail on send tid
com.stove.server20004initialize fail on send tid : error parsing response



setListener

IAP 초기화 성공 이후 결제 결과를 받을 리스너를 설정해요. flush의 처리 결과도 이 리스너로 전달돼요.

  • 추천 시점: 초기화 완료 직후
  • product 값이 있으면 구매 시도한 상품 상태를 응답받은 상태로 변경해요. 특정 상황에서 Available 이외의 상태가 올 수 있어요.

csharp
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

DomainErrorCodeDescription
com.stove.success0Success
com.stove.cancel10Canceled
com.stove.iap40000SuccessButUserMissMatch
com.stove.iap40001Waiting
com.stove.iap40002MarketError (Android: BillingResponseCode / iOS: 결제 실패)
com.stove.iap40006Purchased
com.stove.server10001invalid parameters
com.stove.server10004not exists noti-api
com.stove.server10007is not supported environment
com.stove.server30000verify fail
com.stove.server30006canceled transaction - verify fail
com.stove.server30007revoked transaction - verify fail
com.stove.server30011completed transaction
com.stove.server30012duplicate store receipt
com.stove.server30014failed transaction
com.stove.server30015sandbox transaction (LIVE에서 테스트 계정 미등록 시 발생)
com.stove.server40001supply server response is 'fail'
com.stove.server40005noti api call fail
com.stove.server40006cash api call fail
com.stove.server40007itembox api call fail



상점 구성하기

마켓에서 상품을 조회해 상점을 구성해요. 조회 응답인 IAPProduct의 주요 필드는 다음과 같아요.


IAPProduct 필드

필드명설명예시
ProductType상품 종류 (inapp/subs)inapp
productIdentifier마켓 등록 상품ID (구매 요청 시 전달)google_item01
stoveProductId스토브 파트너스에 등록된 상품IDstove_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로 재지급 처리)

csharp
#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

DomainErrorCodeDescription
com.stove.success0Success
com.stove.server20000not exists value
com.stove.server21000No content
com.stove.server21001not exists game
com.stove.server21002Not exists marketCode
com.stove.base.network10001NoConnectionError
com.stove.base.network10002TimeoutError



구매하기

상품의 ProductState에 따라 다르게 처리해요. Available인 경우만 startPurchase를 호출하고, Purchasedflush로 재지급 처리해요.


csharp
#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

DomainErrorCodeDescription
com.stove.success0Success
com.stove.auth30002UnauthorizedError
com.stove.iap40003InitializeError
com.stove.iap40004InvalidProduct
com.stove.iap40006PurchasedNeedFlush
com.stove.iap40008ListenerError
com.stove.iap40009InvalidUser
com.stove.iap40010iOS pay country code nil
com.stove.server20005initialize fail on already purchased split product
com.stove.server93102invalid product info
com.stove.server93103is not sales product
com.stove.server93104limit purchase
com.stove.server93106exists purchased subscript product
com.stove.server93107not subscript product
com.stove.server93108product is subscript and limit
com.stove.server93110over limit on purchase of payment amount
com.stove.base.network10001NoConnectionError
com.stove.base.network10002TimeoutError



flush

결제는 됐지만 아직 지급 전인 상품의 컨슘 처리 및 상품 재지급 요청 용도예요. 처리 결과는 IAP.setListener에 설정된 리스너로 전달돼요.


csharp
public void Flush()
{
    IAP.Flush();
    // 결과는 setListener의 콜백으로 전달됨
}

결제 시 developer payload 설정

결제 호출 시 serviceOrderId를 설정하면 결제 응답에서 확인할 수 있어요.

⚠️ 주의
payload 값은 이용자의 일부 재지급 케이스(앱 삭제 등)에서 100% 전달이 보장되지 않아요.
지급에 영향을 주는 주요 정보로는 사용하지 않도록 주의하세요.


csharp
string serviceOrderId = "set your developer payload";
IAP.StartPurchase(product, serviceOrderId, null, (Result result) => { });

환불 내역 조회 UI

이용자의 마켓 환불 내역을 화면에 노출해요. 환불 결제가 필요한 경우 이 화면에서 처리할 수 있어요.


csharp
#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} 조합 형태로 가이드가 변경됐어요. 본 설정은 모바일 환경에서만 적용돼요.


csharp
IAP.SetCustomBillingGUID("{STOVE_guid}_{STOVE_character_no}");

iOS Promoted

App Store 내 프로모션 상품 결제 연동이에요.

  • iOS 11 이상부터 사용 가능
  • purchasePromotion 호출 시점은 초기화·로그인 완료 이후

코드 예시 (iOS)

objectivec
#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를 직접 연동·초기화해야 해요. 자세한 동선은 아래 스팀 결제 를 참고하세요.

개발 흐름
  1. 초기화: Base_Initialize 완료 후 IAP_Initialize(shopKey)(부모 창이 필요하면 IAP_InitializeWithWndInfo)로 IAP 모듈을 초기화해요.
  2. 약관 동의 확인: 약관 동의 조회 API로 필수 약관 동의 여부를 조회해요. (IAP_FetchTermsAgreement) 미동의 시 약관 동의 흐름을 진행해요.
  3. 상점 구성: 카테고리 조회(IAP_FetchShopCategories)와 상품 목록 조회(IAP_FetchProducts)로 상품 목록을 가져와 UI를 구성해요.
  4. 구매 진행: 이용자 선택 시 구매 시작 API를 호출해요. (IAP_StartPurchase/IAP_StartPurchaseExStovePCPurchaseOption.operation으로 결제 UI/종료 동작을 제어)
  5. 구매 확정/검증: 결제 완료 후 게임 서버로 알림이 전달되면 구매 확정 API(IAP_ConfirmPurchase)로 클라이언트 측 확정을 호출하고, 게임 서버에서는 결제 유효성 체크 API로 2차 검증을 수행해요.
  6. 보관함/환불: 필요 시 인벤토리 조회(IAP_FetchInventory)를 활용해요. 환불 내역 조회(IAP_FetchVoidedPurchases)도 필요 시 활용할 수 있어요.
  7. 부가 동작: 게임해지(약관철회) UI는 IAP_WithdrawGame, 열린 결제 창 일괄 닫기는 IAP_CloseAllPopups를 사용해요.
  8. 정리: 게임 종료 직전에 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()를 함께 돌려야 해요.

동선

  1. 상점 구성: IAP_FetchShopCategories로 카테고리를, IAP_FetchProducts로 상품 목록을 조회해 상점 UI를 구성해요. 여기까지는 기본 동선과 같아요.
  2. 구매 시작: 이용자가 상품을 고르면 IAP_StartPurchase를 호출해요. 상품 정보(productId·salePrice·quantity)만 채우고, StovePCPurchaseOption은 기본값 그대로 두세요.
  3. 두 가지가 동시에 일어나요: ① IAP_StartPurchase 콜백으로 거래 마스터 번호(transactionMasterNumber) 가 도착하고, ② 스팀런처가 게임 위에 스팀 결제 오버레이를 띄워요. 두 동작의 순서는 보장되지 않으니, 콜백에서 받은 거래 마스터 번호를 변수에 보관해 두세요.
  4. 이용자 결제: 이용자가 오버레이에서 구매를 승인하거나 취소해요. 이 화면은 스팀이 그리므로 개발사가 만들 UI는 없어요.
  5. 결과 수신: 승인·취소 결과는 SDK가 아니라 Steamworks의 MicroTxnAuthorizationResponse_t 콜백으로 도착해요. 콜백 등록과 처리는 게임이 연동한 Steamworks SDK 영역이에요.
  6. 구매 확정: 승인된 경우 보관해 둔 거래 마스터 번호로 IAP_ConfirmPurchase를 호출해요. 스팀 동선에는 자동 확정이 없어서 이 호출을 건너뛰면 아이템이 지급되지 않아요.
  7. 지급 확인: 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_StartPurchaseStovePCPurchaseOption.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 초기화/종료에 대한 자세한 내용은 기본 연동 가이드를 참고하세요.

cpp
#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 웹뷰)을 노출해요.

cpp
#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를 빈 값으로 전달하면 전체 범위에서 조회돼요.

cpp
#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 로 전달합니다.

cpp
#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(&params,
        // 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 콜백으로 전달됩니다.

cpp
#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(&params,
        // 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 에 묶지 마세요.

cpp
#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(&params,
        // 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의 일반적인 사용법과 파라미터 설명은 해당 절을 참고하세요.

cpp
#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(&params,
        [](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_WEBVIEWDEFAULT 모드, 그리고 스팀 결제 동선에서는 결제 완료 후 IAP_ConfirmPurchase로 클라이언트 측 구매를 확정해요. 거래 마스터 번호(transactionMasterNumber)를 전달하며, 게임 서버에서는 결제 유효성 체크 API로 2차 검증을 수행해야 해요.

cpp
#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를 구성해요.

cpp
#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를 호출해요.

cpp
#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 연동으로 결제 완료 유효성 체크가 필요해요.

개발 흐름

  1. SDK 연동으로 전달 받은 아이템 목록에서 결제 요청을 진행해요.
  2. 결제 요청 후 결제를 진행하면 SDK를 통해서 빌링 서버로 결제정보가 전달돼요.
  3. 빌링 서버에서는 결제 정보의 유효성을 판단한 후 결제 완료 알림 URL에 등록된 게임서버로 결제 완료 정보를 전달해요.
  4. 게임서버에서는 결제 정보 유효성 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_REQUESTpendingRefundReviewNotification
스토브 빌링의 응답 APISend Consumption Informationorders.reviewrefund
스토브 빌링의 응답 제한시간12시간48시간

응답 제한시간은 스토브 빌링 기준이에요
애플 12시간·구글 48시간은 스토브 빌링이 마켓에 소비 정보를 회신해야 하는 기한으로, 게임 서버가 별도로 대응할 부분은 없어요. 게임 서버는 아래 아이템 회수 요청 API만 정상 응답하면 돼요.


처리 흐름

아이템 회수 요청 API 응답 규격


게임 서버 구현 API

게임 서버는 아래 API를 구현해야 해요. (파트너스 > 빌링 설정에 등록하는 "아이템 회수 알림 URL"이 여기에 대응돼요.)

필드설명
item_statusint아이템 회수 결과: 0 회수완료 / 1 회수못함
2단계: 환불 재결제

1단계(환불 검토)에서 거부 의견을 회신했음에도 마켓이 이를 반영하지 않고 환불을 강행 처리한 경우, 또는 애초에 1단계 대응이 불가능한 마켓(Steam)에서 환불이 발생한 경우에 적용돼요. 해당 거래는 비정상 환불로 간주되어, 이용자는 다음 게임 접속 시 해당 금액을 재결제해야 정상 이용이 가능해요.

마켓별 재결제 동작

환불이 발생한 마켓구글 출시버전(모바일)애플 출시버전(모바일)스팀 클라이언트스토브 PC 클라이언트
구글에서 환불재결제 화면 노출이용 제한 안내이용 제한 안내이용 제한 안내
애플에서 환불이용 제한 안내재결제 화면 노출이용 제한 안내이용 제한 안내
스팀에서 환불이용 제한 안내이용 제한 안내재결제 화면 노출이용 제한 안내
환불 내역 없음정상 진입정상 진입정상 진입정상 진입

재결제 화면·이용 제한 안내를 띄우는 시점은 게임마다 다를 수 있어요
로그인 성공 후, 월드 진입 후, 캐릭터 선택 시, 상점 진입 시, 구매 시도 시 등 운영 의도에 맞는 시점에 노출하면 돼요.

처리 흐름 — 접속 시 대상 여부 확인

처리 흐름 — 재결제 결제 진행 (모바일)

처리 흐름 — 재결제 결제 진행 (Steam)

이용 제한 안내 예시

게임 이용 제한 안내
마켓(Google, Apple, Steam 등)에서 환불하신 상품으로 인해 일시적으로 게임 이용이 제한되었어요. 환불하신 마켓의 게임에 접속해 해당 상품을 재결제하신 후 게임을 이용하실 수 있어요.
고객센터 문의   닫기

재결제 UI 구성 가이드

기본 재결제 화면은 개발사에 가이드로 제공되며, 실제 디자인·컨텐츠 구성은 각 개발사가 진행해요.

  • 안내 문구: 각 게임에 맞추어 구성
  • 재결제 버튼: SDK의 startPurchase 함수 호출로 상품 결제 화면 노출
  • 고객센터 문의 버튼: SDK의 customerSupport 함수 호출로 문의하기 이동
  • 새로고침 버튼: 재결제 대상 상품 목록 새로고침

트러블슈팅

상황 원인 해결 방법
결제 완료 후 아이템 지급이 실패해요 결제 완료 알림 URL로 빌링 서버에서 호출이 오지 않아요. 게임서버와 빌링 서버간 네트워크 확인이 필요해요.
결제 완료 후 마켓 검증이 실패해요. 마켓별 설정 정보를 잘못 입력할 경우 발생해요. 파트너스를 통해서 마켓 검증 정보를 수정한 후 최대 1시간 후에 다시 확인이 필요해요.

샘플 코드

결제 완료 알림 처리
text
--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": ""
               }]
        }
}    



결제 유효성 체크
  • 결제 유효성 체크
text
  {
    "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)
text
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
        }
    ]
}



비정상 환불
  • 비정상 환불 목록 수신
text
{
    "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
        }
      }
      
  • 인게임 상점(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
      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 }
          ]
        }
      }
      

자주 묻는 질문



Q1. 파트너스에 빌링 정보를 등록하지 않으면 어떻게 되나요?
A. 파트너스에 마켓 별 IAP 정보(마켓 검증 키, OAuth Client ID/Secret, Refresh Token 등)가 입력되지 않으면 결제 완료 후
상품 지급 요청이 정상 동작하지 않아요. 연동 시작 전 반드시 모든 항목을 등록해 주세요.
Q2. 결제 완료 알림을 받지 못했거나 처리에 실패한 경우 어떻게 하나요?
A. 모바일 빌링의 경우 1~5분 간격으로 최대 100회까지 재 전송을 시도해요.
100회 이후에도 실패한 경우에는 [파트너스] > [MobileBilling] > [판매 내역 조회 및 운영] > [마켓 판매 내역]에서 수동으로 지급 처리해야 해요.
Q3. TID는 어떻게 관리해야 하나요?
A. TID(스토브 빌링 결제 주문번호)는 게임 서버에 저장하여 관리해야 해요.
동일한 결제 알림이 재 전송될 수 있으므로, TID를 기준으로 중복 지급 여부를 확인하는 로직이 반드시 필요해요.
Q4. 비정상 환불 재결제 처리 시 voided_tid 필드는 어떻게 처리해야 하나요?
A. voided_tid 필드에 데이터가 있으면 해당 요청이 비정상 환불 후 재결제에 해당하는 것이에요.
이 경우 게임 서버에서는 아이템을 지급하지 않도록 처리해야 해요. 재결제 처리 목적으로만 사용하는 결제이기 때문이에요.
Q5. 비정상 환불 팝업(STOVE 공통 UI)을 사용할 때 판매 종료된 상품은 어떻게 관리해야 하나요?
A. 재결제 팝업은 동일한 환불 상품 ID로 재결제가 이루어지는 구조이에요.
모바일 환경에서 판매 종료된 상품이더라도 스토브 파트너스와 마켓 내 상품이 반드시 판매 상태로 유지되어야 해요.
Disabled 상태로 변경하면 안 돼요.
Q6. 구독 상품의 갱신 알림은 어떻게 받나요?
A. 구독 갱신 알림 수신을 위해 마켓별 사전 설정이 필요해요.
iOS는 App Store Connect에서 서버 알림 URL을 버전 2 알림으로 변경 등록해야 하고,
Android는 Google Cloud Pub/Sub 주제(Topic)와 구독(Subscription)을 생성하여 스토브 빌링 서버 URL을 엔드포인트로 등록해야 해요.
Q7. Google IAP 연동을 위한 Refresh Token은 어떻게 발급하나요?
A. Google Developers OAuth 2.0 Playground에서 발급할 수 있어요.
OAuth 2.0 Configuration에서 "Use your own OAuth credentials"를 체크한 후 OAuth Client ID와 Client Secret을 입력하고,
Play Android Developer API 범위를 선택해 Authorize API를 클릭해요.
Step 2에서 "Exchange authorization code for tokens" 버튼을 클릭하면 Refresh Token을 얻을 수 있어요.
발급된 정보는 스토브 파트너스 > 빌링 설정 정보에 입력해요.
Q8. iOS 2025년 1월 이후 비승인 환불 수신 방식 변경 사항이 있나요?
A. 네, 2025년 1월 24일부터 iOS 영수증 서명 인증서 변경으로 인해 앱 웹 주소 호출 방식이 중단되고 App Store Server API 호출 방식으로 변경됐어요.
App Store Connect에서 서버 알림 URL을 버전 1에서 버전 2 알림으로 반드시 변경해야 해요.
Q9. PC SDK 빌링 연동 시 WebView 모드에 따라 주의할 사항이 있나요?
A. SDK의 구매창 WebView 모드에 따라 게임 화면의 Fullscreen 설정에 제약이 있어요.
WebViewMode::EXTERNAL 사용 시 Exclusive Fullscreen에서도 정상 동작하지만,
WebViewMode::INTERNAL 사용 시 Borderless Windowed Fullscreen을 사용해야 해요.
Exclusive Fullscreen에서는 구매창이 정상 표시되지 않으니 주의하세요.



직접 문의하고 싶으신가요? stove.developers@smilegate.com