Skip to Content
개발자SDKJavaScript

JavaScript SDK

서버 클라이언트

Payri 서버 클라이언트를 만들고 유예 키 알림을 설정해요.

import { Payri } from '@payri/sdk' const payri = new Payri({ apiKey: process.env.PAYRI_API_KEY!, onKeyDeprecated: (expiresAt) => { console.warn('API 키 교체 만료 시각:', expiresAt) }, })

기본 baseUrl은 https://api.payri.kr이며, 별도 지정 시 호스트만 넣고 /v1을 붙이지 않으며 앱 라이선스 API를 제외한 POST는 Idempotency-Key를 자동 생성하고 GET과 멱등 키가 있는 요청만 재시도해요.
재시도는 같은 키를 쓰고 새 호출에서 재사용하려면 마지막 options 인자의 idempotencyKey를 지정해요; onKeyDeprecated를 지정하면 Payri-Key-Deprecated 헤더의 ISO 8601 만료 시각을 expiresAt으로 클라이언트당 한 번 전달하고 기본 경고는 출력하지 않아요.

API 키는 서버 환경변수로 보관하고 브라우저 코드에 넣지 마세요.

결제 세션

checkout.sessions.create()로 세션을 만들고 구매자를 이동시켜요 (체크아웃 REST API).

const session = await payri.checkout.sessions.create({ productId: 'prod_123', planId: 'plan_monthly', interval: 'monthly', successUrl: 'https://shop.example.com/thanks', cancelUrl: 'https://shop.example.com/pricing', }) return Response.redirect(session.url)

주문 조회와 환불

주문을 조회하고 환불을 요청해요 (주문 REST API, 환불 REST API).

const order = await payri.orders.retrieve('ord_123') if (order.status === 'paid') { const refunded = await payri.orders.refund(order.id, { amount: 5000, reason: '고객 요청', }) console.log(refunded.status) }

구독 조회와 해지

구독을 조회하고 해지해요 (구독 REST API).

const subscription = await payri.subscriptions.retrieve('sub_123') const result = await payri.subscriptions.cancel(subscription.id, { timing: 'period_end', reason: '고객 요청', }) console.log(result.subscription.status, result.timing, result.refundAmount)

환불 요청 처리

환불 요청을 조회하고 승인·거절·보류해요 (환불 요청 REST API).

const requests = await payri.refundRequests.list({ page: 1, limit: 20 }) const requestId = requests.data[0]?.id const decision: 'approve' | 'reject' | 'hold' = 'approve' if (requestId) { if (decision === 'approve') { await payri.refundRequests.approve(requestId, { amount: 5000 }) } else if (decision === 'reject') { await payri.refundRequests.reject(requestId, { reason: '환불 조건을 충족하지 않아요.' }) } else { await payri.refundRequests.hold(requestId, { type: 'in_review', reason: '증빙을 확인하고 있어요.', }) } }

구매자 포털 세션

포털 세션을 만들고 구매자를 이동시켜요 (포털 세션 REST API).

const portal = await payri.portalSessions.create({ subscriptionId: 'sub_123', returnUrl: 'https://shop.example.com/account', }) return Response.redirect(portal.url)

서버에서 라이선스 조회

서버에서 라이선스를 조회해요 (라이선스 REST API).

const licenses = await payri.licenses.list({ productId: 'prod_123', status: 'active', page: 1, limit: 20, }) const firstLicense = licenses.data[0] if (firstLicense) { const license = await payri.licenses.retrieve(firstLicense.id) console.log(license.buyerEmail, license.status, license.mode) }

웹훅 서명 검증과 이벤트 분기

JSON 파싱 전 원본 본문을 Webhooks.constructEvent()에 전달해 서명을 검증하고, event.type에 따라 data 타입을 좁혀요 (서명 문서, 이벤트 레퍼런스).

import express from 'express' import { PayriWebhookError, Webhooks } from '@payri/sdk' const app = express() app.post('/webhooks/payri', express.raw({ type: 'application/json' }), async (req, res) => { let event try { event = await Webhooks.constructEvent({ payload: req.body, headers: req.headers, secret: process.env.PAYRI_WEBHOOK_SECRET!, }) } catch (error) { if (error instanceof PayriWebhookError) return res.sendStatus(400) throw error } if (event.type === 'order.paid') { switch (event.data.subscriptionEvent) { case 'renewal': await recordRenewal(event.data) break case 'proration': await recordProration(event.data) break case 'initial': case null: await fulfillOrder(event.data) break } } res.sendStatus(200) })

