SDK
K-OTP 공식 SDK. JavaScript/TypeScript SDK는 브라우저, 서버, React, Vue, Svelte, UI 컴포넌트를 하나로 담은 패키지(`npm i @k-otp/sdk`)이며, C#, Java, Kotlin, PHP, Go, Python, Ruby, Dart용 프리뷰 서버 SDK도 있습니다.
공식 SDK는 오픈소스(github.com/k-otp/sdk)입니다. 이 문서의 curl, fetch 예제처럼 어떤 언어에서든 REST API(https://api.k-otp.dev/v1)를 직접 호출해도 됩니다.
언어
| 언어 | 패키지 | 상태 | 실행 환경 |
|---|---|---|---|
| JavaScript, TypeScript | npm의 @k-otp/sdk | 안정, 2.x | 브라우저(pk_)와 서버(sk_) |
| C#, Java, Kotlin/JVM, PHP, Go, Python, Ruby, Dart | 언어별 서버 SDK 참고 | 프리뷰, 0.2.0 | 서버 전용(sk_) |
브라우저, pk_ 키, 프레임워크, UI 연동은 @k-otp/sdk에만 있습니다. 이 페이지의 나머지는 @k-otp/sdk를 다룹니다.
설치
npm i @k-otp/sdk
# 또는: pnpm add @k-otp/sdk, bun add @k-otp/sdk, yarn add @k-otp/sdkReact(>= 18), Vue(>= 3.3), Svelte(>= 4)는 선택적 peer 의존성입니다. 앱이 이미 쓰는 프레임워크가 있으므로 따로 설치할 것은 없고, 해당 프레임워크의 서브패스만 임포트하면 됩니다. 선택적 UI 테마는 스타일시트 임포트입니다. import "@k-otp/sdk/ui/theme.css". fetch가 있는 런타임(최신 브라우저, Node.js 20.19 이상, Bun, Deno, Cloudflare Workers)이 필요합니다.
패키지 하나, 여러 임포트
모든 기능은 npm 패키지 @k-otp/sdk 하나에 서브패스별로 들어 있습니다. React, Vue, Svelte는 선택적 peer 의존성이므로 앱이 이미 쓰는 프레임워크만 설치하고 해당 서브패스만 임포트하면 됩니다.
| 임포트 | 용도 | 사용하는 키 |
|---|---|---|
@k-otp/sdk (@k-otp/sdk/core와 같음) | 브라우저, SSR, 엣지에서 issue, verify. <script> 번들(window.KOtp)도 제공 | pk_ |
@k-otp/sdk/headless | 프레임워크 바인딩의 바탕이 되는, 프레임워크 독립 발급 → 검증 플로우 | pk_ |
@k-otp/sdk/server | 발급·검증·상태·이력·원장·잔액·템플릿 등 공개 API 전체(Node.js 20.19 이상, Bun, Deno, Workers) | sk_ |
@k-otp/sdk/react | OtpProvider, useOtpIssue, useOtpVerify, useOtpFlow (React 18, 19) | pk_ |
@k-otp/sdk/vue | createOtpPlugin, useOtp, useOtpFlow (Vue 3.3 이상) | pk_ |
@k-otp/sdk/svelte | createOtpStores, 플로우 스토어, use:otpForm 액션 (Svelte 4, 5) | pk_ |
@k-otp/sdk/ui, /ui/react, /ui/vue, /ui/svelte | 완성형 인증 폼: UI 컴포넌트 참고 | pk_ |
@k-otp/sdk/ui/theme.css | UI 컴포넌트용 선택적 기본 테마 | 없음 |
@k-otp/sdk/kiota | 다른 언어 SDK와 같은 형태의 JSON 입력·JSON 출력 서버 클라이언트(Node.js 20.19 이상, 1.3.0+) | sk_ |
@k-otp/sdk/contract | oRPC 계약과 OpenAPI에서 생성한 타입(직접 클라이언트를 만들 때) | 모두 |
프레임워크 임포트는 코어 위의 얇은 계층으로 동작이 같습니다. 로딩·오류 상태, 재발송 쿨다운, 발송 시도마다 멱등키 하나를 관리합니다. 각 임포트는 필요한 것만 불러옵니다. 예를 들어 @k-otp/sdk/react는 Vue, Svelte, UI 컴포넌트, 서버 클라이언트를 불러오지 않습니다. 모든 임포트는 하나의 패키지 버전을 공유합니다.
TypeScript는 패키지 exports로 서브패스를 해석하므로 "moduleResolution"을 "bundler", "node16", "nodenext" 중 하나로 설정해야 합니다.
빠른 시작
import { createIdempotencyKey, createOtpClient } from "@k-otp/sdk";
// baseUrl 기본값은 https://api.k-otp.dev/v1 입니다.
const otp = createOtpClient({ apiKey: "pk_..." });
// "인증번호 보내기" 동작마다 키를 하나 만들어 저장하고, 그 동작을 재시도할 때 같은 키를 쓰세요.
// SDK에서는 필수이며, 비어 있으면 요청 전에 BAD_REQUEST 오류가 납니다.
const idempotencyKey = createIdempotencyKey("login");
const { issueId } = await otp.issue({
phoneNumber: "01012345678",
purpose: "login",
idempotencyKey,
});
// 틀린 코드는 오류가 아니라 verified: false와 reasonCode로 반환됩니다.
const result = await otp.verify({ issueId, code: "123456" });import { createOtpServerClient } from "@k-otp/sdk/server";
// sk_ 키만 허용합니다. 브라우저에서는 실행을 거부합니다.
const otp = createOtpServerClient({ apiKey: process.env.KOTP_SECRET_KEY! });
const status = await otp.getStatus({ issueId });
const balance = await otp.getBalance();import { OtpProvider, useOtpFlow } from "@k-otp/sdk/react";
// <OtpProvider options={{ apiKey: "pk_..." }}> ... </OtpProvider>
function VerifyPhone() {
const otp = useOtpFlow({ resendCooldownMs: 30_000 });
// otp.send({ phoneNumber, purpose: "signup" }), otp.verify(code),
// otp.canSend, otp.cooldownRemainingMs, otp.verified, otp.reasonCode, otp.error
}Vue와 Svelte 임포트도 같은 API 형태입니다. 훅 대신 완성형 폼을 쓰려면 UI 컴포넌트를 참고하세요.
서버 코드는 브라우저로 가지 않습니다
@k-otp/sdk/server는 sk_ 비밀 키가 필요하므로 브라우저 번들에 들어가면 안 됩니다. 브라우저용으로 빌드하는 번들러(Vite, webpack, esbuild, Rollup, Metro)는 browser export 조건으로 이 경로를 같은 export를 가진 스텁으로 해석합니다. 상수는 실제 값이고, OtpApiError 생성자를 포함한 모든 함수는 @k-otp/sdk/server가 브라우저용으로 해석되었다는 오류를 던집니다. 번들은 빌드되지만 서버 클라이언트는 들어가지 않고, 첫 호출에서 바로 실패합니다. 서버 런타임(Node.js, Bun, Deno, Cloudflare Workers의 workerd, Vercel Edge의 edge-light)은 실제 클라이언트를 받습니다.
서버에서 실행해야 하는 코드에서 이 오류가 나면 빌드가 브라우저 조건을 썼다는 뜻입니다. 예를 들어 ssr.target: "webworker"인 Vite SSR이나 jsdom 테스트 환경입니다. 서버 조건(workerd, worker, node)으로 해석하거나 node 환경에서 테스트를 실행하세요.
클라이언트 옵션
| 옵션 | 기본값 | 설명 |
|---|---|---|
apiKey | 필수 | 브라우저에서는 pk_ 키, 서버에서는 sk_ 키. 요청마다 호출되는 (비동기도 가능한) 함수일 수도 있습니다. |
baseUrl | https://api.k-otp.dev/v1 | /v1을 포함해야 합니다. 빼면 /v1/issue가 아니라 /issue로 요청이 가서 실패합니다. 끝의 /는 있어도 됩니다. |
timeoutMs | 10000 | 요청당 시간 제한. 0이면 끕니다. 호출마다 { timeoutMs }로 바꿀 수 있습니다. |
fetch | globalThis.fetch | 직접 지정하는 fetch(테스트, 프록시). |
headers | 추가 요청 헤더. authorization과 idempotency-key는 덮어쓸 수 없습니다. | |
hooks | onRequestStart, onRequestEnd 텔레메트리 콜백. |
pk_ 키는 키에 등록된 허용 출처(스킴 + 호스트 + 포트)에서만 동작합니다. 인증을 참고하세요.
오류
SDK 호출이 실패하면 항상 OtpApiError로 거절됩니다. 설정 실수(키 누락, 브라우저에서 sk_ 키 사용, @k-otp/sdk/server에 sk_가 아닌 키)는 TypeError를 던집니다.
| 필드 | 설명 |
|---|---|
code | BAD_REQUEST, UNAUTHORIZED, PAYMENT_REQUIRED, FORBIDDEN, NOT_FOUND, CONFLICT, TOO_MANY_REQUESTS, INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE, TIMEOUT, NETWORK_ERROR, ABORTED, UNKNOWN. HTTP 코드는 오류를 따르고, 마지막 네 가지는 클라이언트 쪽 결과입니다. |
status | HTTP 상태 코드. 응답을 받지 못했으면(타임아웃, 네트워크 오류, 중단) 0입니다. |
message | 서버 메시지, 또는 클라이언트가 만든 설명입니다. |
requestId | 읽을 수 있을 때 X-Request-Id 응답 헤더 값. 지원팀에 문의할 때 함께 알려 주세요. |
data | 오류 데이터. 예: 402는 { code: "INSUFFICIENT_CREDIT" }, 429는 { limit, policy, retryAfterMs }. |
retryAfterMs | 재시도 전에 기다릴 시간(밀리초). 본문의 data.retryAfterMs, 없으면 Retry-After 헤더 값입니다. 429와, 대기 시간을 보내는 503에 설정됩니다. |
retryable | TOO_MANY_REQUESTS, INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE, TIMEOUT, NETWORK_ERROR이면 true입니다. |
cause | 원인이 된 오류. 예: fetch 실패. |
재시도할 수 있는 issue는 같은 멱등키로 재시도하세요. 타임아웃이나 503 뒤에는 발송 여부가 불분명할 수 있습니다. 멱등성을 참고하세요.
import { isOtpApiError } from "@k-otp/sdk";
try {
await otp.issue({ phoneNumber, purpose: "login", idempotencyKey });
} catch (error) {
if (isOtpApiError(error) && error.retryable) {
// error.retryAfterMs(있으면)만큼 기다린 뒤 같은 idempotencyKey로 재시도
}
}헤드리스 플로우와 React, Vue, Svelte 패키지는 이를 대신 처리합니다. 재시도 가능한 실패 뒤에 같은 입력이면 같은 키를 다시 쓰고, 발송에 성공할 때마다 재발송 쿨다운(resendCooldownMs, 기본 30초)을 시작하며, 429/503이면 retryAfterMs 동안 쿨다운합니다. 속도 제한에 걸린 verify 뒤에는 별도의 검증 쿨다운을 둡니다. 이 액션들은 API 오류로 거절되지 않고 { data }, { error }, { skipped }로 결과를 돌려줍니다.
SDK 2.1.0 변경 사항
SDK 2.1.0은 인증번호를 카카오 알림톡으로만 보내고 SMS는 알림톡 실패 시 자동 대체로만 쓰는 API 2.x를 지원합니다. 지운 것은 없어 2.0.0용 코드는 그대로 컴파일됩니다.
- 발급 요청 오류.
getIssueRequestErrorCode(error)로 400의CHANNEL_NOT_SUPPORTED,TEMPLATE_NOT_AVAILABLE,WEB_OTP_NOT_SUPPORTED를 읽습니다(error.code는BAD_REQUEST그대로). - 지원 중단.
channel: "sms",webOtp옵션과 결과,@k-otp/sdk/ui의 WebOTP 헬퍼와 props,messageType. 넘기면 그대로 보내므로 API가 명확한 400으로 답하며, 다음 major 버전에서 제거합니다. 폼의webOtp설정 기본값은 이제false입니다. - 실제 발송 채널. SMS 대체 발송 뒤에는
listIssues()/getIssue()가 여전히channel: "sms"를 보고할 수 있습니다(OtpDeliveredChannel). - 결제 오류. 402
data타입에TRIAL_DAILY_LIMIT_EXCEEDED의resetAt이 포함됩니다.
SDK 2.0.0 변경 사항
- HTTPS
baseUrl. 루프백 주소(localhost,*.localhost,127.0.0.0/8,[::1])가 아닌http://baseUrl은 거부합니다. 다른 호스트로 로컬·테스트 환경을 구성할 때만allowInsecureHttp: true를 넘기세요(개발·테스트 전용). - 결과가 불분명한
issue는 멱등키를 유지합니다. 시간 초과, 네트워크 오류, 리다이렉트, SDK가 읽을 수 없는 성공 응답에서도 진행 중인 멱등키를 버리지 않습니다. 같은 요청을 다시 시도하면 같은 키를 쓰므로 인증번호가 두 번 발송되지 않습니다.isAmbiguousIssueError(error)로 이런 경우와 확정 실패를 구분할 수 있습니다. - 시간 제한이 키 조회도 포함합니다.
timeoutMs와 abort 신호가 비동기apiKey조회에도 적용됩니다.
크레딧 지갑 필드 (API 1.4.0)
API의 조직 단위 크레딧 지갑 릴리스(스펙 1.4.0)에서 @k-otp/sdk/server가 다음 필드를 제공합니다.
getBalance()가walletId,walletScope("organization", 앱별 지갑이면"app"), 지갑이 공유될 때의organizationId도 돌려줍니다.appId는 계속 호출한 앱입니다.listCreditLedger()와iterateCreditLedger()의 항목에 선택값appId가 추가됩니다.
아직 업데이트되지 않은 API는 이 필드를 보내지 않으므로 SDK 타입에서 walletId와 walletScope는 선택값입니다. 그 릴리스 전까지 잔액은 크레딧과 결제에 설명된 대로 앱 지갑 잔액입니다.