K-OTP

언어별 서버 SDK

C#, Java, Kotlin/JVM, PHP, Go, Python, Ruby, Dart용 프리뷰 서버 클라이언트와 TypeScript용 @k-otp/sdk의 Kiota 클라이언트. 설치, 발급과 검증, 멱등성, 오류 처리.

JavaScript/TypeScript SDK 외에 다른 언어용 서버 클라이언트도 있습니다. 공개 OpenAPI 스펙에서 Kiota로 생성한 코드를 언어별로 작은 클라이언트가 감싸는 구조입니다. github.com/k-otp/sdk의 sdks/ 디렉터리에 오픈소스로 공개되어 있습니다.

언어별 SDK는 버전 0.1.0의 프리뷰이며, npm 패키지와 별도로 버전을 매깁니다. 아직 PyPI, NuGet, Maven Central, Packagist, RubyGems, pub.dev에는 배포되지 않았습니다. server-sdk-v0.1.0 GitHub Release에서 설치하고, Go는 모듈 경로로 설치하세요. 정확한 버전을 고정해 쓰세요. 1.0 전까지 메서드 이름과 옵션이 바뀔 수 있습니다.

이 클라이언트는 sk_ 비밀 키를 쓰므로 서버에서만 사용하세요. 브라우저와 pk_ 키에는 @k-otp/sdk와 그 프레임워크·UI 임포트를 쓰세요.

패키지

언어패키지버전설치 경로검증 환경
C#KOtp.Sdk0.1.0GitHub Release: KOtp.Sdk.0.1.0.nupkg.NET 8
Javadev.kotp:kotp-sdk0.1.0GitHub Release: JAR + POM (Java 17 바이트코드)JDK 21
Kotlin/JVMdev.kotp:kotp-sdk (Java JAR)0.1.0Java와 같은 JARKotlin 2.1.20, JDK 21
PHPk-otp/sdk0.1.0GitHub Release: kotp-sdk-php-0.1.0.zipPHP 8.4 (8.2 이상 필요)
Gogithub.com/k-otp/sdk/sdks/gov0.1.0Go 모듈 프록시Go 1.26
Pythonkotp-sdk (임포트는 kotp_sdk)0.1.0GitHub Release: wheel 또는 sdistPython 3.13
Rubykotp_sdk0.1.0GitHub Release: kotp_sdk-0.1.0.gemRuby 3.3
Dartkotp_sdk0.1.0GitHub Release: kotp-sdk-dart-0.1.0.tar.gzDart 3.9, 서버(dart:io)
TypeScript@k-otp/sdk/kiotanpm 1.3.0+npm, @k-otp/sdk에 포함Node.js 20.19, 22, 24

릴리스 파일은 https://github.com/k-otp/sdk/releases/download/server-sdk-v0.1.0/<파일명>에서 받을 수 있습니다. 받은 파일은 릴리스의 SHA256SUMS로 확인하세요.

shasum -a 256 -c SHA256SUMS --ignore-missing

서버의 TypeScript에는 타입이 있는 @k-otp/sdk/server가 계속 기본 클라이언트입니다. @k-otp/sdk/kiota는 다른 언어와 같은 JSON 입력·JSON 출력 클라이언트로, 여러 서비스에서 같은 API 형태를 쓰고 싶을 때 사용합니다.

모든 클라이언트의 공통 동작

  • 9개 작업. 발급, 검증, 상태 조회, 발급 목록, 발급 상세, 크레딧 원장, 잔액, 템플릿 목록, 템플릿 상세로, API 레퍼런스의 작업과 같습니다. 메서드 이름은 언어 관례를 따릅니다(issue, IssueAsync, issue_detail 등).
  • JSON 입력, JSON 출력. 입력에는 API의 JSON 필드 이름(phoneNumber, purpose, idempotencyKey, issueId, code)을 쓰고, 응답은 언어 기본 JSON 값으로 돌아옵니다. 앞자리 0이 사라지지 않도록 휴대폰 번호와 인증번호는 문자열로 다루세요.
  • API 1.8.0. 0.1.0 클라이언트는 OpenAPI 스펙 1.8.0에서 생성되어, 발급에 smsFallback을 포함한 현재의 모든 필드를 보낼 수 있습니다(채널과 SMS 대체 발송).
  • 비밀 키 전용. 생성자는 sk_로 시작하지 않는 키를 거부합니다.
  • API origin 고정. 기본 URL은 https://api.k-otp.dev/v1이며 HTTPS여야 합니다(로컬 테스트용 루프백 HTTP는 허용). 리디렉션과 다른 origin으로의 요청은 거부되므로 키는 설정한 API로만 전송됩니다. 기본 타임아웃은 10초입니다.

