내 서버에서 발급
셀러 서버가 자체 키를 발급하면 license.issue 웹훅으로 결제 직후 동기 요청을 받아요.
엔드포인트 등록 범위와 충돌 응답은 웹훅 개요를 참고해요.
엔드포인트를 등록하지 않으면 라이선스가 pending으로 남고 셀러 콘솔에 경고가 표시돼요.
동작 흐름
요청 본문
판매자가 발급한 라이선스 키가 필요한 상품의 결제 뒤 동기 발급을 요청해요.
응답 처리가 5회 모두 실패하면 라이선스 상태를 failed로 바꿔요.
본문과 data의 mode는 test 또는 live예요.
서명 헤더와 본문 형식은 웹훅에서 확인해요.
data 필드
buyerobject라이선스 구매자 정보예요.
하위 필드 2개
emailstring구매자 이메일 주소예요.
namestring구매자 이름이에요.
expiresAtstringnull 가능라이선스 만료 시각이에요.
intervalstringnull 가능구독 라이선스의 결제 주기예요.
monthly, yearly, nulllicenseIdstring발급 대기 중인 라이선스 식별자예요.
maxActivationsnumbernull 가능라이선스의 최대 활성화 수예요. 무제한이면 null이에요.
modestring이벤트의 결제 환경이에요.
live, testorderIdstring라이선스 발급을 요청한 주문 식별자예요.
planIdstringnull 가능구독 라이선스의 플랜 식별자예요.
productIdstring라이선스 발급 대상 상품 식별자예요.
요청 헤더와 원문 본문 서명 규칙은 웹훅 서명 검증을 참고해요.
발급 응답
서버는 2xx와 JSON 응답으로 발급한 키를 돌려줘요.
{
"key": "seller-license-key-123",
"expiresAt": "2027-10-15T03:20:05.000Z",
"maxActivations": 3
}필드
keystring필수발급한 라이선스 키예요. 1자 이상 256자 이하 문자열이어야 해요. 요청 data에 key를 더해 그대로 돌려줘도 돼요. key·expiresAt·maxActivations 외 필드는 무시해요.
VENDOR-12345-ABCDEexpiresAtstring발급한 키의 만료 시각이에요. 생략하거나 null이면 요청 data.expiresAt을 그대로 써요. 구독 라이선스는 갱신 때 주기 말로 다시 연장돼요.
2027-10-15T00:00:00.000ZmaxActivationsinteger발급한 키의 최대 활성화 수예요. null이면 무제한이고, 생략하면 요청 data.maxActivations(상품 설정)를 그대로 써요. 0도 무제한으로 받지만 null을 권장해요.
nullexpiresAt·maxActivations를 생략하면 요청 data의 값(상품 설정)을 그대로 써요.
maxActivations: null은 무제한이고, expiresAt: null은 생략과 같아요.
| 실패 | 다시 요청하는 때 | failed가 되는 때 |
|---|---|---|
| 2xx가 아니거나 10초 안에 응답이 없거나 네트워크 오류가 나거나 응답 본문이 64KB를 넘음 | 웹훅 재시도 일정대로 최대 8회 | 8회가 모두 실패할 때 |
2xx지만 key·expiresAt·maxActivations 검증에 실패 | 1분, 5분, 15분, 60분 뒤 | 발급 시도가 누적 5회가 될 때 |
멱등 처리
같은 이벤트 ID인 webhook-id로 다시 요청되면 처음 발급한 키를 그대로 응답해요.
키를 발급하기 전에 서명을 검증하고, 같은 이벤트 ID가 동시에 와도 키를 한 번만 발급하도록 저장소에서 원자적으로 처리해요.
Node.js 예제
import express from 'express'
import { Webhook } from 'standardwebhooks'
const app = express()
const webhook = new Webhook(process.env.PAYRI_WEBHOOK_SECRET) // 외부 연동에서 발급한 whsec_...
app.post('/payri/license-issue', express.raw({ type: 'application/json' }), async (req, res) => {
let event
try {
event = webhook.verify(req.body, {
'webhook-id': req.get('webhook-id'),
'webhook-timestamp': req.get('webhook-timestamp'),
'webhook-signature': req.get('webhook-signature')
})
} catch {
return res.sendStatus(400)
}
if (event.type !== 'license.issue') return res.sendStatus(400)
try {
const issued = await keyStore.getOrCreate(
event.id,
() => issueLicense(event.data)
)
return res.status(200).json({
key: issued.key,
...(issued.expiresAt ? { expiresAt: issued.expiresAt } : {}),
...(issued.maxActivations !== undefined ? { maxActivations: issued.maxActivations } : {})
})
} catch {
return res.sendStatus(500)
}
})