- 마지막 업데이트
본인 확인 서비스
이해하기
스토브 플랫폼이 제공하는 본인 확인 서비스를 연동하면, 본인인증과 점유인증(SMS/Email) 기능을 게임에 활용할 수 있어요. 본인 확인 서비스는 게임 진입 단계, 게임 내 특정 기능 이용 시점, 회원 가입 또는 결제 등 본인 확인이 필요한 다양한 시점에 적용할 수 있어요.
본인인증과 점유인증은 서로 다른 기능이에요.
ㆍ 본인인증: 실제 신원(실명)을 확인해요. 국내 이용자 대상이며 간편인증·토스 인증·휴대폰 인증(PASS 포함)을 사용해요.
ㆍ 점유인증(SMS/Email): 해당 휴대폰 번호나 이메일 주소를 실제로 점유하고 있는지만 확인해요(실명 확인은 아니에요). 스토브가 SMS로 6자리, Email로 4자리 인증코드를 보내 확인해요.
본인인증
본인인증은 국내 이용자 기준으로 간편인증·토스 인증·휴대폰 인증(PASS 포함)을 지원해요.
게임에서 본인인증 기능은 게임 진입 단계(모바일 플랫폼) 또는 게임 내 특정 기능 이용 시점(PC·모바일 플랫폼 공통)에 적용할 수 있어요.
아래 두 가지 방식 중 하나를 선택해 적용할 수 있어요(사업 담당자와 협의).
| 방식 | 제공값 | 스토브 플랫폼 저장 유무 |
|---|---|---|
| Type1 (CI 기반 본인 여부 대조) |
본인인증 대조·등록 완료 확인용 key (simKey) |
ㆍ 스토브가 CI를 저장·관리 ㆍ CI 대조로 동일인 판단(CI 1개당 최대 5계정) |
| Type2 (SDI 기반 식별 값 제공) |
SDI (CI 암호화 값) |
ㆍ CI 미저장, 단방향 암호화 SDI만 전달 ㆍ CP사가 관리, 플랫폼 회원 일치 판단 불가 |
용어 정리
ㆍ CI (Connecting Information): 본인확인기관이 본인인증 과정에서 생성하는 고유 식별 값이에요. 동일 개인은 어디서 인증하더라도 동일한 CI가 부여돼요.
ㆍ SDI (스토브 Duplication Information): 본인인증 과정에서 취득한 CI를 스토브가 단방향 암호화해 생성한 스토브 전용 식별 값이에요.
플랫폼별 인증 동선
ㆍ Mobile SDK: SDK가 본인인증 UI를 띄워 인증을 수행한 뒤, 결과를 게임 서버에서 조회해요.
ㆍ 웹: 본인인증 페이지를 열어(또는 redirect) 인증한 뒤, 복귀해 결과를 조회해요.
점유인증 (SMS/Email)
점유인증은 이용자가 입력한 휴대폰 번호나 이메일 주소가 실제로 본인이 쓰고 있는 연락처인지 확인하는 기능이에요. 스토브가 해당 연락처로 인증코드를 보내고 이용자가 그 코드를 입력하면 확인이 완료돼요. SMS(휴대폰)와 Email(이메일) 두 채널을 지원하고, 어떤 채널을 쓸지는 게임이 선택해요. 게임 안(인게임)과 웹 모두 적용할 수 있어요.
점유인증을 사용하는 시점
ㆍ 스토브 회원가입·로그인·비밀번호 재설정 동선은 스토브가 직접 처리하므로 점유인증을 사용하지 않아요.
ㆍ 게임이 별도로 연락처를 확인해야 하는 상황(사전예약, 이벤트 응모 등)에 사용해요.
활용 사례
연락처가 실제로 닿는지 확인되면 허수 계정이나 중복 참여를 걸러낼 수 있어요. 아래와 같은 상황에서 활용할 수 있어요.
| 활용 상황 | 기대 효과 |
|---|---|
| 사전예약 | 알림 받을 연락처가 실제로 쓰이는 것인지 확인해, 의미 없는 대량 등록으로 인한 보상 어뷰징 방지 |
| 이벤트·경품 응모 | 같은 연락처로 중복 응모했는지 걸러내 1인 1계정 응모 원칙 유지 |
| 커뮤니티·설문 연동 | 공식카페·디스코드 연동이나 설문 참여 시 봇·허수 계정 여부 확인 |
| 오프라인 행사 초청 | 쇼케이스·팬미팅 등 모집 참가자에게 실제로 연락 가능한 채널 확보 |
채널별 비교
두 채널은 인증코드 자릿수와 유효 시간, 발송 제한 정책이 서로 달라요. 이벤트 트래픽과 서비스 국가를 고려해 채널을 선택하세요.
| 항목 | SMS (휴대폰) | Email (이메일) |
|---|---|---|
| 인증 대상 | 휴대폰 번호 | 이메일 주소 |
| 인증코드 | 숫자 6자리 | 숫자 4자리 |
| 인증코드 유효 시간 (유효 시간 내 발송된 인증코드는 모두 유효) |
3분 발송 즉시 수신을 전제로 짧게 적용 |
10분 스팸함 분류·발송 지연 가능성을 고려해 길게 적용 |
| 재발송 제한 (게임별 동일 연락처 기준) |
1분 3회, 1일 20회 | 1시간 5회 |
| 오입력 제한 (게임별 동일 연락처 기준) |
5회 잘못 입력하면 발송된 인증코드 전체 무효화 → 재발송 필요 | SMS와 동일 |
| 지원 국가 | 스토브가 지원하는 국가만 발송 가능 | 제한 없음 |
| 발송 언어 | 번호의 국가코드로 자동 결정 (한국=국문 / 그 외=영문) |
게임이 발송 요청 시 지정 (국문 / 영문, 미지정 시 영문) |
SMS 발송 지원 국가
SMS는 스토브가 지원하는 국가에만 발송할 수 있어요. 아래 목록은 플랫폼이 지원 가능한 전체 국가로, 게임별로 열려 있는 국가는 따로 등록되어 다를 수 있어요. 서비스 오픈 국가와 대조해 SMS 미지원 국가가 있다면 Email 채널로 대체할지 검토하고, 실제 등록 국가는 사업 담당자와 확인해 주세요.
인증코드 문구
이용자에게 전달되는 인증코드 문구는 스토브가 관리해요. 게임이 임의로 바꿀 수 없고 국문·영문 2종만 제공되며, 추후 변경될 수 있어요.
| 언어 | SMS (휴대폰) | Email (이메일) |
|---|---|---|
| 국문 | (게임명) 인증번호 XXXXXX 를 입력해 주세요. 유효시간은 3분입니다. | 아래 인증 번호를 입력하면 인증이 완료됩니다. (유효시간 : 10분) 인증번호 : XXXX |
| 영문 | (GAME) Enter verification code XXXXXX. This code expires in 3 minutes. | Enter the verification code below to complete verification. (valid: 10 minutes) Verification number : XXXX |
이용자 동선
실제 서비스에 적용하면 이용자에게는 다음 흐름으로 보여요. (연락처 등록 시나리오 기준)
- 개인정보 수집·이용 동의를 받고 연락처를 입력받아요.
- 게임이 입력 형식과 발송 횟수 한도를 확인한 뒤 인증코드 발송을 요청해요.
- 이용자가 SMS 또는 이메일로 받은 인증코드를 유효 시간 안에 입력해요.
- 확인에 성공하면 등록이 완료돼요. 실패하면 사유에 맞는 안내를 노출한 뒤 재입력 또는 재발송으로 이어져요.
이용자가 보는 화면은 게임에서 직접 구현해요
ㆍ 스토브는 인증코드 발송·확인 API만 제공해요. 연락처 입력창, 동의 팝업, 인증코드 입력 화면 등 이용자가 보는 UI는 게임에서 직접 구현해야 해요.
ㆍ 아래는 휴대폰 번호 확인 동의 팝업과 인증코드 입력 화면 예시예요.
안내 문구 참고 사항
ㆍ 실패 안내를 얼마나 구체적으로 보여줄지는 게임이 정해요. 구체적일수록 이용자는 편하지만 무차별 대입 시도에 힌트가 될 수 있어 보안과 편의 사이에서 절충이 필요해요.
ㆍ 다국어 서비스라면 안내 문구의 언어별 준비가 필요해요. 스토브가 보내는 인증코드 문구는 국문·영문 2종만 제공돼요.
도입 전 협의 항목
점유인증은 게임별 설정이 등록되어야 사용할 수 있어요. 도입 전에 아래 항목을 사업 담당자와 협의해 주세요.
| 항목 | 협의가 필요한 이유 |
|---|---|
| 사용 채널 (SMS / Email) | 게임마다 지원 채널이 다르게 등록되며, 등록되지 않은 채널로는 발송 자체가 불가 |
| 발송 대상 국가 | 게임별 지원 국가가 별도로 등록되어, 서비스 오픈 국가와 대조 필요 (SMS 한정) |
| 발송 문구·언어 | 문구는 플랫폼이 관리하며 사전 협의 없이 변경 불가. 지원 언어는 국문·영문 |
| 재발송 정책 및 발송 비용 | 사업 담당자와 사전 미팅 필요 |
| 개인정보 처리 | 수집 정보의 보관 기간·파기 기준을 사전 확정하고 개인정보처리방침 반영 여부 법무 검토 |
수집한 정보는 게임이 직접 관리해요
ㆍ 스토브는 발송·확인만 담당해요. 확인한 연락처를 대신 보관하지 않아요.
ㆍ 저장·보관 기간·파기·암호화는 게임이 정해요.
ㆍ 인증 식별키(possession_key)도 개인정보로 취급해요. 중복 참여 판단에 쓸 수 있고, 사용 여부는 선택이에요.
ㆍ 도입 전 법무 검토가 필요해요. 개인정보처리방침 반영 여부, 글로벌 서비스는 국가별 법령(GDPR 등)을 확인해 주세요.
개발하기
본인인증
본인인증 연동은 환경(Mobile SDK / PC SDK / Web / Server)별로 담당 영역이 달라요. 게임 클라이언트의 인증 화면 호출은 Mobile/PC SDK/Web 섹션에서, simKey 발급·결과 조회·SDI 수신은 Server 섹션에서 다뤄요.
Mobile
AuthUI.verifyIdentification은 게임이 능동적으로 본인인증 화면을 띄우는 API예요. 결제·민감 기능 진입 전 재인증이 필요한 시점에 호출해요. compareIdentifier 값으로 SDI 인증과 플랫폼 본인인증을 구분해요.
Mobile SDK는 본인인증 화면을 웹뷰로 자동으로 띄우는 AuthUI.verifyIdentification 인터페이스를 제공해요. 화면을 띄우는 부분만 SDK가 담당하고, simKey 발급·결과 조회·SDI 발급은 서버 API를 통해 이뤄져요.
사전 준비
Auth.initialize와AuthUI.login이 완료돼Auth.AccessToken이 유효한 상태여야 해요.- 호출 전에
compareIdentifier값(SDI / 플랫폼 본인인증)을 결정해 두세요.- 플랫폼 기반 본인인증 (Type1) (
compareIdentifier = false): 콜백의state에 인증 대조 값이 반환돼요. 게임 서버에서 이 값을 검증해 본인 일치 여부를 확정하세요. - SDI 발급 본인인증 (Type2) (
compareIdentifier = true): 콜백의state는 비어 있어요. SDK가 SDI 시스템과 직접 인증을 처리해요.
- 플랫폼 기반 본인인증 (Type1) (
개발 흐름
- 호출 시점 결정: 결제·민감 기능 진입 직전 등 재인증이 필요한 시점인지 확인해요.
- AccessToken 확인:
Auth.AccessToken이null이면 로그인 흐름으로 먼저 유도하세요. AuthUI.verifyIdentification(compareIdentifier, callback)호출: 사전에 결정한compareIdentifier값을 전달해요.- 결과 처리:
- 성공 +
state값이 있는 경우(compareIdentifier = false): 게임 서버로state를 전송해 검증한 뒤 후속 기능 진입을 허용하세요. - 성공 +
state값이 없는 경우(compareIdentifier = true): SDI 검증이 완료된 상태이므로 후속 기능을 바로 진행하세요. - 실패·취소: 이용자에게 재시도를 안내하고 후속 기능 진입을 차단하세요.
- 성공 +
트러블슈팅
| 상황 | 원인 | 해결 방법 |
|---|---|---|
| 호출하자마자 콜백이 실패로 떨어져요 | Auth.AccessToken이 null(로그인 미완료) 상태에서 호출했어요. | 호출 전에 Auth.AccessToken != null을 확인하고, null이면 AuthUI.login을 먼저 호출해 토큰을 발급받으세요. |
성공인데 state 값이 비어 있어요 | compareIdentifier = true(SDI 인증) 호출이었어요. SDI 사용 시 state는 정상적으로 비어 있어요. | 플랫폼 본인인증 대조 값이 필요하면 compareIdentifier = false로 호출하세요. |
state를 받았는데 게임 서버 검증 단계가 없어요 | 플랫폼 본인인증은 state 검증을 게임 서버가 수행해야 본인 일치를 확정할 수 있어요. 클라이언트에서 state 존재 여부만으로 통과 처리하면 우회 위험이 있어요. | state를 게임 서버로 전송해 스토브 인증 서버와 대조 검증한 뒤 후속 기능 진입을 허용하세요. |
샘플 코드
public void VerifyIdentification()
{
if (Auth.AccessToken != null) {
AuthUI.VerifyIdentification(compareIdentifier, (Result result, string state) =>
{
if (result.IsSuccessful)
{
//본인 인증 성공
if (!string.IsNullOrEmpty(state))
{
// 플랫폼 본인 인증 사용 시 전달되는 값
}
}
else
{
//본인 인증 실패
}
});
}
}
PC SDK
View_VerifyIdentificationPopup은 PC 온라인 게임에서 본인인증 화면을 웹뷰로 띄우는 ViewSDK API예요. SDK가 simKey 발급부터 웹뷰 표시까지 처리하고, 인증 완료 후 onDestroy 콜백으로 simKey를 전달해요. 결제·민감 기능 진입 전 재인증이 필요한 시점에 호출해요.
화면을 띄우고 simKey를 받는 부분까지 SDK가 담당하고, 인증 결과 조회·SDI 수신은 서버 API를 통해 이뤄져요.
이 기능은 한국에서만 동작해요. 한국 이외 국가에서 호출 시 NOT_SUPPORTED_COUNTRY(31) 에러가 반환돼요.
스팀런처로 실행한 경우에는 지원하지 않는 기능이에요. 그러므로 스팀런처를 통해 실행중이라면 사용할 수 없어요.
ViewSDK 기능을 사용하려면 BaseSDK 연동·초기화 후 ViewSDK 초기화가 선행돼야 해요. BaseSDK가 초기화되지 않으면 ViewSDK 기능을 사용할 수 없어요.
사전 준비
- BaseSDK 초기화 → ViewSDK 초기화가 완료된 상태여야 해요.
- 호출 전에
CompareIdentifier값(Type1 / Type2)을 결정해 두세요.- 플랫폼 기반 본인인증 (Type1) (
CompareIdentifier = false): 콜백(onDestroy)에 simKey가 전달돼요. 게임 서버에서 이 값을 검증해 본인 일치 여부를 확정하세요. - SDI 발급 본인인증 (Type2) (
CompareIdentifier = true): SDK가 SDI 검증을 직접 처리하며, simKey는 전달되지 않아요.
- 플랫폼 기반 본인인증 (Type1) (
- 웹뷰 모드(
WebViewMode)를 결정해 두세요.Internal(SDK 내장 웹뷰) /External(외부 브라우저)을 선택할 수 있어요.
스토브 PC SDK가 연동되어 게임이 이미 실행된 경우는 본인인증을 마친 뒤 실행하는 상황이에요. 이때 View_VerifyIdentificationPopup은 이미 인증된 상태에서 본인 여부를 확인하는 대조 인증으로 동작해요.
개발 흐름
- 초기화 확인: BaseSDK·ViewSDK 초기화가 완료됐는지 확인해요.
- 호출 시점 결정: 결제·민감 기능 진입 직전 등 재인증이 필요한 시점인지 확인해요.
- 파라미터 설정·호출:
mode(WebViewMode)와compareIdentifier를 인자로View_VerifyIdentificationPopup을 호출해요. onFinished처리: 팝업 호출(웹뷰 실행)의 성공·실패를 확인해요.onDestroy처리: 팝업이 닫힐 때 호출돼요.CompareIdentifier = false(Type1): simKey가 전달돼요. 게임 서버로 simKey를 전송해 결과 조회로 검증한 뒤 후속 기능 진입을 허용하세요. (Server 섹션 참고)CompareIdentifier = true(Type2): SDI 검증이 완료된 상태이므로 후속 기능을 바로 진행하세요.
트러블슈팅
| 상황 | 원인 | 해결 방법 |
|---|---|---|
호출 시 NOT_SUPPORTED_COUNTRY(31) 에러가 떨어져요 | 한국 이외 국가에서 호출했어요. 이 기능은 한국에서만 동작해요. | 본인인증 팝업은 한국 환경에서만 호출하세요. |
| 호출하자마자 실패로 떨어져요 | BaseSDK·ViewSDK 초기화가 완료되지 않은 상태에서 호출했어요. | BaseSDK 초기화 → ViewSDK 초기화를 먼저 완료한 뒤 호출하세요. |
onDestroy에서 simKey가 비어 있어요 | CompareIdentifier = true(Type2, SDI) 호출이었어요. SDI 사용 시 simKey는 정상적으로 전달되지 않아요. | 플랫폼 본인인증 대조 값이 필요하면 CompareIdentifier = false로 호출하세요. |
| simKey를 받았는데 게임 서버 검증 단계가 없어요 | Type1은 simKey를 게임 서버가 검증해야 본인 일치를 확정할 수 있어요. 클라이언트에서 simKey 존재만으로 통과 처리하면 우회 위험이 있어요. | simKey를 게임 서버로 전송해 결과 조회로 대조 검증한 뒤 후속 기능 진입을 허용하세요. (Server 섹션 참고) |
샘플 코드
CompareIdentifier를 Type에 맞춰 설정하세요. (false: Type1 simKey 전달 / true: Type2 SDI 검증)
// 1. 최상단에 ViewSDK 모듈 헤더를 포함합니다.
#include "ViewSDK.h"
// 2. API와 구조체가 포함된 네임스페이스를 사용하도록 선언합니다.
using namespace Stove::PCSDK;
using namespace Stove::PCSDK::Base;
using namespace Stove::PCSDK::View;
void View_VerifyIdentificationPopup_Example()
{
// 3. 웹뷰 모드 설정 (INTERNAL / EXTERNAL)
WebViewMode mode{ WebViewMode::INTERNAL };
// false : simKey 전달(Type1), true : SDI 검증(Type2)
bool compareIdentifier = false;
// 4. 인증 팝업을 호출하여 본인인증을 진행합니다.
View_VerifyIdentificationPopup(compareIdentifier, mode,
[](CallbackResult callbackResult) {
if (callbackResult.GetResult().IsSuccessful())
{
// 인증 팝업 호출 성공 처리
UE_LOG(LogTemp, Log, TEXT("Verification popup succeeded."));
}
else
{
// 인증 팝업 호출 실패 처리
UE_LOG(LogTemp, Log, TEXT("Verification popup failed."));
}
},
[](CallbackResult callbackResult, const wchar_t* simkey) {
// 팝업이 닫혔을 때의 처리
// simkey는 본인인증 완료 후 전달되는 값입니다. (compareIdentifier=false 일 때)
UE_LOG(LogTemp, Log, TEXT("simkey : %s"), *FString(simkey));
if (callbackResult.GetResult().IsSuccessful())
{
// 팝업 닫힘 성공 처리
UE_LOG(LogTemp, Log, TEXT("Verification popup closed successfully."));
}
else
{
// 팝업 닫힘 실패 처리
UE_LOG(LogTemp, Log, TEXT("Failed to close verification popup."));
}
});
}
Web
스토브 본인인증 페이지(https://accounts.onstove.com/verification)를 브라우저로 오픈해 이용자에게 본인인증을 진행시키는 영역이에요. 게임 서버에서 발급받은 simKey(state)를 URL 파라미터로 전달하고, 인증 완료 후 게임 서버 결과 조회로 이어져요.
사전 준비
- 게임 서버에서 simKey(state) 발급이 선행돼야 해요. 발급은 아래 Server 섹션을 참고하세요.
- 환경별 본인인증 페이지 호스트를 사용해요.
- Live:
https://accounts.onstove.com - Sandbox:
https://accounts.gate8.com
- Live:
- 본인인증 방식(Type)에 따라 URL의
type값과 사전 발급해야 하는 simKey 종류가 달라요.- 플랫폼 기반 본인인증 (Type1): 스토브 회원의 CI와 인증 결과 CI를 대조해 동일인 여부 확인. simKey 발급 시
platform_type=PC_WEB_CHECK - SDI 발급 본인인증 (Type2): CI 단방향 암호화 SDI 발급. 스토브 회원과의 일치 여부 판단 불가, CP사 자체 관리. simKey 발급 시
platform_type=CP_GAME
- 플랫폼 기반 본인인증 (Type1): 스토브 회원의 CI와 인증 결과 CI를 대조해 동일인 여부 확인. simKey 발급 시
- 외부 브라우저 동선을 사용한다면
redirect_url은 제거하고, 인증 완료 후 이용자가 직접 게임으로 복귀하는 흐름을 안내해요.
개발 흐름
- 게임 서버로부터 simKey(state)를 수신해요.
- 환경별 호스트에 query string으로
type/state/redirect_url/lang을 조립해요. - 본인인증 페이지를 브라우저로 오픈해요. 모바일 환경에서는 인앱 브라우저 대신 외부 브라우저(Safari/Chrome)를 권장해요. (일부 PASS·통신사 인증이 인앱 브라우저를 차단)
- 이용자가 본인인증을 진행해요.
- 인증 완료 후
redirect_url로 복귀(또는 외부 브라우저 동선은 이용자가 게임으로 복귀)해요. - 게임 서버에 결과 조회를 요청해 후속 처리를 진행해요. (Server 섹션 참고)
| 분기 | URL type | 사전 발급할 simKey |
|---|---|---|
| Type1 대조용 (본인인증 등록 회원) | COMPARE_IDENTIFY | POST /sim/v1/compare (platform_type=PC_WEB_CHECK) |
| Type1 최초 등록용 (본인인증 미등록 회원) | STORAGE_IDENTIFY | POST /sim/v1/cert (platform_type=PC_WEB) |
| Type2 SDI 발급 | COMPARE_IDENTIFY | POST /sim/v1/compare (platform_type=CP_GAME) |
트러블슈팅
| 상황 | 원인 | 처리 방안 |
|---|---|---|
| 페이지 오픈 후 빈 화면이 떠요 | state 값이 잘못되었거나 만료(10분 초과) | 페이지 진입 직전에 simKey를 새로 발급받으세요. |
| 환경별 호스트 혼용 | Sandbox에서 발급한 state로 Live 호스트에 접근(또는 반대) | state 발급 환경과 페이지 호스트 환경을 일치시키세요. |
잘못된 type 사용 | personVerifyYn=Y인데 STORAGE_IDENTIFY 사용(또는 그 반대) | 회원정보 조회로 personVerifyYn을 먼저 확인한 뒤 COMPARE_IDENTIFY/STORAGE_IDENTIFY를 분기하세요. |
| 인앱 브라우저에서 본인인증 실패 | 일부 통신사 PASS 인증이 인앱 브라우저를 차단 | 외부 브라우저로 오픈하세요. 외부 브라우저 동선에서는 redirect_url을 제거하고 게임 내 안내 팝업으로 결과 조회를 트리거하세요. |
| Type2인데 SDI가 발급되지 않아요 | simKey 발급 시 platform_type 값이 CP_GAME이 아님 | simKey 발급 호출에서 platform_type=CP_GAME을 명시하세요. (Server 섹션 참고) |
샘플 코드
URL 조립 예제예요. {state}는 게임 서버에서 발급받은 값으로 교체하세요.
# Type1 대조용 (이미 본인인증이 등록된 회원 대조)
https://accounts.onstove.com/verification?type=COMPARE_IDENTIFY&state={state}&redirect_url={redirect_url}&lang=ko
# Type1 최초 등록용 (본인인증 미등록 회원)
https://accounts.onstove.com/verification?type=STORAGE_IDENTIFY&state={state}&redirect_url={redirect_url}&lang=ko
# Type2 SDI 발급 (CP_GAME 으로 발급된 state)
https://accounts.onstove.com/verification?type=COMPARE_IDENTIFY&state={state}&redirect_url={redirect_url}
Server
게임 서버에서 스토브 본인인증 API를 호출해 simKey 발급·결과 조회를 수행하는 영역이에요.
Type1은 게임 서버가 결과 조회 API를 호출하는 Pull 방식이고, Type2 SDI는 스토브가 게임 서버로 SDI를 전달하는 Push 방식이에요.
사전 준비
- 클라이언트로부터 Game User Access Token을 전달받는 구조여야 해요. simKey 발급 API의 Authorization 헤더에
Bearer {Game User Access Token}형태로 사용해요. - Type 분기 결정: Type1(플랫폼 기반 본인인증) / Type2(SDI 발급 본인인증)
- Type2 SDI를 선택한 경우 게임 서버에 SDI 수신 엔드포인트(e.g.
POST /compareResult)를 별도로 구현하고, 스토브 → 게임 서버 호출이 가능하도록 수신 엔드포인트와 ACL 등록을 담당 기술PM에게 요청해야 해요. 구현 시 주의사항은 트러블슈팅을 참고하세요.
- Type2 SDI를 선택한 경우 게임 서버에 SDI 수신 엔드포인트(e.g.
개발 흐름
- (선택) 회원정보 조회 API(
GET /member/v3.0/{game_id}/memberinfo)로personVerifyYn을 확인해 본인인증 등록 여부 판단 →COMPARE_IDENTIFYvsSTORAGE_IDENTIFY분기 결정. 이 호출에는 API Access Token이 필요해요. (발급 절차는 위 인증 메뉴 → Server 참고) - simKey(state) 발급
- Type1 대조용:
POST /sim/v1/compare(platform_type=PC_WEB_CHECK) - Type1 최초 등록용:
POST /sim/v1/cert(platform_type=PC_WEB) - Type2 SDI:
POST /sim/v1/compare(platform_type=CP_GAME)
- Type1 대조용:
- 발급받은 state를 클라이언트로 전달하고, 이용자가 본인인증 페이지에서 인증을 수행해요. (Web 섹션 참고)
- 인증 완료 후 결과 처리 분기:
- Type1: 게임 서버가 결과 조회 API 호출
- 다회용:
GET /sim/v1/compare/check?state=...— 여러 번 조회 가능 - 일회용:
DELETE /sim/v1/compare/expire?state=...— 조회 후 state 만료 (보안상 권장)
- 다회용:
- Type2 SDI: 스토브가 게임 서버의
POST /compareResult엔드포인트로state/sdi/y_age를 전송 → 게임 서버는guid+sdi+ 인증 콘텐츠 매핑으로 저장
- Type1: 게임 서버가 결과 조회 API 호출
트러블슈팅
| 응답 code | 상황 | 처리 방안 |
|---|---|---|
| 91030 | Wrong Request — 필수 파라미터 누락 또는 잘못된 값 | 요청 Body의 service / platform_type / cert_type / game_id 값을 확인하세요. |
| 91031 | Not Verified — 본인인증 미완료 상태에서 결과 조회 호출 | 이용자가 본인인증 페이지에서 인증을 마쳤는지 확인 후 재조회하세요. |
| 93000 | Redis compare data is null — state 만료(10분) / 잘못된 state / 일회용 API로 이미 조회된 state | 페이지 진입 직전에 simKey를 새로 발급받으세요. |
| 40000 / 40101 / 40103 | API Access Token / User Access Token 인증 오류 | 인증 메뉴 → Server 트러블슈팅을 참고하세요. 토큰 재발급 후 재시도해요. |
Type2 SDI 수신 엔드포인트 구현 시 주의사항
· 스토브는 게임 서버가 HTTP 2xx을 응답하면 전송을 완료해요. 5xx / 4xx 응답 시 재시도하므로, 동일 state로 SDI가 중복 수신될 수 있어요. state 단위 멱등성(중복 저장 방지)을 보장해 주세요.
· 엔드포인트는 HTTPS + TLS 1.2 이상에서 동작해야 해요.
· 스토브 → 게임 서버 호출을 위한 인프라 ACL 등록이 필요해요. 게임 서버 도메인/IP를 담당 기술PM에게 전달하세요.
· 동일 이용자라도 Sandbox / Live 환경별로 다른 SDI가 생성되니 환경별 분리 저장이 필요해요.
SDI는 개인정보예요
로그에 그대로 기록하지 말고 마스킹·제외 처리하세요. 이용자가 게임을 탈퇴할 때 매핑된 SDI는 반드시 삭제해야 해요.
샘플 코드
Type1 대조용 simKey 발급(POST /sim/v1/compare)과 다회용 결과 조회(GET /sim/v1/compare/check) 호출 예제예요.
Type1 최초 등록용은 endpoint를 /sim/v1/cert로, Type2 SDI는 platform_type을 CP_GAME으로 교체하세요.
Base URL은 환경(Live/Sandbox)에 맞춰 교체해 주세요
운영 환경에서는 하드코딩 대신 환경 변수(예: STOVE_API_BASE_URL)나 프레임워크 설정 파일로 분리해 환경별로 주입하는 것을 권장해요.
// Java 25 LTS — java.net.http.HttpClient + Jackson(ObjectMapper)
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
ObjectMapper mapper = new ObjectMapper();
String baseUrl = "https://api.onstove.com";
String serviceId = "STOVE_GAME";
String callerId = serviceId + "_SERVER";
try (HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build()) {
// 1) simKey(state) 발급 (Type1 대조용)
String issueBody = mapper.writeValueAsString(Map.of(
"service", "STOVE",
"platform_type", "PC_WEB_CHECK",
"cert_type", "SELF",
"game_id", serviceId));
HttpRequest issueReq = HttpRequest.newBuilder(URI.create(baseUrl + "/sim/v1/compare"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + userAccessToken)
.header("caller-id", callerId)
.POST(HttpRequest.BodyPublishers.ofString(issueBody))
.build();
HttpResponse<String> issueRes = client.send(issueReq, HttpResponse.BodyHandlers.ofString());
JsonNode issueData = mapper.readTree(issueRes.body());
if (issueData.path("code").asInt() != 0) {
// 트러블슈팅 표 참고 (91030 등)
throw new IllegalStateException("simKey issue failed: " + issueData.path("code"));
}
String state = issueData.path("value").asText();
// 2) 본인인증 결과 조회 (다회용)
// 이용자가 웹에서 본인인증을 마친 시점 이후에 호출
String encodedState = URLEncoder.encode(state, StandardCharsets.UTF_8);
HttpRequest verifyReq = HttpRequest.newBuilder(
URI.create(baseUrl + "/sim/v1/compare/check?state=" + encodedState))
.header("caller-id", callerId)
.GET()
.build();
HttpResponse<String> verifyRes = client.send(verifyReq, HttpResponse.BodyHandlers.ofString());
JsonNode verifyData = mapper.readTree(verifyRes.body());
if (verifyData.path("code").asInt() == 0) {
// 본인인증 완료 — 후속 처리 진행
} else {
// 트러블슈팅 표 참고 (91031 / 93000 등)
}
}
점유인증 (SMS/Email)
점유인증은 스토브 점유인증 API 두 단계(발송 → 확인)를 게임 서버가 호출하고, 게임 클라이언트는 입력·결과 화면 UX만 담당하는 구조예요. SMS와 Email은 같은 엔드포인트를 쓰고 channel 값으로만 구분해요. SDK 호출은 없고, 모든 외부 통신은 게임 서버 ↔ 스토브 API에서 일어나기 때문에 게임 서버 구현이 중심이 돼요.
응답 코드의 상세 의미가 필요한 경우 트러블슈팅 표를 참고하세요.
사전 준비
- 사용할 채널(SMS / Email), 문자 발송 지원 국가, 비용 정산은 사업 담당자와 사전 협의해야 해요. 협의된 채널만 게임별로 등록되며, 등록되지 않은 채널로 요청하면
70115를 응답해요. - 스토브 점유인증 API 호출을 위해 플랫폼 API 액세스 토큰(API Access Token) 을 발급받으세요. (발급 절차는 위 인증(토큰·검증) → Server 참고)
- 게임(service) 식별은 플랫폼 API 액세스 토큰의
sid로 도출돼요. 게임 외 사용처(공식 홈페이지 등)도 플랫폼 API 액세스 토큰을 발급받아 사용하며, 이때도 API 호출은 반드시 백엔드에서 이뤄져야 해요. - API 호출 시
caller-id헤더 값은 파트너스에 등록한 Game ID를 사용하세요. (호출자 식별·모니터링 용도) - 게임 톤앤매너에 맞는 화면(국가 선택 + 휴대폰 번호 입력 또는 이메일 입력, 개인정보 동의, 인증코드 입력, 결과 안내 팝업)을 사전에 구성하세요.
- 정책 한도를 사전 확인하세요. 동일 대상 기준 재발송은 SMS 1분 3회 / 1일 20회, Email 1시간 5회까지 가능하고, 인증코드 오입력은 SMS·Email 공통 5회까지 허용돼요.
개발 흐름
각 구현 범위는 아래처럼 나뉘어요. 게임 클라이언트는 UX만, 게임 서버는 스토브 API 호출과 결과 저장을 담당해요.
| 담당 | 구현 범위 |
|---|---|
| 게임 클라이언트 | 채널에 맞는 입력 화면(국가코드 셀렉터·휴대폰 번호 입력 / 이메일 입력), 개인정보 동의, "인증코드 받기" 버튼(디바운스 포함), 인증코드 입력 화면(SMS 6자리 / Email 4자리), 결과·에러 안내 팝업 |
| 게임 서버 | API Access Token 보관, channel·to 조립·검증(SMS는 E.164 정규화), 두 스토브 API 호출과 응답 처리, 타임아웃 시 지수 백오프 재시도 |
| 스토브 API | 인증코드 생성·발송(SMS / Email) / 코드 유효성 검증 / possession_key 발급 |
구현은 다음 순서로 진행해요.
- (클라이언트) 인증 대상 입력 화면 구성:
sms는 국가코드 셀렉터와 번호 입력 필드를,email은 이메일 입력 필드를 노출하고, 개인정보 동의 후에만 "인증코드 받기" 버튼을 활성화하세요. 버튼은 디바운스·쿨다운으로 단시간 반복 클릭을 차단하세요. - (클라이언트 → 서버) 인증 대상 전달: 이용자 입력값을 게임 서버 자체 엔드포인트로 전송해요. 게임 서버는
channel=sms면to를+{국가코드}{번호}형식(E.164)으로 정규화하고(예:+821012345678),channel=email이면 이메일 형식을 검증하세요. - (서버 → 스토브) 점유인증코드 요청:
POST /sms-sender/v3.2/occupancy_auth/send_code에channel+to(Email은locale추가)를 담아 호출해요. 응답code로 분기하고, 성공 시value.code_length·value.expires_in으로 인증코드 입력 화면의 자릿수와 남은 시간 타이머를 구성하세요. - (클라이언트 → 서버) 인증코드 수신·전달: 이용자가 SMS(6자리) 또는 Email(4자리)로 받은 코드를 입력하면 게임 서버로 보내요. 게임 서버는 공백·길이 검증을 선행한 뒤 다음 호출로 진행해요.
- (서버 → 스토브) 점유인증코드 확인 요청:
POST /sms-sender/v3.2/occupancy_auth/verify_code에channel+to+verification_code를 담아 호출해요. 응답code로 분기해요.code = 0→ 성공.value.possession_key를 받아 다음 단계로 진행하세요.code = 70103→ 인증코드 불일치. 이용자에게 "인증 실패, 다시 입력해 주세요"만 노출하고 코드 재입력을 처리하세요.code = 70104→ 인증코드가 없거나 만료/무효화된 상태예요. 재입력이 아니라 3번 단계(재발송)부터 다시 진행하도록 안내하세요.
- (서버)
possession_key활용(선택): 인증 대상의 중복 여부를 판단해야 한다면 응답받은possession_key를 활용할 수 있어요. 사용 여부와 관리 방식은 개발사 정책에 따라 결정해 주세요.
발송 언어(locale)
ㆍ channel=email: locale 값(ko / en)으로 결정돼요. 미지정 시 en이며, ko/en 이외의 값은 en으로 대체돼요.
ㆍ channel=sms: 번호의 국가코드를 기준으로 자동 결정돼요(한국=국문, 그 외=영문). locale 값은 무시돼요.
Email 발신 정보
ㆍ 발신자: STOVE <noreply@smilegate.com>
ㆍ 제목: 국문 「스토브 인증 메일 안내」 / 영문 「STOVE Verification Email Guide」
ㆍ 이용자가 메일을 받지 못했다고 문의하면 스팸함 확인을 먼저 안내하세요.
구현 시 핵심 주의사항
ㆍ 발송된 인증코드는 유효 시간(SMS 3분 / Email 10분) 만료 전까지 모두 유효하나, 1개 코드라도 인증 완료 시 동일 대상(휴대폰 번호 또는 이메일)으로 발송된 인증코드는 모두 만료돼요.
ㆍ 인증코드를 5회 잘못 입력하면 해당 대상의 인증코드가 전체 무효화돼요(SMS·Email 공통). 이후 확인 요청은 70104로 응답되므로 재발송부터 다시 진행하세요. 인증 성공 또는 인증코드 재발송 시 실패 카운트는 초기화돼요.
ㆍ 인증코드 자릿수는 채널별로 다르므로(SMS 6자리 / Email 4자리) 입력 UI는 발송 응답의 value.code_length를 기준으로 구성하세요.
ㆍ "인증코드 받기" 버튼은 단시간 반복 클릭 방지 처리가 필요해요. (디바운스 + 클라이언트 쿨다운 권장)
ㆍ verify_code 호출 시 verification_code 공백 검증을 게임 서버에서 선행하세요. 빈 값으로 API를 호출하면 안 돼요.
ㆍ API 응답 코드는 서버단에서 분기 처리하고, 프론트엔드에는 성공 / 인증 실패 / 안내 팝업 형태로만 노출하세요. 응답 코드를 그대로 노출하면 플랫폼 내부 설정 상태나 인증 시도의 성패 원인이 외부에 드러나요.
트러블슈팅
점유인증코드 요청 API 응답 코드
| HTTP | Code | 상황 | 처리 방안 |
|---|---|---|---|
| 200 | 0 | 발송 요청 접수 | value.code_length·value.expires_in으로 인증코드 입력 화면 구성 후 전환 |
| 200 | 70100 | invalid parameter | channel / to 필수값 및 형식 점검 후 재호출 |
| 200 | 70101 | 재발송 제한 초과 | SMS 1분 3회 / Email 1시간 5회 초과. 이용자에게 재시도 안내 |
| 200 | 70105 | 1일 20회 초과 (SMS 전용) | 이용자에게 "24시간 이후 다시 시도" 안내 (Email 미적용) |
| 200 | 70107 | 미지원 국가 (SMS) | 이용자에게 지원 국가 안내 (Email 미적용) |
| 200 | 70108 | 게임(service) 미등록 | "고객센터" 안내 (게임기술 담당자를 통한 게임 등록 요청) |
| 200 | 70109 | 점유인증 발송 정책 미등록 | "고객센터" 안내 (게임기술 담당자를 통한 게임 정책 등록 요청) |
| 200 | 70110 | 휴대폰 번호 형식 오류 (SMS) | to 값의 E.164 형식 점검 후 재호출 |
| 200 | 70115 | 등록하지 않은 채널 사용 | channel 값 점검 후 재호출, 실패 시 "고객센터" 안내 (게임기술 담당자를 통한 게임 채널 등록 요청) |
| 200 | 70116 | 이메일 형식 오류 (Email) | to 값의 이메일 형식 점검 후 재호출 |
| 200 | 70199 | 스토브 내부 오류 | 지수 백오프 재시도, 실패 시 "고객센터" 안내 |
| 401 | 40100 / 40101 / 40102 / 40103 / 40104 / 40106 / 40302 / 50000 | 토큰 오류 | API Access Token 재발급 후 재시도 |
| 408 | 40800 | Request Timeout | 지수 백오프 재시도 |
| 429 | 42900 | Too Many Requests | 지수 백오프 재시도 |
| 504 | 50400 | Gateway Timeout | 지수 백오프 재시도 |
점유인증코드 확인 API 응답 코드
| HTTP | Code | 상황 | 처리 방안 |
|---|---|---|---|
| 200 | 0 | 인증 성공 | 콘텐츠 진입 허용 |
| 200 | 70100 | invalid parameter | channel / to / verification_code 형식 점검 (공백 여부 포함) |
| 200 | 70103 | 인증코드 불일치 | 이용자에게 "인증 실패" 안내 + 재입력 처리 (유효 시간 내 재시도 가능, 5회 도달 시 무효화) |
| 200 | 70104 | 발급된 인증코드 없음 | 만료/미발급 또는 5회 오입력으로 무효화된 상태. 이용자에게 재발송 안내 |
| 200 | 70109 | 점유인증 정책 미등록 | "고객센터" 안내 (게임기술 담당자를 통한 게임 정책 등록 요청) |
| 200 | 70110 | 휴대폰 번호 형식 오류 (SMS) | to 값의 E.164 형식 점검 후 재호출 |
| 200 | 70115 | 등록하지 않은 채널 사용 | channel 값 점검 후 재호출, 실패 시 "고객센터" 안내 (게임기술 담당자를 통한 게임 채널 등록 요청) |
| 200 | 70116 | 이메일 형식 오류 (Email) | to 값의 이메일 형식 점검 후 재호출 |
| 200 | 70199 | 스토브 내부 오류 | 지수 백오프 재시도, 실패 시 "고객센터" 안내 |
| 401 | 40100 / 40101 / 40102 / 40103 / 40104 / 40106 / 40302 / 50000 | 토큰 오류 | API Access Token 재발급 후 재시도 |
| 408 | 40800 | Request Timeout | 지수 백오프 재시도 |
| 504 | 50400 | Gateway Timeout | 지수 백오프 재시도 |
샘플 코드
게임 서버에서 점유인증코드 요청(POST /sms-sender/v3.2/occupancy_auth/send_code)과 확인(POST /sms-sender/v3.2/occupancy_auth/verify_code)을 호출하는 예제예요.
각 API 를 ① 변수 값 설정 → ② 요청 파라미터 준비 → ③ Request 생성 → ④ API 호출 → ⑤ 응답 처리 5단계로 나눠 정리했어요. 요청 파라미터는 레퍼런스 문서의 Header · Body 항목과 같은 이름의 변수로 선언했으니 값만 바꿔서 사용하고, 채널은 channel 값을 sms / email로 지정해요.
Base URL은 환경(Live/Sandbox)에 맞춰 교체해 주세요
ㆍ Live: https://api.onstove.com
ㆍ Sandbox: https://api.gate8.com
운영 환경에서는 하드코딩 대신 환경 변수(예: STOVE_API_BASE_URL)나 설정 파일로 분리해 환경별로 주입하는 것을 권장해요.
// java.net.http.HttpClient + Jackson (ObjectMapper)
// Sample for calling the occupancy auth APIs in order: send code -> verify code.
// Each API is organized into STEP 1-5.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.LinkedHashMap;
import java.util.Map;
public class OccupancyAuthSample {
// Base URL : Live https://api.onstove.com / Sandbox https://api.gate8.com
private static final String BASE_URL = "https://api.onstove.com";
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final HttpClient CLIENT = HttpClient.newHttpClient();
// ==============================================================================
// [API 1] Send verification code POST /sms-sender/v3.2/occupancy_auth/send_code
// ==============================================================================
public static JsonNode sendCode(String apiAccessToken, String channel, String to, String locale) throws Exception {
// STEP 1. Set variables
String url = BASE_URL + "/sms-sender/v3.2/occupancy_auth/send_code";
String callerId = "STOVE_GAME_ID"; // (required) Game ID registered in Partners
String callerDetail = "f248a284-7e61-4dcf-a111-a394050e9555"; // (optional) Caller UUID, for monitoring
// STEP 2. Prepare request parameters (Body)
Map<String, String> body = new LinkedHashMap<>();
body.put("channel", channel); // (required) Auth channel : "sms" | "email"
body.put("to", to); // (required) Auth target : sms = "+821012345678" / email = "user@example.com"
if ("email".equals(channel)) {
body.put("locale", locale); // (optional) Email-only language : "ko" | "en" (defaults to "en")
}
// STEP 3. Build the request (Header)
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Authorization", "Bearer " + apiAccessToken) // (required) API Access Token (see /docs/stove/guide/authentication#server)
.header("Content-Type", "application/json") // (required)
.header("caller-id", callerId) // (required)
.header("caller-detail", callerDetail) // (optional)
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(body)))
.build();
// STEP 4. Call the API
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
// STEP 5. Handle the response
if (response.statusCode() != 200) {
// 401 : reissue the token / 408, 429, 504 : retry with exponential backoff
throw new IllegalStateException("send_code http " + response.statusCode());
}
JsonNode result = MAPPER.readTree(response.body());
int code = result.path("code").asInt(); // Response code (0 = success)
if (code != 0) {
// 70101 resend limit / 70105 daily limit / 70107 unsupported country / 70115 unregistered channel ... (see the troubleshooting table)
throw new IllegalStateException("send_code failed: code=" + code);
}
int codeLength = result.path("value").path("code_length").asInt(); // Code length (sms 6 / email 4)
int expiresIn = result.path("value").path("expires_in").asInt(); // Time to live in seconds (sms 180 / email 600)
// Pass codeLength and expiresIn to the game client to set the input length and the timer.
return result;
}
// ==============================================================================
// [API 2] Verify code POST /sms-sender/v3.2/occupancy_auth/verify_code
// ==============================================================================
public static String verifyCode(String apiAccessToken, String channel, String to, String verificationCode) throws Exception {
// STEP 1. Set variables
String url = BASE_URL + "/sms-sender/v3.2/occupancy_auth/verify_code";
String callerId = "STOVE_GAME_ID";
String callerDetail = "f248a284-7e61-4dcf-a111-a394050e9555";
// STEP 2. Prepare request parameters (Body)
if (verificationCode == null || verificationCode.isBlank()) {
throw new IllegalArgumentException("verification_code is empty");
}
Map<String, String> body = new LinkedHashMap<>();
body.put("channel", channel); // (required) Same channel as the send request
body.put("to", to); // (required) Same auth target as the send request
body.put("verification_code", verificationCode); // (required) Code entered by the user (sms 6 digits / email 4 digits)
// STEP 3. Build the request (Header)
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("Authorization", "Bearer " + apiAccessToken)
.header("Content-Type", "application/json")
.header("caller-id", callerId)
.header("caller-detail", callerDetail)
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(body)))
.build();
// STEP 4. Call the API
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
// STEP 5. Handle the response
if (response.statusCode() != 200) {
// 401 : reissue the token / 408, 504 : retry with exponential backoff
throw new IllegalStateException("verify_code http " + response.statusCode());
}
JsonNode result = MAPPER.readTree(response.body());
int code = result.path("code").asInt();
if (code == 70103) {
// Code mismatch -> ask the user to re-enter (all codes are invalidated after 5 failures)
return null;
}
if (code == 70104) {
// No issued code (expired or invalidated) -> ask the user to request a resend
return null;
}
if (code != 0) {
throw new IllegalStateException("verify_code failed: code=" + code);
}
// Verified. possession_key can be used to check whether the auth target is a duplicate. (optional)
return result.path("value").path("possession_key").asText();
}
}