license.issue 처리

Webhooks.handle()은 서명과 license.issue 응답 형식을 검증해요 (라이선스 발급 웹훅).

import express from 'express' import { Webhooks } from '@payri/sdk' const app = express() app.post('/webhooks/payri', express.raw({ type: 'application/json' }), async (req, res) => { const result = await Webhooks.handle({ payload: req.body, headers: req.headers, secret: process.env.PAYRI_WEBHOOK_SECRET!, onEvent: async (event) => processEvent(event), onLicenseIssue: async (event) => licenseStore.getOrCreate(event.id, () => issueAndSaveKey(event.data)), }) res.status(result.status).set(result.headers).send(result.body) })

라이선스 앱 클라이언트

PayriLicensing은 앱 라이선스를 확인·활성화하며 요청마다 32바이트 nonce를 hex 64자로 보내고, 응답 서명·nonce 일치·5분 이내 issuedAt을 검증해요.

메서드인자
verify({ key, activationId? })activationId는 선택이에요.
activate({ key, label? })label은 선택이에요.
deactivate({ key, activationId })activationId를 전달해요.

라이선스 앱 API의 429는 PayriRateLimitError로 전달되며 code와 retryAfterSeconds에서 Retry-After 초를 확인해요 (라이선스 앱 API, 응답 서명).

publicKeys 옵션은 내장 공개 키 목록을 확장하고 같은 kid가 있으면 옵션 값으로 덮어쓰며, 목록에 없는 kid는 검증 오류를 내고 signing-keys를 자동 조회하지 않아요.

키 교체

키 교체 절차는 라이선스 앱 API를 참고해요.

import { PayriLicensing } from '@payri/sdk' const licensing = new PayriLicensing({ productId: 'prod_123', publicKeys: { '<현재 kid>': '<base64 현재 공개 키>', '<다음 kid>': '<base64 다음 공개 키>', }, }) const verified = await licensing.verify({ key: licenseKey }) const activated = await licensing.activate({ key: licenseKey, label: '내 노트북', }) const activationId = activated.activation?.id if (activationId) { await saveActivationId(activationId) await licensing.deactivate({ key: licenseKey, activationId }) }

오류 처리

오류 타입은 PayriApiError, PayriRateLimitError, PayriWebhookError, PayriLicenseSignatureError로 나뉘며, 서명 오류가 나면 검증 결과를 신뢰하지 않아요 (API 오류 코드).

import { PayriApiError, PayriAuthError, PayriRateLimitError } from '@payri/sdk' try { await payri.orders.retrieve('ord_123') } catch (error) { if (error instanceof PayriAuthError && error.code === 'API_KEY_SCOPE_MISSING') { const details = error.details as { required: string[] } console.error('필요한 권한:', details.required) } else if (error instanceof PayriAuthError && error.code === 'API_KEY_INVALID') { console.error('API 키를 확인해 주세요.') } else if (error instanceof PayriAuthError && error.code === 'API_KEY_IP_NOT_ALLOWED') { console.error('요청 IP 제한을 확인해 주세요.') } else if (error instanceof PayriRateLimitError && error.code === 'RATE_LIMITED') { console.error('재시도 대기 시간(초):', error.retryAfterSeconds) } else if (error instanceof PayriApiError && error.code === 'VALIDATION_FAILED') { const details = error.details as { fields: Array<{ field: string; message: string }> } console.error('입력 오류:', details.fields) } else if (error instanceof PayriApiError && error.code === 'IDEMPOTENCY_KEY_CONFLICT') { console.error('같은 멱등 키에 다른 본문을 보냈어요.') } else if (error instanceof PayriApiError && error.code === 'IDEMPOTENCY_IN_PROGRESS') { console.error('같은 멱등 키 요청이 처리 중이에요.') } else if (error instanceof PayriApiError) { console.error(error.status, error.code, error.details) } else { throw error } }

다음 단계

웹훅 이벤트

이벤트 이름과 페이로드 타입을 확인해요.

라이선스 앱 API

앱에서 서명된 라이선스 응답을 검증해요.

REST API

SDK가 호출하는 각 엔드포인트를 확인해요.

Last updated on