Skip to Content

OTP API

OTP API는 SMS·카카오 알림톡·이메일 채널로 일회용 인증을 처리하는 API입니다. 숫자 코드를 입력받는 code 모드와, 수신자가 링크를 눌러 인증을 완료하는 **link 모드(매직링크)**를 지원합니다.

발송 → 검증 → (필요 시) 재발송 → 상태 조회의 전 과정을 이 문서에서 다룹니다.

인증과 스코프

모든 OTP 엔드포인트는 Notification API 공통 인증(API 키 + HMAC 서명)을 사용합니다. 요청마다 아래 4개 헤더가 필요합니다. 서명 생성 방법은 인증 문서를 참고하세요.

헤더설명
x-api-keykeyId.keySecret 형식의 API 키
x-timestampUnix epoch 초. 허용 오차를 벗어나면 401
x-nonce요청마다 고유한 임의 문자열. 재사용 시 401 (Replay detected)
x-signatureHMAC-SHA256(hmacSecret, "METHOD\npath\ntimestamp\nnonce\nsha256(rawBody)") 의 hex

엔드포인트별로 요구하는 스코프가 다릅니다. 스코프가 부족하면 403 (API key missing required scope: <scope>)으로 응답합니다.

스코프허용 작업
otp:send발송, 재발송
otp:verify코드 검증, 상태 조회

엔드포인트 요약

메서드경로스코프설명
POST/v1/otpotp:sendOTP 발송 요청
POST/v1/otp/:verificationId/verifyotp:verify코드 검증
POST/v1/otp/:verificationId/resendotp:send재발송 (코드 회전)
GET/v1/otp/:verificationIdotp:verify상태 조회 (폴링)
GET/v1/otp/link/:linkToken없음 (공개)매직링크 랜딩 페이지
POST/v1/otp/link/:linkToken없음 (공개)매직링크 인증 완료

OTP 발송

POSThttps://api.posmit.io/v1/otpscope otp:send

지정한 채널로 인증 코드(또는 매직링크)를 발송하고 검증 세션을 생성합니다.

요청 필드

필드타입필수설명
channelstringOsms, kakao_alimtalk, email 중 하나
tostringO전화번호 또는 이메일. 최대 320자
modestringXcode(기본) 또는 link
purposestringX용도 태그(signup, login, reset 등). 최대 60자
brandNamestringX메시지 양식의 브랜드 슬롯. 최대 20자
recipientNamestringX수신자 이름. 최대 120자
templateCodestringX알림톡 템플릿 코드. kakao_alimtalk 채널에서는 필수
templateVariablesobjectX알림톡 템플릿 추가 변수 (문자열 맵)
preferredProviderAccountIdsstring[]X우선 사용할 공급자 계정 UUID. 최대 10개

전화번호는 공백·괄호·.·- 를 제거해 정규화하고, 이메일은 trim 후 소문자로 정규화합니다. 알림톡 채널에서 templateCode 를 생략하면 400 (templateCode is required for kakao_alimtalk channel)이 발생합니다. templateVariablescode, brand, minutes, (link 모드의) link 는 서버가 값을 주입하므로 직접 지정해도 덮어써집니다.

요청 예시

curl -X POST https://api.posmit.io/v1/otp \ -H "Content-Type: application/json" \ -H "x-api-key: pk_xxx.sk_xxx" \ -H "x-timestamp: 1783944000" \ -H "x-nonce: 4f9d2c1e-unique-nonce" \ -H "x-signature: COMPUTED_HMAC_SIGNATURE" \ -d '{ "channel": "sms", "to": "010-1234-5678", "mode": "code", "purpose": "login", "brandName": "Posmit" }'

응답

모든 2xx 응답은 공통 규약에 따라 data + meta.timestamp 봉투(envelope)로 감싸 반환됩니다.

{ "data": { "verificationId": "550e8400-e29b-41d4-a716-446655440000", "status": "pending", "mode": "code", "channel": "sms", "expiresAt": "2026-07-10T12:03:00.000Z", "resendAvailableAt": "2026-07-10T12:00:30.000Z" }, "meta": { "timestamp": "2026-07-10T12:00:00.000Z" } }
필드설명
verificationId검증 세션 ID. 이후 검증·재발송·조회에 사용합니다
status항상 pending 으로 시작합니다
expiresAt코드/링크 만료 시각 (기본 발송 후 3분)
resendAvailableAt재발송이 가능해지는 시각 (쿨다운 기본 30초)

코드·링크 토큰의 평문은 저장되지 않습니다. 코드는 세션에 바인딩된 HMAC 해시로만, 링크 토큰은 SHA-256 해시로만 보관되므로 응답이나 조회로 평문을 다시 얻을 수 없습니다.

