K-OTP

OTP 발급

POST /v1/issue

POST https://api.k-otp.dev/v1/issue

개요

Generates a 6-digit OTP, applies quota admission, records the issue, and queues SMS or AlimTalk delivery. The plaintext code is never returned; only issueId, expiry, and attempt metadata are.

Idempotency. An idempotency key is required, either in the Idempotency-Key header or the body idempotencyKey field (both must match when both are sent). A retry with the same key and payload returns the original response without re-sending or re-debiting. When the outcome of the first attempt is unknown (timeout, 503), retry only with the same key.

Replacement. Issuing again for the same phone number and purpose (with a new key) marks the previous issued OTP as replaced.

Billing. Credit is reserved at admission and finalized asynchronously after send processing; the response does not include balance or debit amounts. The charged amount is decided server-side per channel/message type (SMS, LMS, AlimTalk); the deprecated cost field is ignored for billing for every key type. Quota rejection returns 402 with data.code.

Accepts pk_ public keys (browser, Origin must exactly match an allowed origin) or sk_ secret keys.

인증

허용 키필요 scope대체 허용 scope
pk_ / sk_otp:issueotp:send, *

파라미터

이름위치타입필수설명
Idempotency-KeyheaderstringClient-generated key that makes POST /issue safe to retry. Required for every issue request unless the body idempotencyKey is provided; if both are sent they must match. After trimming, the key must be 1-128 visible ASCII characters (no spaces). Retrying with the same key and payload replays the original result without a second send or debit; reusing a key with a different payload returns 409 CONFLICT. If the first attempt timed out or returned 503, retry with the same key — never mint a new key, because the provider outcome may be ambiguous and a new key can cause a duplicate SMS.

요청 본문

필드타입필수설명
phoneNumberstring예Recipient mobile number (at most 32 characters after trimming). Stored only as a hash and never returned by any endpoint.
purposestring예Caller-defined purpose label (1-64 characters after trimming). A new issue for the same phone number and purpose replaces the previous active issue.
channel"alimtalk" | "sms"Delivery channel. Defaults to sms.
templateIdstringWhitelisted template id from GET /v1/templates. The default template is used when omitted.
templateVariablesRecord<string, string>Values for the template's declared variables (see GET /v1/templates/{templateId}). code is always supplied by the server and must not be included.
fromstringOptional sender identity override (at most 64 characters after trimming).
messageType"LMS" | "SMS"SMS message type for the sms channel.
costnumberDeprecated and ignored for billing. The credit amount reserved and debited is always decided server-side from the channel and message type (SMS, LMS, AlimTalk) and cannot be overridden by any key type. Still accepted for compatibility: when sent it must be an integer from 1 to 100000, and it remains part of the idempotency fingerprint.
metadataRecord<string, string>Free-form string key/value tags stored with the issue. Subject to a server-side size budget and never returned by dashboard endpoints.
expiresInSecnumberOTP lifetime in seconds: an integer from 30 to 600. Defaults to 180.
maxAttemptsnumberMaximum verification attempts: an integer from 1 to 10. Defaults to 5.
idempotencyKeystringIdempotency key; alternative to the Idempotency-Key header. One of the two is required, and they must match when both are sent. 1-128 visible ASCII characters without spaces after trimming. Reuse the same key when retrying.

응답 (200)

OTP issued (or an idempotent replay of an earlier issue with the same key).

필드타입필수설명
issueIdstring예Identifier of the issued OTP; pass it to POST /v1/verify.
expiresAtstring예When the OTP stops being verifiable (RFC 3339).
attemptsRemainingnumber예Verification attempts left for this issue.
queuedAtstring예When delivery was queued (RFC 3339).

오류 응답

모든 오류는 { defined, code, status, message, data? } 형태입니다. 자세한 내용은 오류를 참고하세요.

상태code설명
400BAD_REQUESTinvalid payload, missing/blank idempotency key, header/body key mismatch, unknown template or template variables, or expiresInSec/maxAttempts/cost out of range (cost is still range-checked though it no longer affects billing).
401UNAUTHORIZEDthe Authorization: Bearer credential is missing, malformed, inactive, or rejected by introspection.
402PAYMENT_REQUIREDquota admission rejected the issue. data.code is INSUFFICIENT_CREDIT or OVERDRAFT_LIMIT_EXCEEDED; top up the wallet before retrying.
403FORBIDDENthe credential lacks otp:issue (or an accepted alias), or a pk_ public key was sent without an Origin header / with an Origin that is not an exact match for one of the key's allowedOrigins.
409CONFLICTthe idempotency key was already used with a different payload, or the replayed issue has since been replaced by a newer issue.
500INTERNAL_SERVER_ERRORunexpected server failure. Undefined errors (defined: false) use the same envelope.
503SERVICE_UNAVAILABLEintrospection or a downstream dependency is unavailable, or an earlier attempt with this idempotency key is still being resolved. Retry later with the same key.

요청 예시

curl -X POST "https://api.k-otp.dev/v1/issue" \
  -H "Authorization: Bearer $KOTP_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"phoneNumber":"01012345678","purpose":"login"}'

이 페이지의 내용