베타 운영 안내·현재 베타로 운영하며, 유료 결제와 관련 기능은 준비가 끝나는 대로 순차적으로 연동합니다.운영 안내

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 헤더로 전송
baseUrlhttps://saemol.com플랫폼 주소 (개발 환경에서 변경)
timeoutMs10000요청당 타임아웃 (ms)
maxRetries2조회·멱등 확인과 키를 지정한 쓰기의 최대 재시도. 결제 생성·사용량은 idempotencyKey가 있어야 재시도
retryDelayMs300재시도 대기 (시도마다 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/verify1회성 라이선스·인앱 토큰 검증
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/summarySDK 분석 조회 — Business 이상

응답 필드는 HTTP API 응답과 1:1입니다. 대표 응답 형태:

verifyLicense 응답 (판별 유니언 — valid로 분기)
// 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? }
verifyEntitlement 응답
// 키가 조회되면 상태와 무관하게 전체 응답 (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'], … }
reportUsage 응답
{ accepted: true,
  allowed: boolean,          // false = 초과 정책 block → 사용 차단
  usage: { unit, used, limit, remaining },
  overage_policy: 'notify' | 'block',
  over_limit: boolean }
checkUpdate 응답 — 상품 수정 페이지 '업데이트 정보' 카드에서 설정
{ 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 }
currentUser 응답 (판별 유니언 — found로 분기)
// 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)
}
0.4.0은 응답 검증과 OAuth state 검증을 강화했습니다. 업그레이드할 때 expectedState 전달과 오류 처리를 확인하세요. 401은 키·환경·만료·폐기, 403은 권한·요금제, 429는 Retry-After를 확인합니다. 서버 오류를 문의할 때 X-Request-Id를 첨부하고 키·웹훅 원문·개인정보를 로그에 남기지 마세요.

SDK 없이 직접 호출

모든 요청에 X-Saemol-Secret 헤더를 붙여 호출하면 됩니다.

curl
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" }'