발송이 동기적으로 실패하면(예: 미승인 알림톡 템플릿) 세션이 failed 처리되고 요청 자체가 에러로 응답합니다. 이 경우 새 발송 요청을 다시 보내야 합니다.

코드 검증

POSThttps://api.posmit.io/v1/otp/:verificationId/verifyscope otp:verify

수신자가 입력한 코드를 검증합니다. 성공·실패 모두 HTTP 200 으로 응답하고, 결과는 status 필드로 구분합니다.

요청 예시

curl -X POST https://api.posmit.io/v1/otp/550e8400-e29b-41d4-a716-446655440000/verify \ -H "Content-Type: application/json" \ -H "x-api-key: pk_xxx.sk_xxx" \ -H "x-timestamp: 1783944060" \ -H "x-nonce: another-unique-nonce" \ -H "x-signature: COMPUTED_HMAC_SIGNATURE" \ -d '{ "code": "123456" }'

응답 — 성공

{ "data": { "status": "verified", "verificationToken": "eyJ2aWQiOiI1NTBlODQwMC4uLiJ9.9a3b1c...", "verifiedAt": "2026-07-10T12:01:20.000Z" }, "meta": { "timestamp": "2026-07-10T12:01:20.000Z" } }

응답 — 실패

{ "data": { "status": "failed", "reason": "invalid_code", "attemptsRemaining": 3 }, "meta": { "timestamp": "2026-07-10T12:01:25.000Z" } }
reason의미attemptsRemaining
expired세션이 없거나 만료됨0
not_code_modelink 모드 세션에 코드 검증을 시도함0
too_many_attempts최대 시도 횟수 도달. 세션이 failed 로 종료됨0
invalid_code코드 불일치남은 시도 횟수

코드 비교는 timing-safe 비교로 수행되고, 성공 시 코드 해시는 세션에서 즉시 제거됩니다. 이미 verified 인 세션에 다시 verify 를 호출하면 멱등으로 verified 와 새로 서명된 토큰을 반환합니다 (검증 마커는 토큰 TTL 동안 유지됩니다).

재발송

POSThttps://api.posmit.io/v1/otp/:verificationId/resendscope otp:send

같은 세션에서 새 코드(또는 새 링크)를 발송합니다. 요청 바디는 없고, 응답은 발송 응답과 동일한 형태입니다 (새 expiresAt / resendAvailableAt).

재발송은 항상 새 코드로 회전합니다. 평문을 저장하지 않으므로 같은 코드를 다시 보낼 수 없고, 이전 코드·링크는 즉시 무효화됩니다. 시도 횟수는 0으로 초기화되고 TTL도 새 코드 기준으로 재설정됩니다.

에러

상태 코드조건detail
404세션 없음 / 다른 워크스페이스OTP verification not found or expired
400이미 검증 완료OTP is already verified
400만료됨OTP has expired
429최대 재발송 초과Maximum resend count exceeded
429쿨다운 중Resend cooldown active

에러는 공통 규약대로 application/problem+json 형식으로 응답합니다. 쿨다운 중 재발송 예시입니다.

{ "type": "https://api.posmit.io/errors/noti-42900", "title": "Too Many Requests", "status": 429, "detail": "Resend cooldown active", "code": "NOTI-42900", "instance": "/v1/otp/550e8400-e29b-41d4-a716-446655440000/resend", "timestamp": "2026-07-10T12:00:12.000Z" }

쿨다운이 풀리는 시각은 에러 응답에 포함되지 않으므로, 직전 발송/재발송 응답의 resendAvailableAt 을 기준으로 재시도 시점을 계산하세요.

재발송도 수신처별 발송 한도(아래 만료 정책과 레이트리밋)를 소모합니다.

상태 조회

GEThttps://api.posmit.io/v1/otp/:verificationIdscope otp:verify

검증 세션의 현재 상태를 조회합니다. 매직링크의 크로스 디바이스 완료 감지에도 이 엔드포인트를 폴링합니다.

{ "data": { "verificationId": "550e8400-e29b-41d4-a716-446655440000", "status": "verified", "channel": "sms", "mode": "code", "purpose": "login", "recipientMasked": "*******5678", "attempts": 1, "resendCount": 0, "expiresAt": "2026-07-10T12:03:00.000Z", "verifiedAt": "2026-07-10T12:01:20.000Z", "verificationToken": "eyJ2aWQiOi4uLg.9a3b1c..." }, "meta": { "timestamp": "2026-07-10T12:01:30.000Z" } }
  • statuspending, verified, expired, failed, canceled 중 하나입니다.
  • verificationTokenstatusverified 일 때만 포함됩니다.
  • pending 세션이 만료 시각을 지났으면 조회 시점에 expired 로 전이됩니다(lazy 처리).
  • 수신처는 마스킹된 형태로만 반환됩니다. 이메일은 로컬파트 앞 2자만(se****@naver.com), 전화번호는 끝 4자리만 노출합니다.
  • 레코드가 없거나 다른 워크스페이스의 세션이면 404 (OTP verification not found)입니다.

