- Last Updated
Identity Verification Service
Understanding
By integrating the Identity Verification Service provided by the Stove platform, you can utilize identity verification and possession verification (SMS/Email) features in your game. The Identity Verification Service can be applied at various points where identity verification is required, such as the game entry stage, when using specific in-game features, or during account registration or payment.
Identity verification and possession verification are different features.
ㆍ Identity Verification: Verifies actual identity (legal name). Targeted at domestic users, it uses simple authentication, Toss authentication, and mobile phone authentication (including PASS).
ㆍ Possession Verification (SMS/Email): Only verifies whether the user actually possesses the mobile phone number or email address (this is not identity verification). Stove sends a 6-digit code via SMS or a 4-digit code via Email for verification.
Identity Verification
For domestic users, identity verification supports simple authentication, Toss authentication, and mobile phone authentication (including PASS).
In games, the identity verification feature can be applied at the game entry stage (mobile platform) or when using specific in-game features (common to PC/mobile platforms).
You can choose and apply one of the two methods below (consult with your business manager).
| Method | Provided Value | Stove Platform Storage |
|---|---|---|
| Type1 (CI-based identity comparison) |
Key for checking identity comparison/registration completion (simKey) |
ㆍ Stove stores and manages CI ㆍ Determines same person via CI comparison (max 5 accounts per CI) |
| Type2 (SDI-based identification value provision) |
SDI (CI encrypted value) |
ㆍ CI not stored, only one-way encrypted SDI delivered ㆍ Managed by CP, cannot determine platform member match |
Terminology
ㆍ CI (Connecting Information): A unique identification value generated by the identity verification agency during the process. The same individual is assigned the same CI regardless of where they authenticate.
ㆍ SDI (Stove Duplication Information): A Stove-specific identification value generated by one-way encryption of the CI obtained during the identity verification process.
Authentication Flow by Platform
ㆍ Mobile SDK: The SDK displays the identity verification UI to perform authentication, then the game server retrieves the result.
ㆍ Web: Opens (or redirects to) the identity verification page to authenticate, then returns to retrieve the result.
Possession Verification (SMS/Email)
Possession verification is a feature that confirms whether the mobile phone number or email address entered by the user is actually used by them. Verification is completed when Stove sends an authentication code to the contact information and the user enters that code.SMS (Mobile) and Email channels are supported, and the game chooses which channel to use. It can be applied both in-game and on the web.
When to Use Possession Verification
ㆍ Stove handles registration, login, and password reset flows directly, so possession verification is not used there.
ㆍ Use it when the game needs to verify contact information separately (e.g., pre-registration, event participation).
Use Cases
Verifying that contact information is reachable helps filter out dummy accounts or duplicate participation. It can be used in situations such as the following.
| Usage Scenarios | Expected Benefits |
|---|---|
| Pre-registration | Verify that the contact information provided for notifications is valid to prevent reward abusing caused by massive, meaningless registrations. |
| Event/Giveaway Entry | Filter out duplicate entries from the same contact to maintain the one-account-per-person rule. |
| Community/Survey Integration | Verify whether users are bots or fake accounts when linking official cafes/Discord or participating in surveys. |
| Offline Event Invitations | Secure channels that can actually be used to contact participants recruited for showcases, fan meetings, etc. |
Comparison by Channel
The two channels differ in verification code length, validity period, and sending restriction policies. Please select a channel considering event traffic and the service country.
| Item | SMS (Mobile) | |
|---|---|---|
| Verification Target | Mobile Number | Email Address |
| Verification Code | 6-digit number | 4-digit number |
| Verification Code Validity Period (All codes sent within the validity period are valid) |
3 minutes Applied briefly, assuming immediate receipt upon sending |
10 minutes Applied for a longer duration, considering potential spam folder filtering or delivery delays |
| Resend Restriction (Based on the same contact per game) |
3 times per minute, 20 times per day | 5 times per hour |
| Input Error Limit (Based on the same contact per game) |
If entered incorrectly 5 times, all sent verification codes are invalidated → Resending required | Same as SMS |
| Supported Countries | Can only be sent to countries supported by STOVE | No restrictions |
| Sending Language | Automatically determined by the country code of the number (Korea = Korean / Others = English) |
Specified by the game upon sending request (Korean / English, defaults to English if not specified) |
Countries Supported for SMS Sending
SMS can only be sent to countries supported by STOVE. The list below shows all countries supported by the platform; the countries enabled for each game may differ as they are registered separately. Please compare this with the countries where the service is open, consider using the Email channel as a substitute if there are countries where SMS is not supported, and confirm the actual registered countries with the business manager.
Verification Code Message
The verification code message delivered to the user is managed by STOVE. It cannot be changed by the game, and only two versions (Korean and English) are provided, which may be subject to change in the future.
| Language | SMS (Mobile Phone) | |
|---|---|---|
| Korean | Please enter the (Game Name) verification code XXXXXX. The expiration time is 3 minutes. | Enter the verification code below to complete verification. (Valid time: 10 minutes) Verification code: XXXX |
| English | (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 |
User Flow
When applied to the actual service, the process will appear to the user as follows. (Based on the contact registration scenario)
- Obtain consent for the collection and use of personal information and receive the contact information.
- The game checks the input format and the limit on the number of sends, then requests the verification code to be sent.
- The user enters the verification code received via SMS or email within the valid time.
- If verification is successful, registration is complete. If it fails, display a guide according to the reason and lead to re-entry or re-sending.
The screens viewed by the user must be implemented directly by the game
ㆍ STOVE only provides APIs for sending and verifying verification codes. UI elements viewed by the user, such as the contact input field, consent pop-up, and verification code input screen, must be implemented directly by the game.
ㆍ Below are examples of the mobile number verification consent pop-up and the verification code input screen.
Notes on Guidance Text
ㆍ The game determines how specifically to display failure messages. While more specific messages are more convenient for users, they can provide hints for brute-force attempts, so a balance between security and convenience is required.
ㆍ For multilingual services, you need to prepare guidance text for each language. Stove only provides verification code messages in Korean and English.
Items to discuss before implementation
Possession verification can only be used after game-specific settings are registered. Please discuss the items below with your business representative before implementation.
| Item | Reason for discussion |
|---|---|
| Usage channel (SMS / Email) | Supported channels are registered differently for each game, and sending is impossible through unregistered channels. |
| Target countries for sending | Supported countries are registered separately for each game, requiring a cross-check with the service launch countries (SMS only). |
| Sending text/language | The text is managed by the platform and cannot be changed without prior consultation. Supported languages are Korean and English. |
| Resending policy and sending costs | Prior meeting with the business representative is required. |
| Personal information processing | Pre-determine the storage period and destruction criteria for collected information, and conduct a legal review on whether to reflect it in the privacy policy. |
The game manages the collected information directly
ㆍ Stove is only responsible for sending and verification. It does not store the verified contact information on your behalf.
ㆍ The game determines storage, retention period, destruction, and encryption.
ㆍ The authentication identification key ("possession_key") is also treated as personal information. It can be used to determine duplicate participation, and its use is optional.
ㆍ Legal review is required before implementation. Please check whether to reflect it in the privacy policy, and for global services, check the laws of each country (e.g., GDPR).
Development
Identity Verification
Identity verification integration involves different areas depending on the environment (Mobile SDK / PC SDK / Web / Server). Calling the authentication screen in the game client is handled in the Mobile/PC SDK/Web sections, while simKey issuance, result inquiry, and SDI reception are handled in the Server section.
Mobile
AuthUI.verifyIdentification is an API that allows the game to actively trigger the identity verification screen. Call it when re-verification is required before entering payment or sensitive functions.compareIdentifier value is used to distinguish between SDI verification and platform identity verification.
The Mobile SDK provides the AuthUI.verifyIdentification interface, which automatically displays the identity verification screen as a webview. The SDK only handles displaying the screen, while simKey issuance, result retrieval, and SDI issuance are performed via server APIs.
Prerequisites
Auth.initializeandAuthUI.loginmust be completed, andAuth.AccessTokenmust be in a valid state.- Decide on the
compareIdentifiervalue (SDI / Platform Identity Verification) before calling.- Platform-based Identity Verification (Type1) (
compareIdentifier = false): The verification comparison value is returned in the callback'sstate. Verify this value on the game server to confirm identity match. - SDI Issuance Identity Verification (Type2) (
compareIdentifier = true): The callback'sstateis empty. The SDK handles verification directly with the SDI system.
- Platform-based Identity Verification (Type1) (
Development Flow
- Determine Call Timing: Check if it is a point where re-verification is needed, such as just before entering payment or sensitive functions.
- Check AccessToken: If
Auth.AccessTokenisnull, guide the user to the login flow first. - Call
AuthUI.verifyIdentification(compareIdentifier, callback): Pass the pre-determinedcompareIdentifiervalue. - Handle Results:
- Success +
statevalue exists (compareIdentifier = false): Send thestateto the game server for verification before allowing access to subsequent functions. - Success +
statevalue is empty (compareIdentifier = true): SDI verification is complete, so proceed directly to subsequent functions. - Failure/Cancellation: Guide the user to retry and block access to subsequent functions.
- Success +
Troubleshooting
| Situation | Cause | Solution |
|---|---|---|
| Callback fails immediately upon calling | Called while Auth.AccessToken is in null (login incomplete) state. | Check Auth.AccessToken != null before calling, and if null, call AuthUI.login first to issue a token. |
Success but state value is empty | It was a compareIdentifier = true (SDI verification) call. When using SDI, state is normally empty. | If a platform identity verification comparison value is needed, call with compareIdentifier = false. |
Received state but there is no game server verification step | For platform identity verification, the game server must perform state verification to confirm identity match. Passing based solely on the presence of state on the client side poses a bypass risk. | Send state to the game server to verify against the Stove authentication server before allowing access to subsequent functions. |
Sample Code
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 is a ViewSDK API that displays the identity verification screen as a webview in PC online games. The SDK handles everything from simKey issuance to displaying the webview, and delivers the simKey via the onDestroy callback after verification is complete. Call it when re-verification is required before entering payment or sensitive functions.
The SDK handles everything up to displaying the screen and receiving the simKey, while verification result retrieval and SDI reception are performed via server APIs.
This feature only works in South Korea. Calling it in countries other than South Korea will return an NOT_SUPPORTED_COUNTRY(31) error.
This feature is not supported when running via the Steam Launcher. Therefore, it cannot be used if running through the Steam Launcher.
To use ViewSDK features, BaseSDK integration and initialization must precede ViewSDK initialization. If BaseSDK is not initialized, ViewSDK features cannot be used.
Prerequisites
- BaseSDK initialization → ViewSDK initialization must be completed.
- Decide on the
CompareIdentifiervalue (Type1 / Type2) before calling.- Platform-based Identity Verification (Type1) (
CompareIdentifier = false): simKey is delivered to the callback (onDestroy). Verify this value on the game server to confirm identity match. - SDI Issuance Identity Verification (Type2) (
CompareIdentifier = true): The SDK handles SDI verification directly, and no simKey is delivered.
- Platform-based Identity Verification (Type1) (
- Webview mode (
WebViewMode) to be decided.Internal(SDK embedded webview) /External(external browser) can be selected.
If the STOVE PC SDK is integrated and the game is already running, it means the user is performing an action after completing identity verification. In this case, View_VerifyIdentificationPopup operates as a verification check to confirm the user's identity when they are already authenticated.
Development Flow
- Check Initialization: Ensure that BaseSDK and ViewSDK initialization is complete.
- Determine Call Timing: Determine if re-authentication is required, such as immediately before entering payment or sensitive functions.
- Set Parameters and Call: Call
mode(WebViewMode) andcompareIdentifieras arguments toView_VerifyIdentificationPopup. onFinishedHandling**: Check for success or failure of the popup call (webview execution).onDestroyHandling**: Called when the popup closes.CompareIdentifier = false(Type1): simKey is delivered. Send the simKey to the game server to verify the result before allowing access to subsequent functions. (Refer to the Server section)CompareIdentifier = true(Type2): Since SDI verification is complete, proceed directly to subsequent functions.
Troubleshooting
| Situation | Cause | Solution |
|---|---|---|
NOT_SUPPORTED_COUNTRY (31) error occurs during call | Called from a country other than South Korea. This function only works in South Korea. | Only call the identity verification popup in a South Korean environment. |
| Fails immediately upon calling | Called before BaseSDK/ViewSDK initialization was complete. | Complete BaseSDK initialization → ViewSDK initialization before calling. |
onDestroy has an empty simKey | It was a CompareIdentifier = true (Type2, SDI) call. simKey is not normally delivered when using SDI. | If platform identity verification comparison values are required, call with CompareIdentifier = false. |
| Received simKey but there is no game server verification step | For Type1, the game server must verify the simKey to confirm identity. There is a risk of bypass if the client grants access based solely on the existence of the simKey. | Send the simKey to the game server to verify the result before allowing access to subsequent functions. (Refer to the Server section) |
Sample Code
CompareIdentifier according to the Type. (false: Type1 simKey delivery / true: Type2 SDI verification)
cpp // 1. Include the ViewSDK module header at the top.
include "ViewSDK.h"
// 2. Declare the use of namespaces containing APIs and structures. using namespace Stove::PCSDK; using namespace Stove::PCSDK::Base; using namespace Stove::PCSDK::View;
void View_VerifyIdentificationPopup_Example() { // 3. Set webview mode (INTERNAL / EXTERNAL) WebViewMode mode{ WebViewMode::INTERNAL };
// false : simKey delivery (Type1), true : SDI verification (Type2) bool compareIdentifier = false;
// 4. Call the verification popup to proceed with identity verification. View_VerifyIdentificationPopup(compareIdentifier, mode, [](CallbackResult callbackResult) { if (callbackResult.GetResult().IsSuccessful()) { // Handle successful verification popup call UE_LOG(LogTemp, Log, TEXT("Verification popup succeeded.")); } else { // Handle failed verification popup call UE_LOG(LogTemp, Log, TEXT("Verification popup failed.")); } }, [](CallbackResult callbackResult, const wchar_t* simkey) { // Handle when the popup is closed // simkey is the value passed after identity verification is completed. (when compareIdentifier=false) UE_LOG(LogTemp, Log, TEXT("simkey : %s"), *FString(simkey)); if (callbackResult.GetResult().IsSuccessful()) { // Handle successful popup closure UE_LOG(LogTemp, Log, TEXT("Verification popup closed successfully.")); } else { // Handle failed popup closure UE_LOG(LogTemp, Log, TEXT("Failed to close verification popup.")); } }); }
:::
:::dc-tab-item{label="Unity"}
```cs
// 1. A using statement for the ViewSDK module is required at the top.
using static Stove.PCSDK.Base;
using static Stove.PCSDK.View;
// 2. Variable declaration for storing callback implementations
private OnPopupFinished _onVerifyPopupFinished;
private OnVerifyIdentificationPopupDestroyFinished _onVerifyPopupDestroyFinished;
// Function to call the identity verification popup
public void OnClickViewVerifyIdentificationPopup()
{
// 3. Set WebView mode (INTERNAL/EXTERNAL)
WebViewMode mode = WebViewMode.INTERNAL;
// false : pass simKey (Type1), true : SDI verification (Type2)
bool compareIdentifier = false;
// 4. Register callback for identity verification popup call result
_onVerifyPopupFinished = (CallbackResult callbackResult) =>
{
if (callbackResult.result.IsSuccessful())
{
// Handle successful identity verification popup call
Debug.Log("Identity verification popup call successful");
}
else
{
// Handle failed identity verification popup call
Debug.Log("Identity verification popup call failed");
}
};
// Register callback for identity verification popup termination
_onVerifyPopupDestroyFinished = (CallbackResult callbackResult, string simkey) =>
{
// simkey is the value passed after identity verification is completed. (when compareIdentifier=false)
Debug.Log($"Identity verification simkey: {simkey}");
if (callbackResult.result.IsSuccessful())
{
// Handle normal identity verification popup termination
Debug.Log("Identity verification popup terminated normally");
}
else
{
// Handle failed identity verification popup termination
Debug.Log("Identity verification popup termination failed");
}
};
// 5. Call the verification popup to proceed with identity verification.
View_VerifyIdentificationPopup(compareIdentifier, mode, _onVerifyPopupFinished, _onVerifyPopupDestroyFinished);
}
Web
Stove identity verification page(https://accounts.onstove.com/verification)를 This is the area where the user is prompted to proceed with identity verification by opening it in a browser. The simKey (state) issued by the game server is passed as a URL parameter, and after verification is complete, it leads to the game server's result inquiry.
Prerequisites
- Issuing a simKey (state) from the game server must be done first. Refer to the Server section below for issuance.
- Use the identity verification page host for each environment.
- Live:
https://accounts.onstove.com - Sandbox:
https://accounts.gate8.com
- Live:
- Depending on the identity verification method (Type), the URL's
typevalue and the type of simKey that must be issued in advance differ.- Platform-based identity verification (Type1): Compares the Stove member's CI with the verification result CI to check if they are the same person. When issuing a simKey,
platform_type=PC_WEB_CHECK - SDI issuance identity verification (Type2): Issues a one-way encrypted SDI of the CI. Cannot determine if it matches the Stove member; managed by the CP company itself. When issuing a simKey,
platform_type=CP_GAME
- Platform-based identity verification (Type1): Compares the Stove member's CI with the verification result CI to check if they are the same person. When issuing a simKey,
- If using an external browser flow, remove
redirect_urland guide the user to return to the game manually after verification is complete.
Development Flow
- Receive the simKey (state) from the game server.
- Assemble the
type/state/redirect_url/langinto the query string for the host of each environment. - Open the identity verification page in a browser. In mobile environments, an external browser (Safari/Chrome) is recommended instead of an in-app browser. (Some PASS/carrier certifications block in-app browsers)
- The user proceeds with identity verification.
- After verification is complete, return to
redirect_url(or for external browser flows, the user returns to the game). - Request the result from the game server to proceed with follow-up processing. (Refer to the Server section)
| Branch | URL type | simKey to issue in advance |
|---|---|---|
| Type1 for comparison (Registered identity verification member) | COMPARE_IDENTIFY | POST /sim/v1/compare (platform_type=PC_WEB_CHECK) |
| Type1 for initial registration (Unregistered identity verification member) | STORAGE_IDENTIFY | POST /sim/v1/cert (platform_type=PC_WEB) |
| Type2 SDI issuance | COMPARE_IDENTIFY | POST /sim/v1/compare (platform_type=CP_GAME) |
Troubleshooting
| Situation | Cause | Solution |
|---|---|---|
| A blank screen appears after opening the page | state value is incorrect or expired (over 10 minutes) | Issue a new simKey just before entering the page. |
| Mixing hosts for different environments | Accessing the Live host with a state issued in Sandbox (or vice versa) | Match the state issuance environment with the page host environment. |
Incorrect type usage | It is personVerifyYn=Y but STORAGE_IDENTIFY is used (or vice versa) | Check personVerifyYn first by querying member information, then branch to COMPARE_IDENTIFY / STORAGE_IDENTIFY. |
| Identity verification fails in an in-app browser | Some carrier PASS certifications block in-app browsers | Open in an external browser. In the external browser flow, remove redirect_url and trigger the result inquiry with an in-game guide popup. |
| It is Type2 but SDI is not issued | When issuing simKey, platform_type value is not CP_GAME | Specify platform_type=CP_GAME in the simKey issuance call. (Refer to the Server section) |
Sample Code
This is a URL assembly example. Replace {state} with the value issued by the game server.
# 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
This is the area where the game server calls the Stove identity verification API to issue simKeys and inquire about results.
Type1 is a Pull method where the game server calls the result inquiry API, and Type2 SDI is a Push method where Stove delivers the SDI to the game server.
Prerequisites
- It must be structured to receive the Game User Access Token from the client. Use it in the form of
Bearer {Game User Access Token}in the Authorization header of the simKey issuance API. - Type branch decision: Type1 (Platform-based identity verification) / Type2 (SDI issuance identity verification)
- If Type2 SDI is selected, you must separately implement an SDI receiving endpoint (e.g.,
POST /compareResult) on the game server, and request the responsible technical PM to register the receiving endpoint and ACL so that Stove → game server calls are possible. Refer to troubleshooting for implementation precautions.
- If Type2 SDI is selected, you must separately implement an SDI receiving endpoint (e.g.,
Development Flow
- (Optional) Check
GET /member/v3.0/{game_id}/memberinfovia the member information inquiry API (personVerifyYn) to determine if identity verification is registered →COMPARE_IDENTIFYvsSTORAGE_IDENTIFYBranch decision. This call requires an API Access Token. (See above for the issuance procedure 인증 메뉴 → Server ) 2. simKey(state) issuance - For Type1 comparison:POST /sim/v1/compare(platform_type=PC_WEB_CHECK) - For Type1 initial registration:POST /sim/v1/cert(platform_type=PC_WEB) - For Type2 SDI:POST /sim/v1/compare(platform_type=CP_GAME) 3. Deliver the issued state to the client, and the user performs authentication on the identity verification page. (See Web section) 4. Result processing branch after authentication completion: - Type1: Game server calls result inquiry API - Reusable:GET /sim/v1/compare/check?state=...— Can be inquired multiple times - One-time:DELETE /sim/v1/compare/expire?state=...— state expires after inquiry (recommended for security) - Type2 SDI: Stove sendsPOST /compareResult/state/sdito the game server'sy_ageendpoint → Game server saves asguid+sdi+ authentication content mapping```mermaid sequenceDiagram autonumber participant W as Game Web/Client participant GS as Game Server participant A as STOVE Server W ->> GS : simKey(state) 발급 요청 (User Access Token 전달) GS ->>+ A : POST /sim/v1/compare (Auth: Bearer User Access Token) A -->>- GS : state GS -->> W : state Note over W,A: 이용자가 본인인증 페이지에서 인증 (Web 섹션 참고) alt Type1 — 게임 서버가 결과 조회 (Pull) GS ->>+ A : GET /sim/v1/compare/check?state=... (또는 DELETE /expire) A -->>- GS : code 0 / 91031 / 93000 else Type2 SDI — STOVE가 게임 서버 콜백 (Push) A ->>+ GS : POST /compareResult (state, sdi, y_age) GS ->> GS : guid + sdi + 콘텐츠 매핑 저장 GS -->>- A : HTTP 200 end
##### Troubleshooting
| Response code | Situation | Resolution |
|---|---|---|
| 91030 | Wrong Request — Missing required parameter or invalid value | Check the values in the request Body for `service` / `platform_type` / `cert_type` / `game_id` values. |
| 91031 | Not Verified — Result inquiry called before identity verification is complete | Verify that the user has completed the verification on the identity verification page, then try again. |
| 93000 | Redis compare data is null — state expired (10 minutes) / invalid state / state already queried via one-time API | Obtain a new simKey immediately before entering the page. |
| 40000 / 40101 / 40103 | API Access Token / User Access Token authentication error | Refer to the Authentication menu → Server Troubleshooting. Retry after reissuing the token. |
> [!warning]
> **Precautions for implementing Type2 SDI receiving endpoint**
> · Stove considers the transmission complete when the game server responds with HTTP `2xx`. Since it **retries on `5xx` / `4xx` responses**, SDI may be received redundantly with the same `state`. Please ensure idempotency (prevention of duplicate storage) at the `state` level.
> · The endpoint must operate on **HTTPS + TLS 1.2 or higher**.
> · **Infrastructure ACL registration** is required for Stove → game server calls. Please provide the game server domain/IP to the assigned technical PM.
> · Even for the same user, **different SDIs are generated for Sandbox / Live environments**, so separate storage by environment is required.
> [!info]
> **SDI is personal information**
> Do not record it in logs as-is; apply masking or exclusion. When a user withdraws from the game, the mapped SDI must be deleted.
##### Sample Code
<p style="color:#314158;font-family:'Spoqa Han Sans Neo';font-size:16px;font-weight:400;line-height:26px;letter-spacing:-0.48px;">This is an example of calling Type1 comparison simKey issuance (<code>POST /sim/v1/compare</code>) and multi-use result inquiry (<code>GET /sim/v1/compare/check</code>). <br>For Type1 initial registration, change the endpoint to <code>/sim/v1/cert</code>, and for Type2 SDI, replace <code>platform_type</code> with <code>CP_GAME</code>.</p>
> [!warning]
> **Please replace the Base URL according to the environment (Live/Sandbox)**
> In production environments, it is recommended to separate it into environment variables (e.g., `STOVE_API_BASE_URL`) or framework configuration files and inject it per environment instead of hardcoding.```java
// 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 등)
}
}
=== "Java (25 LTS)"
// .NET 10 LTS — System.Net.Http.HttpClient (표준 라이브러리)
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var baseUrl = "https://api.onstove.com";
var serviceId = "STOVE_GAME";
var callerId = $"{serviceId}_SERVER";
using var client = new HttpClient { BaseAddress = new Uri(baseUrl) };
client.DefaultRequestHeaders.Add("caller-id", callerId);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", userAccessToken);
// 1) simKey(state) 발급 (Type1 대조용)
var issueRes = await client.PostAsJsonAsync("/sim/v1/compare", new
{
service = "STOVE",
platform_type = "PC_WEB_CHECK",
cert_type = "SELF",
game_id = serviceId,
});
var issueData = await issueRes.Content.ReadFromJsonAsync<JsonElement>();
if (issueData.GetProperty("code").GetInt32() != 0)
{
// 트러블슈팅 표 참고 (91030 등)
throw new InvalidOperationException($"simKey issue failed: {issueData.GetProperty("code")}");
}
var state = issueData.GetProperty("value").GetString();
// 2) 본인인증 결과 조회 (다회용)
// 이용자가 웹에서 본인인증을 마친 시점 이후에 호출
var encodedState = Uri.EscapeDataString(state);
var verifyRes = await client.GetAsync($"/sim/v1/compare/check?state={encodedState}");
var verifyData = await verifyRes.Content.ReadFromJsonAsync<JsonElement>();
if (verifyData.GetProperty("code").GetInt32() == 0)
{
// 본인인증 완료 — 후속 처리 진행
}
else
{
// 트러블슈팅 표 참고 (91031 / 93000 등)
}
=== "C# (.NET 10 LTS)" https://api.onstove.com" serviceID = "STOVE_GAME" )
var ( callerID = serviceID + "_SERVER" client = &http.Client{Timeout: 5 * time.Second} )
ctx := context.Background()
// 1) simKey(state) issuance (for Type1 comparison) issueBody, _ := json.Marshal(mapstringstring{ "service": "STOVE", "platform_type": "PC_WEB_CHECK", "cert_type": "SELF", "game_id": serviceID, }) issueReq, _ := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/sim/v1/compare", bytes.NewReader(issueBody)) issueReq.Header.Set("Content-Type", "application/json") issueReq.Header.Set("Authorization", "Bearer "+userAccessToken) issueReq.Header.Set("caller-id", callerID) issueRes, err := client.Do(issueReq) if err != nil { return fmt.Errorf("simKey request: %w", err) } defer issueRes.Body.Close()
var issueData struct {
Code int json:"code"
Value string json:"value"
}
if err := json.NewDecoder(issueRes.Body).Decode(&issueData); err != nil {
return err
}
if issueData.Code != 0 {
// Refer to the troubleshooting table (e.g., 91030)
return fmt.Errorf("simKey issue failed: code=%d", issueData.Code)
}
state := issueData.Value
// 2) Verify identity result (multi-use) // Called after the user completes identity verification on the web verifyURL := baseURL + "/sim/v1/compare/check?state=" + url.QueryEscape(state) verifyReq, _ := http.NewRequestWithContext(ctx, http.MethodGet, verifyURL, nil) verifyReq.Header.Set("caller-id", callerID) verifyRes, _ := client.Do(verifyReq) defer verifyRes.Body.Close()
var verifyData struct {
Code int json:"code"
}
_ = json.NewDecoder(verifyRes.Body).Decode(&verifyData)
if verifyData.Code == 0 {
// Identity verification complete — proceed with follow-up processing
} else {
// Refer to the troubleshooting table (91031 / 93000, etc.)
}
=== "Node.js (24 LTS)"
```javascript
// Node.js 24 LTS — Built-in fetch (ESM)
const baseUrl = 'https://api.onstove.com';
const serviceId = 'STOVE_GAME';
const callerId = `${serviceId}_SERVER`;
// 1) Issue simKey(state) (for Type1 comparison)
const issueRes = await fetch(`${baseUrl}/sim/v1/compare`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${userAccessToken}`,
'caller-id': callerId,
},
body: JSON.stringify({
service: 'STOVE',
platform_type: 'PC_WEB_CHECK',
cert_type: 'SELF',
game_id: serviceId,
}),
});
const issueData = await issueRes.json();
if (issueData.code !== 0) {
// Refer to the troubleshooting table (91030, etc.)
throw new Error(`simKey issue failed: ${issueData.code}`);
}
const state = issueData.value;
// 2) Verify identity result (multi-use)
// Called after the user completes identity verification on the web
const encodedState = encodeURIComponent(state);
const verifyRes = await fetch(`${baseUrl}/sim/v1/compare/check?state=${encodedState}`, {
method: 'GET',
headers: {
'caller-id': callerId,
},
});
const verifyData = await verifyRes.json();
if (verifyData.code === 0) {
// Identity verification complete — proceed with follow-up processing
} else {
// Refer to the troubleshooting table (91031 / 93000, etc.)
}
<div style="margin-top:10px;"></div>
### Possession Verification (SMS/Email)
<p style="color:#314158;font-family:'Spoqa Han Sans Neo';font-size:16px;font-weight:400;line-height:26px;letter-spacing:-0.48px;">Possession verification is structured so that the game server calls the two steps of the STOVE possession verification API (Send → Verify), while the game client is only responsible for the input and result screen UX. SMS and Email use <strong>the same endpoint</strong> and are distinguished only by the <code>channel</code> value. There is no SDK call, and all external communication occurs between the game server and the STOVE API, so game server implementation is central. <br>Refer to the troubleshooting table if you need detailed meanings of response codes.</p>
##### Prerequisites
- Channels to be used (SMS / Email), countries supported for message sending, and cost settlement must be discussed in advance with the business manager. Only agreed-upon channels are registered per game, and requesting an unregistered channel will return
70115. - Obtain a Platform API Access Token to call the STOVE possession verification API. (Refer to the above 인증(토큰·검증) → Server for the issuance procedure)
- Game (service) identification is derived from the Platform API Access Token's
sid. Other usage areas (official website, etc.) also use the issued Platform API Access Token, and in these cases, API calls must also be made from the backend. - When calling the API, use the Game ID registered in Partners for the
caller-idheader value. (For caller identification and monitoring) - Prepare screens that match the game's tone and manner (country selection + phone number or email input, privacy consent, verification code input, result notification popup) in advance.
- Check policy limits in advance. Re-sending for the same target is allowed up to 3 times per minute / 20 times per day for SMS, and 5 times per hour for Email. Incorrect verification code entry is allowed up to 5 times for both SMS and Email.
Development Flow
The implementation scope is divided as follows. The game client handles only the UX, while the game server handles STOVE API calls and result storage.
| Responsibility | Implementation Scope |
|---|---|
| Game Client | Input screens for each channel (country code selector/phone number input / email input), privacy consent, "Get Verification Code" button (including debounce), verification code input screen (6 digits for SMS / 4 digits for Email), result/error notification popup |
| Game Server | API Access Token storage, channel·to assembly/validation (SMS uses E.164 normalization), calling both STOVE APIs and handling responses, exponential backoff retries on timeout |
| STOVE API | Verification code generation/sending (SMS / Email) / code validity verification / possession_key issuance |
Implementation proceeds in the following order.
- (Client) Configure verification target input screen:
smsdisplays a country code selector and number input field, whileemaildisplays an email input field; activate the "Get Verification Code" button only after privacy consent. Use debounce/cooldown on the button to block repeated clicks in a short time. - (Client → Server) Transfer verification target: Send user input to the game server's own endpoint. If it is
channel=sms, the game server should normalizetoto+{국가코드}{번호}format (E.164) (e.g.,+821012345678), and if it ischannel=email, verify the email format. - (Server → STOVE) Request possession verification code: Call
POST /sms-sender/v3.2/occupancy_auth/send_codewithchannel+to(addlocalefor Email). Branch based on the responsecode, and upon success, usevalue.code_length·value.expires_into configure the verification code input screen's digit count and remaining time timer. - (Client → Server) Receive/Transfer verification code: When the user enters the code received via SMS (6 digits) or Email (4 digits), send it to the game server. The game server should perform whitespace/length validation first before proceeding to the next call.
- (Server → STOVE) Request possession verification code confirmation: Call
POST /sms-sender/v3.2/occupancy_auth/verify_codewithchannel+to+verification_code. Branch based on the responsecode.code = 0→ Success. Receivevalue.possession_keyand proceed to the next step.code = 70103→ Authentication code mismatch. Display only "Authentication failed, please try again" to the user and process the code re-entry.code = 70104→ The authentication code is missing, expired, or invalidated. Do not just re-enter; instruct the user to restart from step 3 (resend).
- (Server)
possession_keyUtilization (Optional): If you need to determine whether the authentication target is a duplicate, you can use the receivedpossession_key. Please decide on the usage and management method according to your development company's policy.
!infoSending Language(
locale)
ㆍchannel=email:localeDetermined by the value (ko/en). If not specified, it isen, and values other thanko/enare replaced withen.
ㆍchannel=sms: Automatically determined based on the phone number's country code (Korea=Korean, others=English). Thelocalevalue is ignored.
!infoEmail Sending Information
ㆍ Sender:STOVE <noreply@smilegate.com>
ㆍ Subject: Korean 「STOVE Verification Email Guide」 / English 「STOVE Verification Email Guide」
ㆍ If a user inquires about not receiving the email, advise them to check their spam folder first.
!warningKey Implementation Precautions
ㆍ Sent authentication codes are valid until the expiration time (SMS 3 minutes / Email 10 minutes), but if even one code is successfully verified, all authentication codes sent to the same target (phone number or email) will expire.
ㆍ If the authentication code is entered incorrectly 5 times, the authentication code for that target will be completely invalidated (common for SMS/Email). Subsequent verification requests will return70104, so restart from the resend process. The failure count is reset upon successful authentication or when the authentication code is resent.
ㆍ The number of digits for the authentication code varies by channel (SMS 6 digits / Email 4 digits), so configure the input UI based on thevalue.code_lengthof the sending response.
ㆍ The "Get Authentication Code" button requires prevention of rapid repeated clicks. (Debounce + client cooldown recommended)
ㆍ When callingverify_code, performverification_codeempty value validation on the game server first. Do not call the API with empty values.
ㆍ API response codes should be handled with branching logic on the server side, and only display Success / Authentication Failed / Guide popup forms to the frontend. Exposing raw response codes may reveal internal platform settings or the reasons for authentication attempt success/failure to the outside.
Troubleshooting
Possession Verification Code Request API Response Codes
| HTTP | Code | Situation | Handling Method |
|---|---|---|---|
| 200 | 0 | Request for sending received | value.code_length·value.expires_inConfigure and transition to the authentication code entry screen using |
| 200 | 70100 | invalid parameter | channel / to Check mandatory values and format, then retry call |
| 200 | 70101 | Resend limit exceeded | SMS 3 times per 1 minute / Email 5 times per 1 hour exceeded. Advise user to retry |
| 200 | 70105 | 20 times per day exceeded (SMS only) | Advise user to "try again after 24 hours" (Not applicable to Email) |
| 200 | 70107 | Unsupported country (SMS) | Advise user on supported countries (Not applicable to Email) |
| 200 | 70108 | Game (service) not registered | Advise to "Contact Customer Support" (Request game registration via Game Tech representative) |
| 200 | 70109 | Occupancy auth sending policy not registered | Advise to "Contact Customer Support" (Request game policy registration via Game Tech representative) |
| 200 | 70110 | Phone number format error (SMS) | to Check E.164 format of value, then retry call |
| 200 | 70115 | Unregistered channel used | channel Check value and retry call; if failed, advise to "Contact Customer Support" (Request game channel registration via Game Tech representative) |
| 200 | 70116 | Email format error (Email) | to Check email format of value, then retry call |
| 200 | 70199 | Stove internal error | Exponential backoff retry; if failed, advise to "Contact Customer Support" |
| 401 | 40100 / 40101 / 40102 / 40103 / 40104 / 40106 / 40302 / 50000 | Token error | Reissue API Access Token and retry |
| 408 | 40800 | Request Timeout | Exponential backoff retry |
| 429 | 42900 | Too Many Requests | Exponential backoff retry |
| 504 | 50400 | Gateway Timeout | Exponential backoff retry |
Occupancy Authentication Code Verification API Response Code
| HTTP | Code | Situation | Handling Method |
|---|---|---|---|
| 200 | 0 | Authentication successful | Allow entry to content |
| 200 | 70100 | invalid parameter | channel / to / verification_code Check format (including whitespace) |
| 200 | 70103 | Authentication code mismatch | Advise user "Authentication failed" + handle re-entry (retry possible within valid time, invalidated upon 5 attempts) |
| 200 | 70104 | No authentication code issued | Expired/not issued or invalidated due to 5 incorrect attempts. Advise user to resend |
| 200 | 70109 | Occupancy auth policy not registered | Advise to "Contact Customer Support" (Request game policy registration via Game Tech representative) |
| 200 | 70110 | Phone number format error (SMS) | to Check E.164 format of value, then retry call |
| 200 | 70115 | Unregistered channel used | channel Check value and retry call; if failed, advise to "Contact Customer Support" (Request game channel registration via Game Tech representative) |
| 200 | 70116 | Email format error (Email) | to Check email format of value, then retry call |
| 200 | 70199 | Stove internal error | Exponential backoff retry; if failed, advise to "Contact Customer Support" |
| 401 | 40100 / 40101 / 40102 / 40103 / 40104 / 40106 / 40302 / 50000 | Token error | Reissue API Access Token and retry |
| 408 | 40800 | Request Timeout | Exponential backoff retry |
| 504 | 50400 | Gateway Timeout | Exponential backoff retry |
Sample Code
This is an example of calling the occupancy authentication code request (POST /sms-sender/v3.2/occupancy_auth/send_code) and verification (POST /sms-sender/v3.2/occupancy_auth/verify_code) from the game server.
Each API is ① Set variable values → ② Prepare request parameters → ③ Create Request → ④ Call API → ⑤ Process response organized into 5 steps. The request parameters are declared as variables with the same names as the Header/Body items in the reference document, so simply change the values to use them, and specify the channel as channel value sms / email.
!warningPlease replace the Base URL according to the environment (Live/Sandbox)
ㆍ Live:https://api.onstove.com
ㆍ Sandbox:https://api.gate8.com
In production environments, it is recommended to separate them into environment variables (e.g.,STOVE_API_BASE_URL) or configuration files instead of hardcoding, and inject them per environment.
=== "Java" java // 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
// 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
// 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(); } }
=== "C#"
```csharp
// System.Net.Http.HttpClient (standard library)
// Sample for calling the occupancy auth APIs in order: send code -> verify code.
// Each API is organized into STEP 1-5.
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
public static class OccupancyAuthSample
{
// Base URL : Live https://api.onstove.com / Sandbox https://api.gate8.com
private static readonly HttpClient Client = new()
{
BaseAddress = new Uri("https://api.onstove.com"),
};
// ==============================================================================
// [API 1] Send verification code POST /sms-sender/v3.2/occupancy_auth/send_code
// ==============================================================================
public static async Task<JsonElement> SendCodeAsync(string apiAccessToken, string channel, string to, string locale)
{
// STEP 1. Set variables
var path = "/sms-sender/v3.2/occupancy_auth/send_code";
var callerId = "STOVE_GAME_ID"; // (required) Game ID registered in Partners
var callerDetail = "f248a284-7e61-4dcf-a111-a394050e9555"; // (optional) Caller UUID, for monitoring
// STEP 2. Prepare request parameters (Body)
var body = new Dictionary<string, string>
{
["channel"] = channel, // (required) Auth channel : "sms" | "email"
["to"] = to, // (required) Auth target : sms = "+821012345678" / email = "user@example.com"
};
if (channel == "email")
{
body["locale"] = locale; // (optional) Email-only language : "ko" | "en" (defaults to "en")
}
// STEP 3. Build the request (Header)
using var request = new HttpRequestMessage(HttpMethod.Post, path)
{
Content = JsonContent.Create(body), // (required) Content-Type: application/json
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiAccessToken); // (required) API Access Token (see /docs/stove/guide/authentication#server)
request.Headers.Add("caller-id", callerId); // (required)
request.Headers.Add("caller-detail", callerDetail); // (optional)
// STEP 4. Call the API
var response = await Client.SendAsync(request);
// STEP 5. Handle the response
if (!response.IsSuccessStatusCode)
{
// 401 : reissue the token / 408, 429, 504 : retry with exponential backoff
throw new InvalidOperationException($"send_code http {(int)response.StatusCode}");
}
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
var code = result.GetProperty("code").GetInt32(); // 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 InvalidOperationException($"send_code failed: code={code}");
}
var codeLength = result.GetProperty("value").GetProperty("code_length").GetInt32(); // Code length (sms 6 / email 4)
var expiresIn = result.GetProperty("value").GetProperty("expires_in").GetInt32(); // 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 async Task<string> VerifyCodeAsync(string apiAccessToken, string channel, string to, string verificationCode)
{
// STEP 1. Set variables
var path = "/sms-sender/v3.2/occupancy_auth/verify_code";
var callerId = "STOVE_GAME_ID";
var callerDetail = "f248a284-7e61-4dcf-a111-a394050e9555";
// STEP 2. Prepare request parameters (Body)
// NOTE: Do not call the API when verification_code is empty. Validate blank and length first.
if (string.IsNullOrWhiteSpace(verificationCode))
{
throw new ArgumentException("verification_code is empty");
}
var body = new Dictionary<string, string>
{
["channel"] = channel, // (required) Same channel as the send request
["to"] = to, // (required) Same auth target as the send request
["verification_code"] = verificationCode, // (required) Code entered by the user (sms 6 digits / email 4 digits)
};
// STEP 3. Build the request (Header)
using var request = new HttpRequestMessage(HttpMethod.Post, path)
{
Content = JsonContent.Create(body),
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiAccessToken);
request.Headers.Add("caller-id", callerId);
request.Headers.Add("caller-detail", callerDetail);
// STEP 4. Call the API
var response = await Client.SendAsync(request);
// STEP 5. Handle the response
if (!response.IsSuccessStatusCode)
{
// 401 : reissue the token / 408, 504 : retry with exponential backoff
throw new InvalidOperationException($"verify_code http {(int)response.StatusCode}");
}
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
var code = result.GetProperty("code").GetInt32();
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 InvalidOperationException($"verify_code failed: code={code}");
}
// Verified. possession_key can be used to check whether the auth target is a duplicate. (optional)
return result.GetProperty("value").GetProperty("possession_key").GetString();
}
}
=== "Go"
// net/http + encoding/json (standard library)
// Sample for calling the occupancy auth APIs in order: send code -> verify code.
// Each API is organized into STEP 1-5.
package occupancyauth
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
)
// Base URL : Live https://api.onstove.com / Sandbox https://api.gate8.com
const baseURL = "https://api.onstove.com"
var httpClient = &http.Client{}
// Verify API response codes that require user guidance.
var (
ErrCodeMismatch = errors.New("verification code mismatch") // 70103
ErrCodeNotFound = errors.New("no verification code") // 70104
)
// SendCodeResponse is the response of the send-code API.
type SendCodeResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Value struct {
Channel string `json:"channel"` // Sent channel
CodeLength int `json:"code_length"` // Code length (sms 6 / email 4)
ExpiresIn int `json:"expires_in"` // Time to live in seconds (sms 180 / email 600)
} `json:"value"`
}
// VerifyCodeResponse is the response of the verify-code API.
type VerifyCodeResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Value struct {
PossessionKey string `json:"possession_key"`
} `json:"value"`
}
// ==============================================================================
// [API 1] Send verification code POST /sms-sender/v3.2/occupancy_auth/send_code
// ==============================================================================
func SendCode(ctx context.Context, apiAccessToken, channel, to, locale string) (*SendCodeResponse, error) {
// STEP 1. Set variables
url := baseURL + "/sms-sender/v3.2/occupancy_auth/send_code"
callerID := "STOVE_GAME_ID" // (required) Game ID registered in Partners
callerDetail := "f248a284-7e61-4dcf-a111-a394050e9555" // (optional) Caller UUID, for monitoring
// STEP 2. Prepare request parameters (Body)
body := map[string]string{
"channel": channel, // (required) Auth channel : "sms" | "email"
"to": to, // (required) Auth target : sms = "+821012345678" / email = "user@example.com"
}
if channel == "email" {
body["locale"] = locale // (optional) Email-only language : "ko" | "en" (defaults to "en")
}
payload, err := json.Marshal(body)
if err != nil {
return nil, err
}
// STEP 3. Build the request (Header)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiAccessToken) // (required) API Access Token (see /docs/stove/guide/authentication#server)
req.Header.Set("Content-Type", "application/json") // (required)
req.Header.Set("caller-id", callerID) // (required)
req.Header.Set("caller-detail", callerDetail) // (optional)
// STEP 4. Call the API
res, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
// STEP 5. Handle the response
if res.StatusCode != http.StatusOK {
// 401 : reissue the token / 408, 429, 504 : retry with exponential backoff
return nil, fmt.Errorf("send_code http %d", res.StatusCode)
}
var result SendCodeResponse
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, err
}
if result.Code != 0 {
// 70101 resend limit / 70105 daily limit / 70107 unsupported country / 70115 unregistered channel ... (see the troubleshooting table)
return nil, fmt.Errorf("send_code failed: code=%d", result.Code)
}
// Pass CodeLength and ExpiresIn in result.Value to the game client to set the input length and the timer.
return &result, nil
}
// ==============================================================================
// [API 2] Verify code POST /sms-sender/v3.2/occupancy_auth/verify_code
// ==============================================================================
func VerifyCode(ctx context.Context, apiAccessToken, channel, to, verificationCode string) (string, error) {
// STEP 1. Set variables
url := baseURL + "/sms-sender/v3.2/occupancy_auth/verify_code"
callerID := "STOVE_GAME_ID"
callerDetail := "f248a284-7e61-4dcf-a111-a394050e9555"
// STEP 2. Prepare request parameters (Body)
// NOTE: Do not call the API when verification_code is empty. Validate blank and length first.
if verificationCode == "" {
return "", errors.New("verification_code is empty")
}
body := map[string]string{
"channel": channel, // (required) Same channel as the send request
"to": to, // (required) Same auth target as the send request
"verification_code": verificationCode, // (required) Code entered by the user (sms 6 digits / email 4 digits)
}
payload, err := json.Marshal(body)
if err != nil {
return "", err
}
// STEP 3. Build the request (Header)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+apiAccessToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("caller-id", callerID)
req.Header.Set("caller-detail", callerDetail)
// STEP 4. Call the API
res, err := httpClient.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
// STEP 5. Handle the response
if res.StatusCode != http.StatusOK {
// 401 : reissue the token / 408, 504 : retry with exponential backoff
return "", fmt.Errorf("verify_code http %d", res.StatusCode)
}
var result VerifyCodeResponse
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return "", err
}
switch result.Code {
case 0:
// Verified. possession_key can be used to check whether the auth target is a duplicate. (optional)
return result.Value.PossessionKey, nil
case 70103:
// Code mismatch -> ask the user to re-enter (all codes are invalidated after 5 failures)
return "", ErrCodeMismatch
case 70104:
// No issued code (expired or invalidated) -> ask the user to request a resend
return "", ErrCodeNotFound
default:
return "", fmt.Errorf("verify_code failed: code=%d", result.Code)
}
}
=== "Node.js"
// Built-in fetch (ESM)
// Sample for calling the occupancy auth APIs in order: send code -> verify code.
// Each API is organized into STEP 1-5.
// No extra import is needed because the built-in fetch is used.
// (On Node.js below 18, a separate package such as "node-fetch" is required.)`import fetch from 'node-fetch';` is required.)
// Base URL : Live https://api.onstove.com / Sandbox https://api.gate8.com
const BASE_URL = 'https://api.onstove.com';
// ==============================================================================
// [API 1] Send verification code POST /sms-sender/v3.2/occupancy_auth/send_code
// ==============================================================================
export async function sendCode(apiAccessToken, channel, to, locale) {
// STEP 1. Set variables
const url = `${BASE_URL}/sms-sender/v3.2/occupancy_auth/send_code`;
const callerId = 'STOVE_GAME_ID'; // (required) Game ID registered in Partners
const callerDetail = 'f248a284-7e61-4dcf-a111-a394050e9555'; // (optional) Caller UUID, for monitoring
// STEP 2. Prepare request parameters (Body)
const body = {
channel, // (required) Auth channel : 'sms' | 'email'
to, // (required) Auth target : sms = '+821012345678' / email = 'user@example.com'
};
if (channel === 'email') {
body.locale = locale; // (optional) Email-only language : 'ko' | 'en' (defaults to 'en')
}
// STEP 3. Build the request (Header)
const request = {
method: 'POST',
headers: {
Authorization: "`Bearer ${apiAccessToken}`, // (required) API Access Token (see /docs/stove/guide/authentication#server)
'Content-Type': 'application/json', // (required)
'caller-id': callerId, // (required)
'caller-detail': callerDetail, // (optional)
},
body: JSON.stringify(body),
};
// STEP 4. Call the API
const response = await fetch(url, request);
// STEP 5. Handle the response
if (!response.ok) {
// 401 : reissue the token / 408, 429, 504 : retry with exponential backoff
throw new Error("`send_code http ${response.status}`);
}
const result = await response.json();
if (result.code !== 0) {
// 70101 resend limit / 70105 daily limit / 70107 unsupported country / 70115 unregistered channel ... (see the troubleshooting table)
throw new Error("`send_code failed: code=${result.code}`);
}
const { code_length: codeLength, expires_in: expiresIn } = result.value;
// Use codeLength (sms 6 / email 4) and expiresIn (sms 180 / email 600) to set the input length and the timer.
return { codeLength, expiresIn };
}
// ==============================================================================
// [API 2] Verify code POST /sms-sender/v3.2/occupancy_auth/verify_code
// ==============================================================================
export async function verifyCode(apiAccessToken, channel, to, verificationCode) {
// STEP 1. Set variables
const url = "`${BASE_URL}/sms-sender/v3.2/occupancy_auth/verify_code`;
const callerId = 'STOVE_GAME_ID';
const callerDetail = 'f248a284-7e61-4dcf-a111-a394050e9555';
// STEP 2. Prepare request parameters (Body)
// NOTE: Do not call the API when verification_code is empty. Validate blank and length first.
if (!verificationCode || verificationCode.trim() === '') {
throw new Error('verification_code is empty');
}
const body = {
channel, // (required) Same channel as the send request
to, // (required) Same auth target as the send request
verification_code: verificationCode, // (required) Code entered by the user (sms 6 digits / email 4 digits)
};
// STEP 3. Build the request (Header)
const request = {
method: 'POST',
headers: {
Authorization: `Bearer ${apiAccessToken}`,
'Content-Type': 'application/json',
'caller-id': callerId,
'caller-detail': callerDetail,
},
body: JSON.stringify(body),
};
// STEP 4. Call the API
const response = await fetch(url, request);
// STEP 5. Handle the response
if (!response.ok) {
// 401 : reissue the token / 408, 504 : retry with exponential backoff
throw new Error(`verify_code http ${response.status}`);
}
const result = await response.json();
if (result.code === 70103) {
// Code mismatch -> ask the user to re-enter (all codes are invalidated after 5 failures)
return null;
}
if (result.code === 70104) {
// No issued code (expired or invalidated) -> ask the user to request a resend
return null;
}
if (result.code !== 0) {
throw new Error(`verify_code failed: code=${result.code}`);
}
// Verified. possession_key can be used to check whether the auth target is a duplicate. (optional)
return result.value.possession_key;
}
<div style="margin-top:70px;"></div>
## Frequently Asked Questions
---
<br />
<details open style="margin:0 0 32px 0;border:1px solid #e3e6ea;border-radius:8px;overflow:hidden;">
<summary style="cursor:pointer;list-style:none;padding:14px 18px;background-color:#F8FAFC;font-size:16px;font-weight:600;color:#314158;">Q1. [Identity Verification] What is the difference between Type1 and Type2?</summary>
<div style="padding:12px 18px;"><div style="font-size:15px;font-weight:400;color:#314158;line-height:1.7;">A. Type1 is a CI-based identity verification method where the Stove platform stores and manages the CI itself to determine if the user is the same person and returns the result. <br>Up to 5 accounts (memberNo) can be linked to a single CI value.<br> Type2 is an SDI-based identification value provision method where the original CI is not stored on the Stove platform, and only the one-way encrypted SDI value is delivered to the CP server. <br>SDI value management is handled by each service provider (CP).</div></div>
</details>
<details open style="margin:0 0 32px 0;border:1px solid #e3e6ea;border-radius:8px;overflow:hidden;">
<summary style="cursor:pointer;list-style:none;padding:14px 18px;background-color:#F8FAFC;font-size:16px;font-weight:600;color:#314158;">Q2. [Identity Verification] Should I choose the multi-use or single-use API when checking the results?</summary>
<div style="padding:12px 18px;"><div style="font-size:15px;font-weight:400;color:#314158;line-height:1.7;">A. If the flow ends after confirming the identity verification result, please use the single-use identity verification result check API (<code>DELETE /sim/v1/compare/expire</code>). <br>The state expires after the result check is complete. If you need to perform checks until the simKey (state) expires, use the multi-use result check API (<code>GET /sim/v1/compare/check</code>). <br>The validity period of the issued simKey (state) is 10 minutes from the initial issuance.</div></div>
</details>
<details open style="margin:0 0 32px 0;border:1px solid #e3e6ea;border-radius:8px;overflow:hidden;">
<summary style="cursor:pointer;list-style:none;padding:14px 18px;background-color:#F8FAFC;font-size:16px;font-weight:600;color:#314158;">Q3. [Possession Verification] What are the validity period and resend limits for the verification code?</summary>
<div style="padding:12px 18px;"><div style="font-size:15px;font-weight:400;color:#314158;line-height:1.7;">A. It varies by channel. SMS has a 6-digit verification code with a 3-minute validity period, and Email has a 4-digit verification code with a 10-minute validity period (you can also check this via <code>value.code_length</code>·<code>value.expires_in</code> in the send response). <br>When requesting for the same target, all sent verification codes remain valid until the expiration time, and if even one verification code is successfully verified, all other codes are invalidated. <br>For resends based on the same game and same target, you can request up to 3 times per minute and 20 times per day for SMS, and 5 times per hour for Email.</div></div>
</details>
<details open style="margin:0 0 32px 0;border:1px solid #e3e6ea;border-radius:8px;overflow:hidden;">
<summary style="cursor:pointer;list-style:none;padding:14px 18px;background-color:#F8FAFC;font-size:16px;font-weight:600;color:#314158;">Q4. [Possession Verification] How do I select between SMS and Email channels?</summary>
<div style="padding:12px 18px;"><div style="font-size:15px;font-weight:400;color:#314158;line-height:1.7;">A. Supported channels (SMS only / Email only / Both) are registered on the platform for each game, and you specify one of <code>channel</code> as <code>sms</code> / <code>email</code> when calling the API. <br>If you request an unregistered channel, it will return <code>70115</code> (unsupported channel), so please consult with your business manager in advance regarding the channels to use. <br>SMS has limited supported countries, and unsupported countries will return <code>70107</code>, but Email has no country restrictions. <br>For Email, the sending language is specified by <code>locale</code> (<code>ko</code> / <code>en</code>, defaults to <code>en</code> if not specified), and for SMS, it is automatically determined by the country code of the number.</div></div>
</details>
<details open style="margin:0 0 32px 0;border:1px solid #e3e6ea;border-radius:8px;overflow:hidden;">
<summary style="cursor:pointer;list-style:none;padding:14px 18px;background-color:#F8FAFC;font-size:16px;font-weight:600;color:#314158;">Q5. [Possession Verification] What happens if I enter the verification code incorrectly multiple times?</summary>
<div style="padding:12px 18px;"><div style="font-size:15px;font-weight:400;color:#314158;line-height:1.7;">A. If the verification code is entered incorrectly 5 times for the same game and target, the verification code for that target will be completely invalidated (applies to both SMS and Email).<br>Requests for verification after invalidation will return <code>70104</code>(no verification code), so please guide the user to resend the verification code.<br>The failure count is reset if the verification succeeds once or if the verification code is resent.</div></div>
</details>
<details open style="margin:0 0 32px 0;border:1px solid #e3e6ea;border-radius:8px;overflow:hidden;">
<summary style="cursor:pointer;list-style:none;padding:14px 18px;background-color:#F8FAFC;font-size:16px;font-weight:600;color:#314158;">Q6. [Ownership Verification] How should I inform users about API errors?</summary>
<div style="padding:12px 18px;"><div style="font-size:15px;font-weight:400;color:#314158;line-height:1.7;">A. Please check and handle API errors on the server side, and avoid exposing response codes directly to the frontend.<br>If you must provide an error code to the user, please display it in the <strong>S + code</strong> format so that it is not recognized as a phone number. (e.g., <code>S70199</code>)
<div style="max-width:375px;margin:16px 0 4px 0;background:#FFFFFF;border:1px solid #CBD5E1;border-radius:16px;overflow:hidden;">
<div style="background:#314158;color:#FFFFFF;font-family:'Spoqa Han Sans Neo';font-size:15px;font-weight:700;line-height:22px;letter-spacing:-0.28px;text-align:center;padding:12px 16px;">Mobile phone number verification failed</div>
<div style="padding:20px;color:#314158;font-family:'Spoqa Han Sans Neo';font-size:14px;font-weight:400;line-height:22px;letter-spacing:-0.28px;text-align:center;word-break:keep-all;overflow-wrap:normal;">
The service is currently unavailable.<br>Please try again.
<p style="color:#314158;font-size:14px;line-height:22px;margin:12px 0 0 0;">If the problem persists, please contact customer support. (S70199)</p>
<div style="margin:16px auto 0 auto;width:60%;padding:8px 0;background:#6B7280;border-radius:18px;color:#FFFFFF;font-size:13px;line-height:22px;text-align:center;">Confirm</div>
</div>
</div>
</div></div>
</details>
<br><br>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#6588F9" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><rect x="3" y="5" width="18" height="14" rx="2.5"/><path d="m3.5 7.5 8.5 6 8.5-6"/></svg><span style="color:#314158;font-size:14px;">Would you like to contact us directly? <a href="mailto:stove.developers@smilegate.com" style="color:#314158;text-decoration:underline;">stove.developers@smilegate.com</a></span></div></div>