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.

PUThttps://pay.cp-platform.com/developers/apps/{app_id}/webhook
인증
사용자 토큰
요청 body
{
  "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 필드에 있습니다.

your-app/webhooks/cp-pay.ts
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 에서 재시도 이력 확인 가능
GEThttps://pay.cp-platform.com/developers/apps/{app_id}/webhook/deliveries
인증
사용자 토큰