Skip to content
Stove
Last Updated

Curious about the actual application flow?

Usage Scenarios / Using CAPTCHA

CAPTCHA

Understanding


CAPTCHA is an automated-input-prevention feature for blocking abnormal access by automated bots and macros.
STOVE provides two kinds of CAPTCHA.

Category Platform CAPTCHA In-game CAPTCHA (implemented by the CP company)
Application area Platform authentication flows such as sign-up, login, and password change In-game areas such as game-server entry, item trading, and acquiring currency
Form provided Shown automatically by the platform's own judgment
(suspicious IP, 5 wrong password entries, etc.)
STOVE provides only the CAPTCHA API
(the display conditions, timing, and targets are freely designed by the CP company)
CAPTCHA type Text input / puzzle (Slide, Click, Drag&drop, Rotate) Text-input CAPTCHA
CP company's work No separate integration needed Needed (API calls + CAPTCHA UI display handling)

This guide covers in-game CAPTCHA integration
STOVE Platform CAPTCHA requires no separate integration work.

Use Cases

In-game CAPTCHA is used to block automated bot/macro accounts from abusing game systems.
The CP company can freely design whether to show it to all users or only to users who meet specific conditions.

Use scenario Display timing Description
Blocking automated bot accounts At game-server selection Used to block mass simultaneous game entry by bot accounts
On passing the CAPTCHA, you can hide it from the same user for a set period (e.g., 1 week) to reduce the burden on regular users
Detecting economy-system abuse When a specific-action threshold is exceeded Show the CAPTCHA when a specific action such as item trading or acquiring currency occurs N times within a short period
Effective at identifying and blocking macro users
Verification just before sensitive actions Just before payment or important transactions Show it just before high-value transactions, account transfers, or suspected auto-trading to prevent fraudulent use

How It Works

Both the CAPTCHA resource-request and verification APIs must be called from the game server.
It's a structure where the game server directly controls the display conditions in the middle.

Component Role
Game client Shows the CAPTCHA resource (image) on screen and passes the user's entered CAPTCHA value to the game server
Game server Determines the CAPTCHA display conditions and calls the CAPTCHA resource-request and verification APIs on the STOVE Captcha server (an API Access Token is needed for API calls)
STOVE Captcha server Issues the CAPTCHA image and answer key, and verifies the user input passed by the game server

Integration Guide


Integration Preparation

Before CAPTCHA integration, preparation is needed in three main areas.

  • Authentication: issue an authentication token (API Access Token) for calling the STOVE CAPTCHA API
  • Display-policy design: predefine the display period, CAPTCHA level, display frequency, and display conditions (see Designing the CAPTCHA Display Policy below)
  • Operations-tool setup: set up your own ops tool so you can change policy values dynamically without a client patch (recommended)

Designing the CAPTCHA Display Policy

To flexibly adjust CAPTCHA strength during operation, we recommend a structure where you manage the items below in your own ops tool and the game server references them when calling.

Setting item Description Example
Display period The operation period during which the CAPTCHA is shown Always-on / a specific date-time range
CAPTCHA level The CAPTCHA difficulty. Passed via the captcha_level parameter (higher numbers mean higher difficulty) Static image (PNG) types 1–7 / animated image (GIF) types 101–107
Display frequency How many times it's shown to the same user within the period Every access / once within the period / once a day within the period
Display condition The condition that triggers the CAPTCHA On server selection / hidden for 1 week after passing / when trades exceed N times

Blindly raising the difficulty increases the burden on regular users
If you only raise the CAPTCHA level when an attack pattern is observed, the pass rate for legitimate users may drop.
Operate it together with policies such as threshold-based display and hiding for 1 week after passing.

Processing Flow

Applying CAPTCHA is a two-step flow: resource request → verification.
It's a structure where the game server judges the display conditions in the middle and calls the two APIs on the STOVE Captcha server in order.


