JavaScript/TypeScript SDK
@payri/sdk는 의존성이 없고 Node.js 20+, 브라우저, 엣지 런타임에서 동작해요.
NestJS를 쓴다면 모듈·가드를 제공하는 @payri/nest를 함께 설치해요.
npm install @payri/sdk전체 실행 가능한 예제(Express·Next.js·NestJS)는 sdk-js 저장소의 examples
서버에서 시작하기
서버에서는 Payri 클라이언트에 secret 키를 넣어 사용해요.
import { Payri } from '@payri/sdk'
const payri = new Payri({ apiKey: process.env.PAYRI_API_KEY! })
const products = await payri.products.list({ page: 1, limit: 20 })
const result = await payri.licenses.verify({ key: 'XXXX-YYYY-ZZZZ' })
// 체크아웃 세션 — 반환된 url로 구매자를 리다이렉트해요.
const session = await payri.checkout.create({ linkId: 'link_abc' })API 키는 서버 전용 비밀키예요.
브라우저·모바일 앱 번들에 포함하거나 NEXT_PUBLIC_ 같은 공개 환경변수에 넣으면 안 돼요.
브라우저에서는 키가 필요 없는 PayriPublic을 쓰세요.
생성자 옵션
| 옵션 | 기본값 | 설명 |
|---|---|---|
apiKey | (필수) | 서버 전용 secret 키 (key_live_* / key_test_*) |
baseUrl | https://api.payri.kr | API base URL. 스테이징 등 다른 환경을 겨눌 때만 변경해요 |
timeoutMs | 30000 | 요청 타임아웃(ms) |
maxRetries | 0 | 429/5xx 응답 시 재시도 횟수 |
fetch | 전역 fetch | 커스텀 fetch 구현 (테스트·엣지 런타임용) |
리소스
| 리소스 | 메서드 | REST 문서 |
|---|---|---|
payri.products | list() · get(id) | Products |
payri.orders | list() · get(id) · refund(id, params) | Orders · Refunds |
payri.subscriptions | list() · get(id) · cancel(id, params) | Subscriptions |
payri.checkout | create(params) · get(id) | Checkout |
payri.licenses | verify(params) · activate(params) · deactivate(params) | Licenses |
브라우저에서 사용하기
PayriPublic은 API 키를 받지 않아서 브라우저(React/Next 클라이언트 컴포넌트 포함)에서 안전하게 쓸 수 있어요.
구매자가 이름·이메일 본인확인으로 자기 주문의 라이선스를 조회하는 용도예요.
import { PayriPublic } from '@payri/sdk'
const pub = new PayriPublic()
const license = await pub.lookupLicense('ord_123', {
name: '홍길동',
email: 'hong@example.com',
})
if (!license) {
// 주문이 없거나 이름·이메일이 일치하지 않는 경우 — 존재 여부는 알려주지 않아요.
}일치하지 않으면 에러 대신 null을 반환해요.
주문 존재 여부를 노출하지 않기 위한 동작이라, 실패 사유를 구분해 보여줄 수 없어요.
웹훅 검증
Webhooks.constructEvent가 Standard Webhooks { type, data } 이벤트를 반환해요.
검증에는 원본(raw) 본문이 필요해요 — 프레임워크가 JSON으로 파싱한 객체를 다시 직렬화해 넘기면 서명이 깨져요.
검증 실패 시 PayriWebhookError를 던지고, reason으로 사유를 확인할 수 있어요.
Express
express.json()보다 먼저 해당 라우트만 express.raw()로 받아요.
import express from 'express'
import { Webhooks, PayriWebhookError } from '@payri/sdk'
const app = express()
// 서명 검증엔 원본 바디가 필요해요 — express.json()보다 먼저 raw로 받아요.
app.post('/webhooks/payri', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const event = await Webhooks.constructEvent({
payload: req.body,
headers: req.headers,
secret: process.env.PAYRI_WEBHOOK_SECRET!,
})
// TODO: webhook-id로 멱등 확인 후 event.type에 따라 처리
res.sendStatus(200)
} catch (err) {
if (err instanceof PayriWebhookError) {
return res.status(400).json({ error: err.reason })
}
throw err
}
})
app.use(express.json())Next.js (App Router)
Route Handler에서 request.text()로 원본 본문을 읽어요.
// app/api/webhooks/payri/route.ts
import { NextResponse } from 'next/server'
import { Webhooks, PayriWebhookError } from '@payri/sdk'
export async function POST(request: Request) {
// request.json()으로 파싱하면 서명이 깨져요 — text()로 원본을 읽어요.
const payload = await request.text()
try {
const event = await Webhooks.constructEvent({
payload,
headers: Object.fromEntries(request.headers),
secret: process.env.PAYRI_WEBHOOK_SECRET!,
})
return NextResponse.json({ received: true })
} catch (err) {
if (err instanceof PayriWebhookError) {
return NextResponse.json({ error: err.reason }, { status: 400 })
}
throw err
}
}이벤트 종류와 페이로드 형식은 이벤트 종류에서 확인해요.
NestJS: @payri/nest
npm install @payri/nest @payri/sdk웹훅 가드는 원본 바디가 필요해요.
반드시 NestFactory.create(AppModule, { rawBody: true })로 앱을 생성하세요.
모듈 등록
import { Module } from '@nestjs/common'
import { PayriModule } from '@payri/nest'
@Module({
imports: [
PayriModule.forRoot({
apiKey: process.env.PAYRI_API_KEY!,
webhookSecret: process.env.PAYRI_WEBHOOK_SECRET,
}),
],
})
export class AppModule {}기본으로 전역 모듈로 등록돼요 (global: false로 끌 수 있어요).
ConfigService 등에서 옵션을 읽어야 하면 forRootAsync({ useFactory, inject })를 사용해요.
PayriService 주입
import { Injectable } from '@nestjs/common'
import { PayriService } from '@payri/nest'
@Injectable()
export class StoreService {
constructor(private readonly payri: PayriService) {}
listProducts() {
return this.payri.products.list()
}
}payri.products처럼 리소스를 바로 쓸 수 있고, 전체 클라이언트가 필요하면 payri.client로 접근해요.
웹훅 가드
PayriWebhookGuard가 서명을 검증하고, 검증된 이벤트를 @PayriEvent()로 핸들러에 주입해요.
검증에 실패하면 401을 반환해요.
import { Controller, Post, HttpCode, UseGuards } from '@nestjs/common'
import { PayriWebhookGuard, PayriEvent } from '@payri/nest'
import type { WebhookEvent } from '@payri/nest'
@Controller('webhooks')
export class WebhookController {
@Post('payri')
@HttpCode(200)
@UseGuards(PayriWebhookGuard)
handle(@PayriEvent() event: WebhookEvent) {
// TODO: webhook-id로 멱등 확인 후 event.type에 따라 처리
return { received: true }
}
}에러 처리
모든 에러는 PayriError를 상속해요. (에러 계층)
import { PayriApiError, PayriAuthError, PayriConnectionError } from '@payri/sdk'
try {
await payri.orders.get('ord_123')
} catch (err) {
if (err instanceof PayriAuthError) {
// 401/403 — 키 누락·무효, 스코프 부족, IP 제한
} else if (err instanceof PayriApiError) {
// 그 외 4xx/5xx — err.status, err.code, err.body 확인
} else if (err instanceof PayriConnectionError) {
// 네트워크 실패·타임아웃 — 응답을 받지 못했어요
}
}PayriAuthError는 PayriApiError를 상속하므로 순서대로 검사해요.
에러 코드 목록은 에러 코드에서 확인해요.