언어별 서버 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.Sdk | 0.1.0 | GitHub Release: KOtp.Sdk.0.1.0.nupkg | .NET 8 |
| Java | dev.kotp:kotp-sdk | 0.1.0 | GitHub Release: JAR + POM (Java 17 바이트코드) | JDK 21 |
| Kotlin/JVM | dev.kotp:kotp-sdk (Java JAR) | 0.1.0 | Java와 같은 JAR | Kotlin 2.1.20, JDK 21 |
| PHP | k-otp/sdk | 0.1.0 | GitHub Release: kotp-sdk-php-0.1.0.zip | PHP 8.4 (8.2 이상 필요) |
| Go | github.com/k-otp/sdk/sdks/go | v0.1.0 | Go 모듈 프록시 | Go 1.26 |
| Python | kotp-sdk (임포트는 kotp_sdk) | 0.1.0 | GitHub Release: wheel 또는 sdist | Python 3.13 |
| Ruby | kotp_sdk | 0.1.0 | GitHub Release: kotp_sdk-0.1.0.gem | Ruby 3.3 |
| Dart | kotp_sdk | 0.1.0 | GitHub Release: kotp-sdk-dart-0.1.0.tar.gz | Dart 3.9, 서버(dart:io) |
| TypeScript | @k-otp/sdk/kiota | npm 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/Kotlin | new KotpClient.RequestOptions(Map.of(), null, true) |
| PHP | ['retry503' => true] |
| Go | kotp.RequestOptions{Retry503: true} |
| Python | retry503=True |
| Ruby | retry503: true |
| Dart | options: 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, RetryAfterMs | KotpTransportException |
| Java/Kotlin | KotpClient.KotpApiException: status, envelope, requestId, retryAfterMs | KotpClient.KotpTransportException |
| PHP | KOtp\KotpApiError: status, envelope, requestId, retryAfterMs | KOtp\KotpTransportError |
| Go | *kotp.APIError: Status, Envelope, RequestID, RetryAfterMs | *kotp.TransportError |
| Python | KotpApiError: status, envelope, request_id, retry_after_ms | KotpTransportError |
| Ruby | KOtp::ApiError: status, envelope, request_id, retry_after_ms | KOtp::TransportError |
| Dart | KotpApiError: status, envelope, requestId, retryAfterMs | KotpTransportError |
| TypeScript | KotpApiError: status, envelope, requestId, retryAfterMs | KotpTransportError |
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.0using 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.0use 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.0import (
"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.whlimport 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.gemrequire '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_sdkdependencies:
kotp_sdk:
path: third_party/kotp_sdkimport '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/sdkimport { 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 조건에서는 클라이언트를 생성하는 순간 오류가 나므로, 서버 클라이언트가 브라우저 번들에서 실행될 수 없습니다.