The game server performs the following 3 steps in sequence.

  1. Judge the display condition — inspect the ops-tool policy (display period, level, frequency, condition) and the pass history (last pass time, etc.) to decide whether to show the CAPTCHA.
  2. Request the CAPTCHA resource — if display is needed, get a Captcha Key and CAPTCHA image from the STOVE Captcha server and deliver them to the client.
  3. Verify the CAPTCHA — pass the user's input to the STOVE Captcha server to check validity, and branch into proceeding with the original action, retrying, or blocking based on the result.

Development


1. Prerequisites

These are the preconditions that must be in place before writing CAPTCHA integration code. The CAPTCHA feature operates as a Server-to-Server API called only from the game server, with no SDK dependency.

Item Details Notes
Issue an API Access Token The authentication token for calling the CAPTCHA API. Must be issued separately per environment (Live, Sandbox) Request from the publishing technical contact
Confirm the caller All CAPTCHA APIs are called only from the game server (Server-to-Server).
Direct calls from the client leak the API Access Token and allow bypassing CAPTCHA verification, so they are prohibited
Abuse prevention
Define the Caller-ID The header identifying the API caller (game server). Define it as EXT-SERVER-{game name} in advance with the publishing technical contact Required
Define the Caller-Detail (user identifier) The header identifying the user on the API caller's side.
Choose from UUID, CUID, or character ID per your policy and pass it as a header when calling the CAPTCHA API
Recommended
Extract the client IP Used to identify blocked IPs and apply per-IP rate limits.
The game server must extract the correct client IP and pass it when calling the CAPTCHA API
Required
Display-policy logic Manage the display period, level, frequency, and condition in your own ops tool; the game server references the policy at call time to judge CAPTCHA display and pass history Recommended
Own ops tool Set up the ops tool so you can change the CAPTCHA level, frequency, and condition dynamically without a client patch Recommended

2. Game Server Implementation


Deciding whether to show the CAPTCHA

Inspect the ops-tool policy (display period, level, frequency, condition) and pass history to decide whether to show the CAPTCHA. STOVE does not manage the display history, so store and manage it yourself on the game server.

  • Reference the ops-tool policy values to decide whether to show the CAPTCHA
  • Store each user's last CAPTCHA-pass time and apply the display-frequency policy
  • For threshold-based display, implement action-count accumulation and reset-on-pass logic
  • Display-history management is the game server's responsibility (not managed by STOVE)



Integrating the CAPTCHA resource-request and verification APIs

The CAPTCHA API consists of two parts: resource request and verification. Both are called only from the game server.

API Method Path Request parameters Key response fields
CAPTCHA resource request GET /blockchecker/v1.0/server/captcha Query: captcha_level, client_ip code, value.captcha_key, value.resource.image_url
CAPTCHA verification POST /blockchecker/v1.0/server/verify Body: captcha_level, captcha_key, captcha_value, client_ip code

The common headers are as follows.

  • Authorization: Bearer <API Access Token>

  • Caller-ID: EXT-SERVER-{game name}

  • Caller-Detail: {user identifier}

  • The verification API additionally needs the Content-Type: application/json header

⚠️ Caution: captcha_key usage rules
The captcha_key received from the CAPTCHA resource-request API is valid only when all of the following conditions are met.

  • 2-minute (120s) validity — it auto-expires 120 seconds after issuance.
  • One successful verification only — on successful verification, it's immediately invalidated and cannot be reused.
  • Up to 3 cumulative failures — a value mismatch (49702) can be re-entered with the same key, but on the 4th failure the key is invalidated.

If any of the above conditions is exceeded, 49701 is returned, and you must call the resource-request API again.



Branching by response code

Responses come back as HTTP 200 or 401; for both APIs, code 0 is normal, and even on a 200 response, code != 0 must branch as a business error. HTTP 4xx/5xx or network exceptions outside the spec may indicate a temporary STOVE-side failure, so design retry, logging, and alarm policies together. Don't expose the response code value directly to users; branch with a context-appropriate guidance UI.