설정 오류(키 누락 또는 sk_가 아닌 키, 잘못된 기본 URL, 없거나 잘못된 멱등 키)는 요청을 보내기 전에 API 오류가 아닌 설정 오류로 실패합니다. 예: C#의 ArgumentException, Java의 IllegalArgumentException, TypeScript와 Python의 TypeError, Go의 kotp.ConfigurationError.

멱등성과 재시도

issue의 입력에는 idempotencyKey가 반드시 있어야 합니다. 공백 없는 출력 가능한 ASCII 1~128자(앞뒤 공백은 제거)이며, 예를 들어 UUID를 쓰면 됩니다. 클라이언트는 보내기 전에 키를 검사하고, 같은 키를 Idempotency-Key 헤더와 본문에 함께 보냅니다. 발송 시도마다 키를 하나 만들어 저장하고, 그 시도를 재시도할 때 같은 키를 다시 쓰세요(멱등성).

클라이언트는 자동으로 재시도하지 않습니다. 두 경우를 주의하세요.

  • 타임아웃이나 네트워크 실패는 outcome이 "unknown"인 전송 오류가 됩니다. 인증번호가 이미 발송되었을 수 있습니다. 같은 키로 issue를 재시도하고, 새 키로 다시 시작하지 마세요(결과가 불명확할 때).
  • HTTP 503은 API 오류가 됩니다. 옵션을 켜면 503 뒤에 같은 본문과 키로 issue를 정확히 한 번 자동 재시도합니다. verify는 재시도하지 않습니다.
언어503 뒤 issue 1회 재시도 허용
C#new RequestOptions(Retry503: true)
Java/Kotlinnew KotpClient.RequestOptions(Map.of(), null, true)
PHP['retry503' => true]
Gokotp.RequestOptions{Retry503: true}
Pythonretry503=True
Rubyretry503: true
Dartoptions: RequestOptions(retry503: true)
TypeScript{ retry503: true }

오류

2xx가 아닌 응답은 오류 envelope 전체를 담은 API 오류가 됩니다. HTTP 상태, envelope(defined, code, status, message, data), X-Request-Id 값(지원팀에 문의할 때 함께 알려 주세요), 재시도 전 대기 시간(밀리초, data.retryAfterMs 또는 없으면 Retry-After. 429와 대기 시간을 보내는 503에 설정), 소문자 이름의 응답 헤더를 제공합니다. 분기는 envelope의 code로 하세요.

틀리거나 만료된 인증번호는 오류가 아닙니다. verify가 verified: false와 reasonCode를 돌려줍니다(검증 결과).

언어API 오류전송 오류(outcome "unknown")
C#KotpApiException: Status, Envelope, RequestId, RetryAfterMsKotpTransportException
Java/KotlinKotpClient.KotpApiException: status, envelope, requestId, retryAfterMsKotpClient.KotpTransportException
PHPKOtp\KotpApiError: status, envelope, requestId, retryAfterMsKOtp\KotpTransportError
Go*kotp.APIError: Status, Envelope, RequestID, RetryAfterMs*kotp.TransportError
PythonKotpApiError: status, envelope, request_id, retry_after_msKotpTransportError
RubyKOtp::ApiError: status, envelope, request_id, retry_after_msKOtp::TransportError
DartKotpApiError: status, envelope, requestId, retryAfterMsKotpTransportError
TypeScriptKotpApiError: status, envelope, requestId, retryAfterMsKotpTransportError

C# (.NET)

KOtp.Sdk.0.1.0.nupkg를 로컬 ./packages 디렉터리에 받아 NuGet 소스로 추가하고 패키지를 참조하세요. 패키지의 런타임 의존성은 nuget.org에서 받으므로 nuget.org는 켜 두세요.

dotnet nuget add source ./packages --name kotp-local
dotnet add package KOtp.Sdk --version 0.1.0
using System.Text.Json.Nodes;
using KOtp;

