크레딧과 결제
선불 크레딧, 잔액 부족 시 402, 잔액·원장 조회
K-OTP는 선불 크레딧 방식입니다. 콘솔에서 앱 지갑에 크레딧을 충전하고, 발송 1건마다 크레딧 1개가 차감됩니다. 크레딧은 화폐가 아닌 수량 단위이며 API 응답의 currency는 항상 CREDIT입니다.
- 지갑은 앱 단위입니다. 같은 앱의 키는 모두 하나의 지갑을 공유합니다.
- 충전은 콘솔에서 합니다. 공개 API로는 충전할 수 없습니다.
크레딧 1개 = OTP 발송 1건
OTP 발송 1건은 SMS, 알림톡, 알림톡 실패 후 SMS 자동 대체 발송 중 어느 채널로 전달되든 정확히 1 크레딧입니다. 대체 발송에 크레딧이 추가로 차감되지 않습니다. 채널과 대체 발송을 참고하세요.
크레딧 팩
| 팩 | 가격 | 크레딧 | 크레딧당 실질 가격 |
|---|---|---|---|
| Starter | $19.90 | 1,500 (기본 1,000 + 보너스 500) | 약 $0.0133 |
| Pro | $199.0 | 17,000 (기본 10,000 + 보너스 7,000) | 약 $0.0117 |
정가는 크레딧당 $0.0199이며, 보너스 크레딧만큼 실질 가격이 낮아집니다. 구독은 없습니다. 최신 팩은 k-otp.dev/ko/pricing에서 확인하세요.
차감 흐름
POST /v1/issue시점에 1 크레딧을 예약합니다. 잔액이 부족하면 이 단계에서 거절됩니다.- 발송 처리가 끝나면 비동기로 정산됩니다.
- 발송에 성공하면 예약분이 차감(debit) 됩니다.
- 발송이 실패하면 예약분이 해제(release) 되어 차감되지 않습니다.
- 발급 응답에는 잔액이나 차감 금액이 포함되지 않습니다. 필요하면 아래 조회 API를 사용하세요.
발급 건별 정산 상태(billingStatus)는 GET /v1/issues/{issueId}에서 확인할 수 있습니다.
billingStatus | 의미 |
|---|---|
reserved | 발급 시 크레딧을 예약함 |
debit_pending → debited | 차감 진행 중 → 차감 완료 |
release_pending → released | 예약 해제 진행 중 → 해제 완료(차감 없음) |
잔액 부족: 402 PAYMENT_REQUIRED
잔액이 부족하면 발급이 402로 거절되고 문자는 발송되지 않습니다. data.code로 사유를 구분합니다.
{
"defined": true,
"code": "PAYMENT_REQUIRED",
"status": 402,
"message": "PAYMENT_REQUIRED",
"data": { "code": "INSUFFICIENT_CREDIT" }
}data.code | 의미 |
|---|---|
INSUFFICIENT_CREDIT | 잔액이 부족합니다. |
OVERDRAFT_LIMIT_EXCEEDED | 앱에 허용된 초과 사용(overdraft) 한도를 넘었습니다. |
크레딧을 충전한 뒤 다시 시도하세요. 사용자에게는 "잠시 후 다시 시도" 같은 일반 오류로 보여 주고, 운영 알림으로 잔액 부족을 감지하는 것을 권장합니다.
잔액 조회
GET /v1/balance (sk_ 키, otp:balance)
curl "https://api.k-otp.dev/v1/balance" \
-H "Authorization: Bearer $KOTP_SECRET_KEY"{
"appId": "app_...",
"balance": 1200,
"currency": "CREDIT",
"updatedAt": "2026-09-28T03:00:05.000Z"
}원장 조회
GET /v1/credit-ledger (sk_ 키, otp:ledger:read)는 지갑의 변동 내역을 최신순으로 반환합니다.
entryType | 의미 | amountDelta |
|---|---|---|
credit | 충전 | 양수 |
debit | 발급 차감 | 음수 |
refund | 환불 | 양수 |
balanceAfter는 해당 항목 반영 후 잔액입니다. 초과 사용이 허용된 앱은 음수일 수 있습니다.issueId는 발급과 연결된 항목(debit,refund)에만 있습니다.- 페이지네이션:
limit(1–100, 기본 50), 응답의nextCursor를 다음 요청의cursor로 그대로 전달합니다.nextCursor가 없으면 마지막 페이지입니다. 페이지를 넘기는 동안 필터를 바꾸지 마세요. - 필터:
entryType,createdFrom(포함),createdTo(미포함) — RFC 3339 형식.
const API = "https://api.k-otp.dev/v1";
export async function* listLedger(createdFrom: string) {
let cursor: string | undefined;
do {
const params = new URLSearchParams({ limit: "100", createdFrom });
if (cursor) params.set("cursor", cursor);
const res = await fetch(`${API}/credit-ledger?${params}`, {
headers: { Authorization: `Bearer ${process.env.KOTP_SECRET_KEY}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.code}: ${body.message}`);
yield* body.items;
cursor = body.nextCursor;
} while (cursor);
}환불
사용하지 않은 크레딧은 구매 후 7일 이내에 청약철회할 수 있습니다. 일부 사용한 팩은 사용한 크레딧을 정가로 계산합니다. 자세한 내용은 환불 정책을 참고하세요.