Follow-up handling by key code:

  • Verification success (0) — close the CAPTCHA layer and proceed with the original action (game entry, trade, sensitive action, etc.). Store the pass time on the game server to use for the next display-frequency/threshold judgment
  • Value mismatch (49702) — show a re-entry UI with the same captcha_key. Note that up to 3 attempts are allowed and the 4th failure switches to 49701, so handle it together with a new-resource-issuance branch
  • Key expired / cumulative failures exceeded / already-used key (49701) — get a new resource from the CAPTCHA resource-request API and show it again. Retrying with the same key is meaningless
  • Blocked IP (49500, can occur in both resource request and verification) — show an access-blocked notice without retry
  • Token error (40101–40104, 49318) — an API Access Token setup problem. Show a general notice to users and alert the operations team
  • Parameter error (49200, 49240, 49241, 49700, 49713) — missing or malformed game-server call parameters. Show a general notice to users and alert the operations team
  • Server internal error (49106) — a temporary STOVE-server-side error. If it persists after retry, alert the operations team



Sequence diagram

Normal flow (Happy Path)


Error-handling flow (Error Handling)


For the detailed meaning of each response code and recommended handling, refer to the "Branching by response code" item above.



Sample code

This example is a reference implementation of the area the game server operates directly. Supplement dependencies, exception handling, and logging to fit your actual environment. Applying the same policy as the code classification in §"Branching by response code", we recommend a structure that throws token errors as TokenAuthException and blocked IPs as BlockedIpException and handles them collectively at the controller layer. The sample code is based on using RestClient in a Spring Boot 3.2+ (Spring Framework 6.1+) environment. Retry logic for temporary failures (49106, HTTP 5xx, network exceptions, etc.) is not included in this sample. We recommend implementing it in a separate layer with exponential backoff using libraries such as Spring Retry or Resilience4j.


1) CAPTCHA resource request
java
@Component
public class StoveCaptchaClient {

    private static final String HOST_LIVE    = "https://api.onstove.com";
    private static final String HOST_SANDBOX = "https://api.gate8.com";

    private final RestClient restClient;

    @Value("${stove.api.access-token}") private String apiAccessToken;
    @Value("${stove.caller-id}")        private String callerId;
    @Value("${stove.profile:live}")     private String profile;

    public StoveCaptchaClient(RestClient.Builder builder) {
        this.restClient = builder.build();
    }

    /** CAPTCHA resource request. Throws BlockedIpException for blocked IPs and TokenAuthException for token errors. */
    public CaptchaResource requestCaptcha(int level, String clientIp, String callerDetail) {
        String host = "sandbox".equalsIgnoreCase(profile) ? HOST_SANDBOX : HOST_LIVE;

        CaptchaResponse res = restClient.get()
                .uri(host + "/blockchecker/v1.0/server/captcha?captcha_level={lvl}&client_ip={ip}",
                     level, clientIp)
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiAccessToken)
                .header("Caller-ID", callerId)
                .header("Caller-Detail", callerDetail)
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, (req, r) -> {
                    // 40101~40104 → HTTP 401 token error
                    throw new TokenAuthException("HTTP " + r.getStatusCode());
                })
                .body(CaptchaResponse.class);

        if (res == null) throw new IllegalStateException("empty captcha response");
        switch (res.code) {
            case 0:     return res.value;
            case 49500: throw new BlockedIpException("49500");                                         // Blocked IP
            case 49318: throw new TokenAuthException("code=" + res.code + ", message=" + res.message); // Token error (in response body)
            case 49106:                                                                                 // Server internal error
                // log.error("STOVE captcha server error: code={}, message={}", res.code, res.message);
                throw new IllegalStateException("server internal: code=" + res.code + ", message=" + res.message);
            case 49240: case 49241: case 49713:                                                         // Parameter error
                // log.error("STOVE captcha param error: code={}, message={}", res.code, res.message);
                throw new IllegalStateException("param error: code=" + res.code + ", message=" + res.message);
            default:
                // log.error("STOVE captcha unknown error: code={}, message={}", res.code, res.message);
                throw new IllegalStateException("captcha request failed: code=" + res.code + ", message=" + res.message);
        }
    }

    public static class BlockedIpException extends RuntimeException {
        public BlockedIpException(String code) { super("blocked IP, code=" + code); }
    }

    public static class TokenAuthException extends RuntimeException {
        public TokenAuthException(String detail) { super("captcha token error: " + detail); }
    }

    @Getter @Setter
    public static class CaptchaResponse {
        public int code;
        public String message;
        public CaptchaResource value;
    }

    @Getter @Setter
    public static class CaptchaResource {
        @JsonProperty("captcha_key")  public String captchaKey;
        @JsonProperty("captcha_type") public String captchaType;
        public Resource resource;
    }

    @Getter @Setter
    public static class Resource {
        @JsonProperty("image_url") public String imageUrl;
    }
}