mode: "link" 로 발송하면 수신자는 코드 입력 대신 메시지의 링크를 눌러 인증을 완료합니다. 링크 URL 형식은 https://api.posmit.io/v1/otp/link/{linkToken} 입니다.

GEThttps://api.posmit.io/v1/otp/link/:linkToken

HTML 랜딩 페이지만 렌더링하며 상태를 변경하지 않습니다. 메일 보안 스캐너나 링크 미리보기 봇이 링크를 자동 GET 해도 인증이 완료되지 않도록 하기 위한 설계입니다. 토큰이 유효하면 “인증 완료하기” 버튼(POST 폼)이, 무효·만료면 실패 페이지가 표시됩니다.

POSThttps://api.posmit.io/v1/otp/link/:linkToken

실제 인증 완료 요청입니다. 랜딩 페이지의 버튼이 이 요청을 보내고, 성공·실패 HTML을 반환합니다. 이미 verified 인 세션이면 멱등으로 성공 처리됩니다.

verificationToken 은 수신자 브라우저에 절대 노출되지 않습니다. 링크 완료는 세션 상태만 verified 로 전환하고, 토큰은 고객사 백엔드가 GET /v1/otp/:verificationId 로 받아야 합니다.

크로스 디바이스 완료 감지 (폴링 패턴)

수신자가 다른 기기(휴대폰)에서 링크를 누르는 동안, 원래 화면(웹 로그인 페이지 등)의 백엔드는 상태 조회 엔드포인트를 폴링해 완료를 감지합니다.

async function waitForLinkVerification(verificationId: string): Promise<string | null> { const deadline = Date.now() + 180_000; // 세션 TTL(기본 3분)만큼 대기 while (Date.now() < deadline) { const res = await fetch(`https://api.posmit.io/v1/otp/${verificationId}`, { headers: buildHmacAuthHeaders('GET', `/v1/otp/${verificationId}`), // 인증 문서 참고 }); const body = await res.json(); if (body.data.status === 'verified') return body.data.verificationToken; if (body.data.status === 'expired' || body.data.status === 'failed') return null; await new Promise((r) => setTimeout(r, 2000)); // 2초 간격 폴링 } return null; }

verificationToken

검증 성공 시 발급되는 토큰은 JWT가 아니라 자체 컴팩트 형식입니다.

base64url(JSON payload) + "." + HMAC-SHA256-hex(OTP_SIGNING_SECRET, base64url 부분)

payload 클레임:

클레임의미
vidverificationId
wsid워크스페이스 ID
purpose발송 시 지정한 용도 태그
channel발송 채널
sub마스킹된 수신처
iat / exp발급·만료 시각. TTL 기본 300초

고객사에서 토큰을 신뢰하는 방법은 두 가지입니다.

  • A 패턴 (오프라인 검증) — 서명 시크릿을 공유받았다면 토큰의 HMAC을 직접 재계산해 검증합니다.
  • B 패턴 (서버 재확인, 권장)GET /v1/otp/:verificationId 를 호출해 서버에서 결과를 재확인합니다.

만료 정책과 레이트리밋

v1은 전역 기본 정책이 적용됩니다 (워크스페이스별 커스터마이징은 아직 지원하지 않습니다).

항목기본값
코드 길이6자리
코드/링크 유효시간 (TTL)180초 (3분)
최대 검증 시도5회
재발송 쿨다운30초
최대 재발송 횟수3회
수신처별 시간당 발송 한도5회
수신처별 일일 발송 한도10회
verificationToken TTL300초 (5분)

수신처별 발송 한도를 초과하면 발송·재발송 요청이 429 (application/problem+json)로 거절됩니다.

{ "type": "https://api.posmit.io/errors/noti-42900", "title": "Too Many Requests", "status": 429, "detail": "OTP send rate limit exceeded", "code": "NOTI-42900", "instance": "/v1/otp", "timestamp": "2026-07-10T12:00:00.000Z" }
  • 시간당 한도는 다음 UTC 정시에, 일일 한도는 UTC 자정에 초기화됩니다. 남은 대기 시간은 응답에 포함되지 않습니다.

발송 한도는 워크스페이스 + 정규화된 수신처 단위로 집계됩니다. 같은 번호에 하이픈 유무만 다르게 보내도 동일 수신처로 계산됩니다.

Last updated on