CP-Pay Webhook
결제 성공·환불 이벤트를 자기 백엔드로 받습니다.
현재 발송되는 이벤트는 payment.succeeded(결제 성공)와 payment.refunded(환불) 두 가지이며, events 를 생략하면 이 둘이 기본값입니다. 예전 이름 payment.completed 로 등록된 webhook 도 payment.succeeded 를 받습니다. 환불 이벤트는 원결제와 같은 environment(test/live)의 webhook 으로만 갑니다.
등록
cp-platform.com/developers 콘솔에서 webhook URL을 등록합니다. 로컬 개발 중이면 ngrok URL 그대로 OK.
PUT
https://pay.cp-platform.com/developers/apps/{app_id}/webhook{
"url": "https://your-app.com/webhooks/cp-pay",
"events": ["payment.succeeded", "payment.refunded"]
}응답의 secret(서명 검증용)을 보관하세요. 다시 발급하려면 POST .../webhook/regenerate-secret.
HMAC 시그니처 검증
CP-Pay는 webhook 전송 시 X-CP-Webhook-Signature: t=<unix초>,v1=<hex> 헤더를 붙입니다. v1 은 `${t}.${rawBody}` 를 secret 으로 HMAC-SHA256 한 값입니다. 이벤트명은 X-CP-Webhook-Event 헤더와 본문 event 필드에 있습니다.
import crypto from "node:crypto";
export async function POST(req: Request) {
const rawBody = await req.text();
const header = req.headers.get("x-cp-webhook-signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const signature = parts.v1 ?? "";
const expected = crypto
.createHmac("sha256", process.env.CP_WEBHOOK_SECRET!)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
if (signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return new Response("invalid signature", { status: 401 });
}
const event = JSON.parse(rawBody);
switch (event.event) {
case "payment.succeeded":
// 결제 완료 처리
break;
case "payment.refunded":
// 환불 처리 (event.data.original_payment_id = 원결제 id)
break;
}
return Response.json({ ok: true });
}재시도 정책
- 2xx 응답이 아니면 최대 5회 재시도 (exponential backoff: 1m / 5m / 30m / 2h / 12h)
- 5회 실패 후 DLQ(Dead Letter Queue) 로 이동
- 콘솔의 Delivery Log 에서 재시도 이력 확인 가능
GET
https://pay.cp-platform.com/developers/apps/{app_id}/webhook/deliveries