2) CAPTCHA verification
java
public VerifyResult verifyCaptcha(int level, String key, String value, String clientIp, String callerDetail) {
    String host = "sandbox".equalsIgnoreCase(profile) ? HOST_SANDBOX : HOST_LIVE;

    Map<String, Object> body = Map.of(
        "captcha_level", level,
        "captcha_key",   key,
        "captcha_value", value,
        "client_ip",     clientIp
    );

    VerifyResponse res = restClient.post()
            .uri(host + "/blockchecker/v1.0/server/verify")
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiAccessToken)
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .header("Caller-ID", callerId)
            .header("Caller-Detail", callerDetail)
            .body(body)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, (req, r) -> {
                // 40101~40104 → HTTP 401 token error
                throw new TokenAuthException("HTTP " + r.getStatusCode());
            })
            .body(VerifyResponse.class);

    if (res == null) throw new IllegalStateException("empty verify response");
    switch (res.code) {
        case 0:     return VerifyResult.PASS;          // Verification success → proceed with the original action
        case 49702: return VerifyResult.RETRY;         // Value mismatch → re-enter with the same key
        case 49701: return VerifyResult.NEW_RESOURCE;  // Key invalid/expired → request a new resource
        case 49500: throw new BlockedIpException("49500");                                         // Blocked IP
        case 49318: throw new TokenAuthException("code=" + res.code + ", message=" + res.message); // Token error (in response body)
        case 49200: case 49240: case 49241: case 49700: case 49713:                                 // Parameter error
            // log.error("STOVE captcha verify param error: code={}, message={}", res.code, res.message);
            throw new IllegalStateException("param error: code=" + res.code + ", message=" + res.message);
        default:
            // log.error("STOVE captcha verify unknown error: code={}, message={}", res.code, res.message);
            throw new IllegalStateException("verify failed: code=" + res.code + ", message=" + res.message);
    }
}

public enum VerifyResult { PASS, RETRY, NEW_RESOURCE }

@Getter @Setter
public static class VerifyResponse {
    public int code;
    public String message;
}



3) Request → verification full flow
java
@Service
public class GameServerCaptchaFlow {
    private final StoveCaptchaClient captchaClient;
    private final CaptchaSessionStore sessionStore;   // The game studio's own ops tool / session management

    /** When the display condition is met, issue the CAPTCHA and deliver it to the client */
    public CaptchaIssued issueCaptcha(String userId, String clientIp) {
        int level = sessionStore.resolveLevel(userId);
        CaptchaResource res = captchaClient.requestCaptcha(level, clientIp, userId);
        sessionStore.bindKey(userId, res.captchaKey, level);          // Store the user-key mapping
        return new CaptchaIssued(res.captchaKey, res.captchaType, res.resource.imageUrl);
        // Blocked IPs are thrown as BlockedIpException, so the controller handles the block-UI response
    }

