Skip to content

Webhook

결제 상태 변경 시 실시간으로 알림을 받습니다.

Webhook 개요

Webhook을 설정하면 결제 상태가 변경될 때 지정한 URL로 HTTP POST 요청을 받을 수 있습니다.

왜 Webhook을 사용해야 하나요?

  • 실시간 알림: 상태 변경 즉시 알림
  • 서버 리소스 절약: 폴링 불필요
  • 신뢰성: 재시도 메커니즘 내장

이벤트 타입

이벤트status 값설명
payment.paidPAID온체인 결제 확인, 자금이 상점으로 전송됨
payment.invalidINVALID온체인 결제 감지되었으나 검증 실패

payment.paid 수신 시 주문을 완료 처리합니다. payment.invalid 수신 시 해당 주문을 수동 검토 대상으로 표시합니다.

Payload 구조

Webhook payload는 플랫 JSON 객체로 전송됩니다 (래퍼 없음). Content-Type 헤더는 application/json입니다.

json
{
  "paymentId": "0xabc123...",
  "orderId": "order-001",
  "status": "PAID",
  "txHash": "0xdef789...",
  "amount": "10500000000000000000",
  "tokenSymbol": "SUT",
  "paidAt": "2024-01-26T12:35:42.000Z"
}

헤더

헤더설명
Content-Typeapplication/json
X-SoloPay-Signaturet=timestamp,v1=signature (웹훅 시크릿 설정 시 포함)

이벤트별 Payload 상세

payment.paid

온체인 결제가 확인된 상태입니다. 사용자가 결제를 완료했고 자금이 상점으로 직접 전송되었습니다.

json
{
  "paymentId": "0xabc123...",
  "orderId": "order-001",
  "status": "PAID",
  "txHash": "0xdef789...",
  "amount": "10500000000000000000",
  "tokenSymbol": "SUT",
  "paidAt": "2024-01-26T12:35:42.000Z"
}

payment.invalid

온체인에서 결제 트랜잭션이 감지되었으나, 검증에 실패한 상태입니다. 금액, 토큰, 수신자 주소 등이 기대 값과 일치하지 않을 때 발생합니다.

json
{
  "paymentId": "0xabc123...",
  "orderId": "order-001",
  "status": "INVALID",
  "txHash": "0xdef789...",
  "amount": "10500000000000000000",
  "tokenSymbol": "SUT",
  "paidAt": "2024-01-26T12:35:42.000Z"
}

이벤트 핸들러 예시

typescript
async function handleWebhook(payload: any) {
  const { status, orderId, paymentId } = payload;

  switch (status) {
    case 'PAID':
      await completeOrder(orderId);
      break;
    case 'INVALID':
      await flagOrderForReview(orderId);
      break;
  }
}

재시도 정책

Webhook은 최대 3회 재시도되며, 재시도 간격은 10초, 30초, 90초입니다. 엔드포인트가 HTTP 2xx 상태 코드를 반환하면 전송 성공으로 처리됩니다.

HMAC 서명 검증

가맹점에 Webhook 서명 시크릿이 설정되어 있으면, 모든 Webhook 요청에 X-SoloPay-Signature 헤더가 t=timestamp,v1=signature 형식으로 포함됩니다. 페이로드를 처리하기 전에 서명을 검증하여 SoloPay에서 보낸 요청이며 변조되지 않았는지 확인해야 합니다.

동작 원리

헤더 형식은 t=<unix_timestamp>,v1=<hmac_hex>입니다. 서명은 다음과 같이 계산됩니다: HMAC-SHA256(secret, "timestamp.body") 여기서 timestampt 값이고 body는 원본 JSON 요청 본문입니다.

설정 방법

  1. 가맹점 대시보드 > 설정 > Webhook Secret 으로 이동
  2. Generate Secret 클릭하여 새 서명 시크릿 생성 (whsec_...)
  3. 시크릿을 서버에 안전하게 저장

검증 예시

typescript
import { verifyWebhookSignature } from '@solo-pay/gateway-sdk';

// 웹훅 핸들러에서:
const rawBody = await request.text();
const sigHeader = request.headers.get('x-solopay-signature');

const isValid = verifyWebhookSignature(rawBody, sigHeader, process.env.WEBHOOK_SECRET);
if (!isValid) {
  return new Response('Invalid signature', { status: 401 });
}

수동 검증:

typescript
import crypto from 'crypto';

function verifyWebhook(rawBody: string, header: string, secret: string): boolean {
  const parts = header.split(',');
  let ts = '',
    sig = '';
  for (const p of parts) {
    const [k, v] = p.split('=', 2);
    if (k === 't') ts = v;
    else if (k === 'v1') sig = v;
  }

  const age = Math.abs(Math.floor(Date.now() / 1000) - parseInt(ts, 10));
  if (age > 300) return false;

  const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(sig, 'hex'));
}

타이밍 안전 비교 사용

타이밍 공격을 방지하기 위해 === 대신 반드시 crypto.timingSafeEqual()을 사용하세요.

결제 결과 검증

Webhook으로 수신한 이벤트의 결제 정보를 검증하는 방법입니다.

서버 사이드 검증

Webhook payload에 포함된 paymentId를 이용하여 서버에서 SoloPay API를 직접 호출해 결제 상태를 확인합니다.

반드시 서버에서 검증하세요

Webhook payload의 내용을 그대로 신뢰하지 마세요. 반드시 API를 통해 실제 결제 상태를 재확인해야 합니다.

bash
curl https://gateway.dev.solonetwork.io/api/v1/payments/0xabc123... \
  -H "x-public-key: pk_xxxxx"

검증 체크리스트

  • [ ] status === 'PAID' 확인 (결제 성공)
  • [ ] amount자사 주문 DB에 저장된 기대 금액과 일치 확인 (위젯은 클라이언트에서 실행되므로 금액이 변조될 수 있음)
  • [ ] tokenAddress가 기대한 토큰 컨트랙트 주소와 일치 확인
  • [ ] orderId가 DB에 저장된 orderId와 일치 확인
  • [ ] 동일 paymentId의 중복 처리 방지

멱등성 처리

같은 이벤트가 여러 번 전송될 수 있습니다. paymentId를 기준으로 중복 처리를 방지하세요.

typescript
// 이미 처리된 paymentId인지 확인 (DB 조회)
const alreadyProcessed = await db.orders.isPaymentProcessed(data.paymentId);
if (alreadyProcessed) {
  return res.status(200).json({ received: true }); // 중복 이벤트 무시
}

가맹점 Webhook URL 설정

현재 가맹점 데이터 모델에 webhook_url 필드가 존재합니다. 관리자에게 문의하여 설정하세요.

다음 단계

Non-custodial Web3 payment infrastructure for ERC-20 checkout, sponsored gas, and wallet-to-wallet settlement.