SDK 레퍼런스
@saemol/sdk 0.4.0 — Node.js 18+ 서버용 클라이언트. 플랫폼 API 래퍼와 웹훅 서명 검증, 새몰 계정 로그인(SSO/OAuth 2.0) 헬퍼를 제공합니다. 런타임 의존성 없음, TypeScript 타입 내장. GitHub에서 소스 보기 ↗
클라이언트 생성
import { Saemol } from '@saemol/sdk'
const saemol = new Saemol({
secret: process.env.SAEMOL_SECRET!, // 필수 — 연동 정보 카드의 연동 시크릿
})| 옵션 | 기본값 | 설명 |
|---|---|---|
secret | (필수) | 상품별 연동 시크릿 — X-Saemol-Secret 헤더로 전송 |
baseUrl | https://saemol.com | 플랫폼 주소 (개발 환경에서 변경) |
timeoutMs | 10000 | 요청당 타임아웃 (ms) |
maxRetries | 2 | 조회·멱등 확인과 키를 지정한 쓰기의 최대 재시도. 결제 생성·사용량은 idempotencyKey가 있어야 재시도 |
retryDelayMs | 300 | 재시도 대기 (시도마다 2배 증가) |
fetch | 전역 fetch | 테스트·폴리필용 주입 |
onResponse | 없음 | { path, status, requestId, attempt } 관찰 콜백. attempt는 1부터 시작하며 키·본문은 전달하지 않음 |
메서드
| 메서드 | HTTP API | 용도 |
|---|---|---|
health() | GET /api/v1/health | 연동 테스트 |
verifyLicense({ licenseKey, deviceId?, version? }) | POST /api/v1/licenses/verify | 1회성 라이선스·인앱 토큰 검증 |
verifyEntitlement({ apiKey, version? }) | POST /api/v1/entitlements/verify | 구독/API/사용량/화이트라벨 권한 확인 |
reportUsage({ apiKey, quantity, metadata?, idempotencyKey? }) | POST /api/v1/usage/report | 사용량 집계 — Business 이상 |
checkUpdate({ currentVersion? }) | POST /api/v1/updates/check | 최신 버전·강제 업데이트 확인 |
currentUser({ licenseKey? | apiKey? }) | POST /api/v1/users/current | 키 소유 구매자 컨텍스트 조회 |
createCheckout({ itemCode, …, idempotencyKey? }) | POST /api/v1/checkout/sessions | 등록된 아이템 코드로 결제 링크 생성. 유료 거래는 운영 게이트 필요 |
getCheckout({ sessionId }) | GET /api/v1/checkout/sessions/{id} | 현재 결제 상태 확인 |
acknowledgeCheckout({ sessionId }) | POST /api/v1/checkout/sessions/{id}/acknowledge | 지급 트랜잭션 커밋 뒤 판매자 서버의 적용 완료 보고 |
customerPortal() | GET /api/v1/customer-portal | 새몰 로그인이 필요한 본인 서비스 관리 주소 |
canAccess({ apiKey, feature?, version? }) | verifyEntitlement 응답을 확인 | 최신 권한·만료·구매한 기능을 boolean으로 판단. 통신 오류는 예외 |
analytics({ period? }) | GET /api/v1/analytics/summary | SDK 분석 조회 — Business 이상 |
응답 필드는 HTTP API 응답과 1:1입니다. 대표 응답 형태:
// valid: true
{ valid: true, status: 'active', kind: 'license', plan_name: 'Pro',
sku: 'com.myapp.pro' | null,
devices: { registered: 2, limit: 3 | null },
expires_at: '2026-08-05T…' | null, // 기간제·trial 만료 (null=무기한)
is_trial: false }
// valid: false
{ valid: false, reason: 'revoked' | 'expired' | 'suspended'
| 'device_limit_exceeded' | 'version_not_allowed' | …,
status?, devices?, allowed_versions?, expires_at?, is_trial? }// 키가 조회되면 상태와 무관하게 전체 응답 (valid만 false일 수 있음)
{ valid: boolean, status: 'active' | 'expired' | 'revoked' | …,
kind: 'subscription' | 'usage' | 'api' | …, plan_name: 'Pro' | null,
plan_features: ['core', 'advanced'] | null, rate_limit_per_min: 60 | null,
usage: { unit, used, limit } | null, // kind가 usage일 때만
current_period_end: '…' | null, expires_at: '…' | null, is_trial: boolean }
// 버전 미허용 등 거부 응답 (reason 존재 여부로 판별)
{ valid: false, reason: 'version_not_allowed', allowed_versions: ['1.0'], … }{ accepted: true,
allowed: boolean, // false = 초과 정책 block → 사용 차단
usage: { unit, used, limit, remaining },
overage_policy: 'notify' | 'block',
over_limit: boolean }{ product_id, latest_version: '1.4.0' | null,
update_available: boolean, // currentVersion < latest
update_required: boolean, // currentVersion < min_version → 강제 업데이트
min_version: '1.0.0' | null, download_url: '…' | null, release_notes: '…' | null }// found: true — 키 소유 구매자 컨텍스트
{ found: true, entitlement_id, kind, status: 'active' | 'expired' | …,
plan_name: 'Pro' | null, is_trial: boolean, expires_at: '…' | null,
user: { id, name, email } }
// found: false (키 미존재 — 던지지 않고 반환)
{ found: false, reason: '…' }결제 복구·고객 관리 연결
판매자 서버에서 로그인 사용자를 확인한 뒤 등록된 아이템 코드를 보내세요. 가격은 새몰 DB에서 조회합니다. 구매 시도 ID는 서버에 저장하고 복구 요청에서도 재사용해야 합니다.
const checkout = await saemol.createCheckout({
itemCode: 'pro_monthly',
appUserId: loggedInUser.id,
idempotencyKey: savedPurchaseAttempt.id,
})
// checkout.url로 이동. 복귀 URL만으로 결제 완료를 판단하지 않습니다.
if (!(await saemol.canAccess({ apiKey, feature: 'export' }))) return denyAccess()
const { url } = await saemol.customerPortal() // 새몰 로그인 후 본인 서비스 관리서명된 웹훅과 최신 결제 상태를 확인하고, 이벤트 중복 처리 방지와 실제 지급을 같은 DB 트랜잭션에서 커밋하세요. 그 뒤 acknowledgeCheckout({ sessionId })을 호출합니다. 적용 확인은 판매자 서버의 보고이며 새몰이 외부 DB를 직접 검사한 결과가 아닙니다.
같은 멱등 키로 다른 요청을 보내면 409입니다. 사용량 복구 응답은 최초 저장 당시의 집계이므로 최신 권한은 다시 확인하세요. 서버별 키 권한과 실행 가능한 연결 도구는 연결과 운영 확인을 참고하세요.
새몰 계정 로그인 — SaemolOAuth
구독형 상품의 인증 연동 방식이 새몰 계정으로 로그인일 때 사용합니다. 구매자가 키를 복사·입력할 필요 없이 새몰 계정으로 로그인하고, 판매자는 같은 응답에서 구독 상태·플랜까지 받습니다. 자격증명은 상품 수정 페이지의 연동 정보 카드에서 확인합니다.
import { SaemolOAuth, isSubscriptionActive, hasPlanFeature } from '@saemol/sdk'
const oauth = new SaemolOAuth({
clientId: process.env.SAEMOL_OAUTH_CLIENT_ID!,
clientSecret: process.env.SAEMOL_OAUTH_CLIENT_SECRET!,
redirectUri: 'https://app.example.com/auth/callback',
})
// 1) 로그인 버튼 — state(CSRF)는 SDK가 생성해 반환
const { url, state } = oauth.createAuthorizationUrl()
session.oauthState = state
redirect(url)
// 2) 콜백 — 오류 확인·state 검증·토큰 교환·userinfo를 한 번에
const { user, tokens } = await oauth.handleCallback({
url: req.url,
expectedState: session.oauthState,
})
if (!isSubscriptionActive(user)) return showPricing()
if (hasPlanFeature(user, 'advanced')) enableAdvanced()| 메서드 | 용도 |
|---|---|
createAuthorizationUrl() | 동의 화면 URL + state 생성 |
handleCallback() | 콜백 한 번에 처리 (아래 3개를 묶은 것) |
exchangeCode() | 인가 코드 → 토큰 |
getUserInfo() | 액세스 토큰 → 구매자 정보·구독 상태 |
refresh() | 토큰 갱신 — 두 토큰 모두 회전 |
isSubscriptionActive() | 구독 개방 판정 (순수 함수) |
hasPlanFeature() | 플랜별 기능 게이팅 (순수 함수) |
redirectUri는 상품 설정의 OAuth Redirect URI와 정확히 일치해야 합니다 — 부분 일치·와일드카드는 허용되지 않습니다(오픈 리다이렉트 방지).scope로 무엇을 받을지 정합니다 — identity(로그인만) 또는 entitlement(로그인 + 구독 상태·플랜). 생략하면 entitlement가 기본이며, 같은 상품에서 페이지마다 다르게 요청할 수 있습니다.expectedState는 필수입니다. 서버 세션에 보관한 값을 콜백에서 한 번만 소비하세요. 누락·공백·불일치는 토큰 교환 전에 거부됩니다. 이미 저장한 구독 응답도 만료 시각과 최신 상태를 다시 확인해야 합니다.
실패는 SaemolOAuthError로 던져지며 code로 구분합니다 — access_denied(구매자 거부) · state_mismatch(CSRF 차단) · invalid_client · invalid_grant(코드 재사용·만료) · invalid_token(갱신 필요) · missing_code.
에러 규약
| 상황 | SDK 동작 |
|---|---|
| 키가 유효하지 않음 (미존재·회수·만료·제한 초과) | 던지지 않음 — valid: false 응답 반환 |
| 연동 설정 문제 (시크릿 불일치 401 · 잘못된 요청 400) | SaemolApiError |
| 일시 서버 장애 (502·503·504) | 멱등 호출은 재시도 후 SaemolApiError |
| 네트워크 실패·타임아웃 | SaemolNetworkError (timedOut 플래그) |
| 웹훅 검증 실패 | SaemolWebhookError (code로 원인 구분) |
import { SaemolApiError, SaemolNetworkError } from '@saemol/sdk'
try {
const result = await saemol.verifyLicense({ licenseKey })
} catch (err) {
if (err instanceof SaemolApiError) log(err.status, err.reason, err.requestId) // 설정 점검
if (err instanceof SaemolNetworkError) retryLater(err.timedOut)
}expectedState 전달과 오류 처리를 확인하세요. 401은 키·환경·만료·폐기, 403은 권한·요금제, 429는 Retry-After를 확인합니다. 서버 오류를 문의할 때 X-Request-Id를 첨부하고 키·웹훅 원문·개인정보를 로그에 남기지 마세요.SDK 없이 직접 호출
모든 요청에 X-Saemol-Secret 헤더를 붙여 호출하면 됩니다.
curl -X POST https://saemol.com/api/v1/licenses/verify \
-H 'Content-Type: application/json' \
-H 'X-Saemol-Secret: <연동 시크릿>' \
-d '{ "license_key": "MF-XXXX-XXXX-XXXX-XXXX", "device_id": "machine-abc" }'