    /** When the client passes the input, verify and then handle the follow-up */
    public VerifyResult verify(String userId, String userInput, String clientIp) {
        CaptchaSessionStore.Binding binding = sessionStore.lookup(userId)
            .orElseThrow(() -> new IllegalStateException("no captcha issued"));

        VerifyResult result = captchaClient.verifyCaptcha(
            binding.level, binding.captchaKey, userInput, clientIp, userId
        );

        if (result == VerifyResult.PASS) {
            sessionStore.markPassed(userId);   // Update the pass time
            sessionStore.clear(userId);
        } else if (result == VerifyResult.NEW_RESOURCE) {
            sessionStore.clear(userId);        // Key expired → prompt a new resource request
        }
        return result;
        // Recommended: handle blocked IP / token errors collectively with try-catch in the controller
    }

    public record CaptchaIssued(String captchaKey, String captchaType, String imageUrl) {}
}

3. Game Client Implementation


CAPTCHA UI

Show the CAPTCHA image issued by the game server on screen and pass the user's entered value back to the game server. The game implements the CAPTCHA UI design itself.

  • Show the CAPTCHA image (image_url) and Captcha Key (captcha_key) received from the game server on screen
  • Implement the flow that passes the user's input and the Captcha Key back to the game server
  • The game designs and implements the CAPTCHA display UI itself

Resource spec and type branching

  • The CAPTCHA image provided via image_url is 240 × 80 (PNG or GIF). Use this size as the basis for the input area and margins when designing the UI layout and scaling.
  • The game server also passes the captcha_type received in the CAPTCHA resource response to the client, so the client can branch its rendering by type.
    • image — a static PNG image
    • animated_image — an animated GIF image

▲ Example of a CAPTCHA UI implemented by the CP company (LORDNINE)



Client branching by verification result

Branch the client UI based on the verification result the game server returns. Don't expose the response code value directly to users; branch with a context-appropriate guidance UI.


Input preprocessing (recommended)

  • Trim leading/trailing whitespace from the user input before passing it.
  • CAPTCHA verification is case-insensitive. For user convenience, the client can standardize display/input to uppercase (or lowercase) with no effect on the result.

UI branching by verification result

The client does not receive the STOVE Captcha server's response code directly. It assumes the game server converts the STOVE response into its own status/code and passes it to the client. The branches below are based on meaning (status), not the response code, and the CP company can define and map the statuses with its own interface convention.

  • Verification success — close the CAPTCHA layer and proceed with the original action
  • Value mismatch — the default behavior is to keep the same CAPTCHA image and show a re-entry UI. Up to 3 retries with the same CAPTCHA are allowed, and per the CP company's own policy, refreshing on each failure (requesting a new resource every time) is also possible.
  • Captcha Key expired/invalid — get a new resource, refresh the CAPTCHA image, and show it again
  • Blocked IP — show a block-notice alert without retry
  • Other errors — a general error notice. Design the retry flow to fit your operations policy

Summary of UI branching by situation

Verification-result status UI behavior New resource request Retry
Verification success Close the CAPTCHA layer and proceed with the original action Not needed
Value mismatch Keep the same image + prompt re-entry
(can refresh every time per your own policy)
Optional (per policy) Possible (up to 3 times)
Key expired/invalid Refresh with a new image and show again Required Restart with a new key
Blocked IP Block-notice alert Prohibited None
Other errors General error notice Per policy Per policy

The statuses above map to STOVE response codes as follows (by game-server conversion): verification success ← 0, value mismatch ← 49702, key expired/invalid ← 49701, blocked IP ← 49500. For details on code handling in the game server, see §3 "Branching by response code".



Localization and accessibility handling

For global games, localize the CAPTCHA UI guidance text into the game's supported languages. Also provide auxiliary-action buttons so users can correct mistakes while entering the CAPTCHA.

  • Localize the CAPTCHA UI guidance text into the game's supported languages
  • Provide auxiliary-action buttons such as refresh, re-enter, and cancel
  • A required review item for global games

4. Operations Guide

To respond to CAPTCHA failure situations during operation, design a retry strategy and a failure-time fallback policy in advance.