using var client = new KotpClient(Environment.GetEnvironmentVariable("KOTP_SECRET_KEY")!);

try {
    var issued = await client.IssueAsync(new JsonObject {
        ["phoneNumber"] = "01012345678",
        ["purpose"] = "login",
        ["idempotencyKey"] = idempotencyKey, // 예: Guid.NewGuid().ToString(). 재시도용으로 저장
    });
    var issueId = issued!["issueId"]!.GetValue<string>();

    // 나중에 사용자가 입력한 인증번호로:
    var result = await client.VerifyAsync(new JsonObject { ["issueId"] = issueId, ["code"] = "123456" });
    var verified = result!["verified"]!.GetValue<bool>();
} catch (KotpApiException e) {
    var code = e.Envelope?["code"]?.GetValue<string>();
    Console.Error.WriteLine($"{e.Status} {code} {e.RequestId}");
} catch (KotpTransportException) {
    // 결과 불명확: 같은 idempotencyKey로 IssueAsync 재시도
}

모든 메서드는 CancellationToken도 받습니다. 생성자에서 baseUrl과 timeoutMs를 바꿀 수 있습니다. 다 쓴 클라이언트는 Dispose하세요.

Java

kotp-sdk-0.1.0.jar, kotp-sdk-0.1.0.pom, kotp-sdk-0.1.0-sources.jar, kotp-sdk-0.1.0-javadoc.jar를 받아 로컬 Maven 저장소에 설치하고 의존성을 추가하세요.

mvn install:install-file -Dfile=kotp-sdk-0.1.0.jar -DpomFile=kotp-sdk-0.1.0.pom \
  -Dsources=kotp-sdk-0.1.0-sources.jar -Djavadoc=kotp-sdk-0.1.0-javadoc.jar
<dependency>
  <groupId>dev.kotp</groupId>
  <artifactId>kotp-sdk</artifactId>
  <version>0.1.0</version>
</dependency>
import com.google.gson.JsonObject;
import dev.kotp.sdk.KotpClient;

try (KotpClient client = new KotpClient(System.getenv("KOTP_SECRET_KEY"))) {
    JsonObject issue = new JsonObject();
    issue.addProperty("phoneNumber", "01012345678");
    issue.addProperty("purpose", "login");
    issue.addProperty("idempotencyKey", idempotencyKey); // 예: UUID.randomUUID().toString()
    String issueId = client.issue(issue).getAsJsonObject().get("issueId").getAsString();

    // 나중에 사용자가 입력한 인증번호로:
    JsonObject verify = new JsonObject();
    verify.addProperty("issueId", issueId);
    verify.addProperty("code", "123456");
    boolean verified = client.verify(verify).getAsJsonObject().get("verified").getAsBoolean();
} catch (KotpClient.KotpApiException e) {
    System.err.println(e.status + " " + e.envelope.get("code") + " " + e.requestId);
} catch (KotpClient.KotpTransportException e) {
    // 결과 불명확: 같은 idempotencyKey로 issue 재시도
}

호출은 동기 방식이며 Gson 값을 돌려줍니다. new KotpClient(apiKey, baseUrl, timeoutMs) 생성자로 기본값을 바꿀 수 있습니다. 연결 풀을 해제하려면 클라이언트를 close하세요.

Kotlin/JVM

Kotlin/JVM은 Java JAR와 API를 그대로 씁니다. Java처럼 설치하세요. Android와 Kotlin Multiplatform은 이번 릴리스 범위가 아닙니다.

import com.google.gson.JsonObject
import dev.kotp.sdk.KotpClient

KotpClient(System.getenv("KOTP_SECRET_KEY")).use { client ->
    try {
        val issued = client.issue(JsonObject().apply {
            addProperty("phoneNumber", "01012345678")
            addProperty("purpose", "login")
            addProperty("idempotencyKey", idempotencyKey)
        })
        val issueId = issued.asJsonObject.get("issueId").asString

        val result = client.verify(JsonObject().apply {
            addProperty("issueId", issueId)
            addProperty("code", "123456")
        })
        val verified = result.asJsonObject.get("verified").asBoolean
    } catch (e: KotpClient.KotpApiException) {
        println("${e.status} ${e.envelope.get("code")} ${e.requestId}")
    } catch (e: KotpClient.KotpTransportException) {
        // 결과 불명확: 같은 idempotencyKey로 issue 재시도
    }
}