Recommended retry strategy
  • Retry targets: temporary HTTP 5xx, network exceptions, code 49106 (server internal error)
  • Retry policy: exponential backoff (e.g., 200ms → 500ms) recommended

Fallback policy for temporary STOVE Captcha server failures

When the STOVE Captcha server is temporarily down or response delays persist, we recommend a policy to pass CAPTCHA verification (bypass).

  • Automatically switch to fallback mode when HTTP 5xx / timeouts exceed a threshold within a set time
  • During fallback, generate logs/alarms so the operations team can be aware immediately
  • After the STOVE Captcha server recovers, either auto-release fallback mode or release it manually at operational discretion

Operational alarms

Design the following events to immediately raise alarms to the operations team.

  • Token error (40101–40104, 49318) — a transient occurrence just before reissuance after token expiry can appear even in a normal operating environment, but if it persists, check the API Access Token setup
  • Parameter error (49200, 49240, 49241, 49700, 49713) — missing or malformed parameters on the game-server call side. Code review needed

Frequently Asked Questions



Q. Do I also need to integrate STOVE Platform CAPTCHA separately?
A. No, no separate integration is needed. STOVE automatically shows the CAPTCHA when it detects risks such as a suspicious IP or 5 wrong password entries in platform authentication flows like sign-up, login, and account recovery.
For the in-game CAPTCHA covered in this guide, STOVE provides only the API, so the CP company must integrate it directly.
Q. Should I show the CAPTCHA to all users?
A. No, the CP company designs this freely. You can operate it for all users, or show it only to users who meet specific conditions (e.g., N actions within a short period).
To reduce the burden on regular users, operate threshold-based display together with a policy of hiding the CAPTCHA for a set period after it's passed.
Q. Where is the 'hide for a set period after passing' handling managed?
A. It must be managed in the CP company's own ops tool or game server. The STOVE Captcha server simply handles resource issuance and verification.
We recommend a structure where you store each user's last pass time and the game server judges whether to hide it just before the API call.
Q. Can I call the CAPTCHA API directly from the game client?
A. No. Both the CAPTCHA resource-request API and the verification API must be called only from the game server.
Calling directly from the client can expose the API Access Token and let external attackers bypass or manipulate the CAPTCHA.
Q. What values should I enter for Caller-ID and Caller-Detail?
A. Caller-ID is caller (game server) identification info, entered in the format EXT-SERVER-{game name}. (e.g., EXT-SERVER-LORDNINE)
Caller-Detail is client (user) identification info, entered as a UUID or user ID (CUID or in-game character ID).
Q. How should I decide the captcha_level value?
A. Choose from the range per CAPTCHA type. Static images (PNG) are 1 ~ 7 and animated images (GIF) are 101 ~ 107, with higher numbers meaning higher difficulty.
Use a low level for normal flows and apply a high level to users with accumulated suspicious activity, operating it in stages.
Blindly raising the difficulty also lowers legitimate users' pass rate and can lead to churn, so design it together with threshold-based display conditions.
Q. Is user input case-sensitive during CAPTCHA verification?
A. No, it's not case-sensitive. For example, for a CAPTCHA whose answer is 6a537Y, entering 6A537y still passes.
So even if the client force-converts/displays input as uppercase (or lowercase) for convenience, it doesn't affect the verification result. You can use this to reduce inquiries caused by Caps Lock state or IME auto-conversion.
However, we recommend trimming leading/trailing whitespace and line breaks before passing the input.
Q. How should I handle 49702 (CAPTCHA value mismatch)?
A. Show a CAPTCHA re-entry UI. You can retry using the same Captcha Key.
However, if it fails more than a set number of times, you need follow-up policies such as requesting a new CAPTCHA resource or blocking access.
Q. If I get a 49500 (blocked Client IP) response, should I let the user solve the CAPTCHA again?
A. No, show an access-blocked notice alert without retry.
STOVE has judged it a clearly aggressive IP and blocked it, so re-requesting the CAPTCHA is meaningless and could even increase attack traffic.



Need to contact us directly? stove.developers@smilegate.com