호출이 블로킹되므로 코루틴에서는 적절한 디스패처(예: Dispatchers.IO)에서 실행하세요.

PHP

kotp-sdk-php-0.1.0.zip을 로컬 ./packages 디렉터리에 받아 Composer artifact 저장소로 등록하고 패키지를 require하세요.

composer config repositories.kotp artifact ./packages
composer require k-otp/sdk:0.1.0
use KOtp\KotpApiError;
use KOtp\KotpClient;
use KOtp\KotpTransportError;

$client = new KotpClient(getenv('KOTP_SECRET_KEY'));

try {
    $issued = $client->issue([
        'phoneNumber' => '01012345678',
        'purpose' => 'login',
        'idempotencyKey' => $idempotencyKey, // 예: bin2hex(random_bytes(16)). 재시도용으로 저장
    ]);

    // 나중에 사용자가 입력한 인증번호로:
    $result = $client->verify(['issueId' => $issued->issueId, 'code' => '123456']);
    $verified = $result->verified;
} catch (KotpApiError $e) {
    error_log("{$e->status} {$e->envelope->code} {$e->requestId}");
} catch (KotpTransportError $e) {
    // 결과 불명확: 같은 idempotencyKey로 issue 재시도
}

호출은 동기 방식입니다. JSON 객체는 stdClass, 목록은 배열로 돌아옵니다. 생성자에서 $baseUrl과 $timeoutMs를 바꿀 수 있습니다.

Go

go get github.com/k-otp/sdk/sdks/go@v0.1.0
import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"

	kotp "github.com/k-otp/sdk/sdks/go"
)

func sendAndVerify(ctx context.Context, idempotencyKey string) (bool, error) {
	client, err := kotp.New(kotp.Options{APIKey: os.Getenv("KOTP_SECRET_KEY")})
	if err != nil {
		return false, err
	}

	input, _ := json.Marshal(map[string]any{
		"phoneNumber":    "01012345678",
		"purpose":        "login",
		"idempotencyKey": idempotencyKey,
	})
	issued, err := client.Issue(ctx, input, kotp.RequestOptions{})
	var apiErr *kotp.APIError
	var transportErr *kotp.TransportError
	switch {
	case errors.As(err, &apiErr):
		return false, fmt.Errorf("%d %v %s", apiErr.Status, apiErr.Envelope["code"], apiErr.RequestID)
	case errors.As(err, &transportErr):
		return false, err // 결과 불명확: 같은 idempotencyKey로 Issue 재시도
	case err != nil:
		return false, err
	}
	issueID := issued.(map[string]any)["issueId"].(string)

	// 나중에 사용자가 입력한 인증번호로:
	verifyInput, _ := json.Marshal(map[string]any{"issueId": issueID, "code": "123456"})
	result, err := client.Verify(ctx, verifyInput, kotp.RequestOptions{})
	if err != nil {
		return false, err
	}
	return result.(map[string]any)["verified"].(bool), nil
}

Issue와 Verify는 요청 본문을 json.RawMessage로 받고, 응답은 디코딩된 JSON(객체는 map[string]any)입니다. 모든 호출은 context를 받습니다. Options에는 BaseURL과 Timeout도 있습니다.

Python

wheel을 받아 설치하세요.

python -m pip install ./kotp_sdk-0.1.0-py3-none-any.whl
import asyncio
import os

from kotp_sdk import KotpApiError, KotpClient, KotpTransportError

async def main(idempotency_key: str) -> None:
    async with KotpClient(os.environ["KOTP_SECRET_KEY"]) as client:
        try:
            issued = await client.issue({
                "phoneNumber": "01012345678",
                "purpose": "login",
                "idempotencyKey": idempotency_key,  # 예: str(uuid.uuid4()). 재시도용으로 저장
            })

            # 나중에 사용자가 입력한 인증번호로:
            result = await client.verify({"issueId": issued["issueId"], "code": "123456"})
            verified = result["verified"]
        except KotpApiError as e:
            print(e.status, e.envelope["code"], e.request_id)
        except KotpTransportError:
            pass  # 결과 불명확: 같은 idempotency_key로 issue 재시도


asyncio.run(main(idempotency_key))

비동기 클라이언트입니다. async with를 쓰거나 await client.aclose()를 호출하세요. 생성자에서 base_url과 timeout_ms를 바꿀 수 있습니다.

Ruby

gem을 받아 로컬에서 설치하세요.

gem install --local kotp_sdk-0.1.0.gem
require 'kotp_sdk'

client = KOtp::Client.new(ENV.fetch('KOTP_SECRET_KEY'))

begin
  issued = client.issue({
    'phoneNumber' => '01012345678',
    'purpose' => 'login',
    'idempotencyKey' => idempotency_key # 예: SecureRandom.uuid. 재시도용으로 저장
  })

  # 나중에 사용자가 입력한 인증번호로:
  result = client.verify({ 'issueId' => issued['issueId'], 'code' => '123456' })
  verified = result['verified']
rescue KOtp::ApiError => e
  warn "#{e.status} #{e.envelope['code']} #{e.request_id}"
rescue KOtp::TransportError
  # 결과 불명확: 같은 idempotency_key로 issue 재시도
end

응답은 문자열 키를 쓰는 Hash, Array, 스칼라 값입니다. 생성자에서 base_url:과 timeout_ms:를 바꿀 수 있습니다. gem을 불러오면 프로세스 전체에 보호된 JSON 호환 훅이 설치되므로, 고정된 런타임 의존성 버전을 그대로 유지하세요.

Dart

kotp-sdk-dart-0.1.0.tar.gz를 디렉터리에 풀고 kotp_sdk라는 이름의 path 의존성으로 추가하세요.

mkdir -p third_party/kotp_sdk
tar -xzf kotp-sdk-dart-0.1.0.tar.gz -C third_party/kotp_sdk
dependencies:
  kotp_sdk:
    path: third_party/kotp_sdk
import 'dart:io';

import 'package:kotp_sdk/kotp_sdk.dart';

final client = KotpClient(Platform.environment['KOTP_SECRET_KEY']!);
try {
  final issued = await client.issue({
    'phoneNumber': '01012345678',
    'purpose': 'login',
    'idempotencyKey': idempotencyKey, // 재시도용으로 저장
  }) as Map<String, dynamic>;

  // 나중에 사용자가 입력한 인증번호로:
  final result = await client.verify({
    'issueId': issued['issueId'],
    'code': '123456',
  }) as Map<String, dynamic>;
  final verified = result['verified'] as bool;
} on KotpApiError catch (e) {
  stderr.writeln('${e.status} ${e.envelope['code']} ${e.requestId}');
} on KotpTransportError {
  // 결과 불명확: 같은 idempotencyKey로 issue 재시도
} finally {
  client.close();
}

지원 런타임은 서버(dart:io)의 Dart 3.9입니다. sk_ 키와 마찬가지로 Flutter 앱이나 웹 앱에 넣지 마세요. 생성자에서 baseUrl:과 timeoutMs:를 바꿀 수 있습니다. 다 쓰면 close()를 호출하세요.

TypeScript (@k-otp/sdk/kiota)

npm i @k-otp/sdk
import { KotpApiError, KotpClient, KotpTransportError } from "@k-otp/sdk/kiota";

const client = new KotpClient({ apiKey: process.env.KOTP_SECRET_KEY! });

try {
	const issued = (await client.issue({
		phoneNumber: "01012345678",
		purpose: "login",
		idempotencyKey, // 예: crypto.randomUUID(). 재시도용으로 저장
	})) as { issueId: string };

	// 나중에 사용자가 입력한 인증번호로:
	const result = (await client.verify({ issueId: issued.issueId, code: "123456" })) as {
		verified: boolean;
		reasonCode?: string;
	};
} catch (error) {
	if (error instanceof KotpApiError) {
		console.error(error.status, error.envelope.code, error.requestId);
	} else if (error instanceof KotpTransportError) {
		// 결과 불명확: 같은 idempotencyKey로 issue 재시도
	} else {
		throw error;
	}
}

응답은 타입이 없는 JSON(unknown)입니다. 타입이 있는 결과가 필요하면 @k-otp/sdk/server를 쓰세요. 생성자에서 baseUrl, timeoutMs, 사용자 지정 fetch를 설정할 수 있습니다. browser export 조건에서는 클라이언트를 생성하는 순간 오류가 나므로, 서버 클라이언트가 브라우저 번들에서 실행될 수 없습니다.

이 페이지의 내용