tgpay cryptoAPI
crypto-payapiwebhookssignature

API 레퍼런스: 웹훅

읽는 데 3분마지막 수정 2026년 11월 2일

이벤트가 생기면 웹훅으로 내 서버에 알려 줘요. 더보기 → Merchant API에서 앱의 **웹훅 URL (선택)**을 지정해 두면, 구독한 이벤트마다 서명된 JSON 본문을 POST해 드려요.

이벤트 형식

{
  "update_id": 123,
  "update_type": "invoice_paid",
  "request_date": "2026-08-11T12:00:00.123456Z",
  "payload": { … }
}

update_id는 같은 이벤트를 다시 보낼 때도 그대로예요. 중복 제거는 이 값으로 하세요. request_date (UTC, 마이크로초가 포함된 ISO 8601 형식)는 보낼 때마다 새로 찍혀요. payload에는 이벤트 종류에 맞는 객체가 통째로 들어가요. 청구서 이벤트에는 청구서 객체가 들어가고, 다른 이벤트에는 종류에 따라 송금 링크 객체, 송금 객체 또는 구독 객체가 들어가요(각각의 모양은 메서드 페이지에 있어요: 청구서, 송금과 송금 링크, 구독).

이벤트 종류

update_type언제 오나요페이로드
invoice_paid청구서가 결제됐을 때 — 별도 구독 설정이 필요 없어요청구서 객체
invoice_expired결제되지 않은 채 기한이 지났을 때청구서 객체
check_activated송금 링크 전체나 명단의 한 사람이 자기 몫을 받았을 때송금 링크 객체. 명단형 수령에는 activated_slot도 포함돼요
check_expired명단형 송금 링크가 만료되어 미수령 금액이 앱으로 돌아왔을 때status: "expired", recipients와 각 항목의 claimed_at을 포함한 송금 링크 객체
transfer_completed송금이 완료되거나 온체인 지급이 확인됐을 때송금 또는 출금 객체
transfer_returned미수령 송금이 앱으로 반환되거나 온체인 지급이 실패해 보류액이 반환됐을 때송금 또는 출금 객체
refund_completed환불이 나갔을 때refunded_* 필드가 채워진 청구서 객체
subscription_activated사용자가 요금제를 승인했을 때구독 객체
subscription_charged주기가 청구됐을 때 — 어떤 청구인지는 charge.kind에 있어요: initial, renewal, resubscribe구독 객체 + charge: {period_no, kind, asset, amount, fee, paid_at}
subscription_cancelled어느 쪽이든 구독을 취소했을 때구독 객체
subscription_expired유예 기간이 결제 없이 끝났을 때구독 객체

앱에서 만든 온체인 지급도 같은 송금 이벤트를 사용해요. 페이로드에는 kind: "withdrawal", network, address, txid 같은 체인 필드와 completed 또는 failed 상태가 있어요. transfer_id는 별도의 ID 순번을 사용해요. kind는 이 형식에만 있어요. 일반 송금의 페이로드에는 kind 필드가 아예 없으니, kind: "withdrawal"이 있으면 온체인 지급으로, kind가 없으면 송금으로 처리하세요. 다운로드용 OpenAPI 레퍼런스는 두 페이로드 형식을 설명해요.

invoice_paid 말고는 전부 원할 때만 켜는 방식이에요(Crypto Bot에는 없는 확장 기능이라, Crypto Bot 규격만 아는 클라이언트는 직접 켜지 않는 한 모르는 update_type을 만날 일이 없어요). 앱마다 Merchant API 화면의 추가 웹훅 이벤트에서 켜면 돼요. 웹훅 섹션의 스위치에는 위 표의 식별자가 표시돼요. 이벤트를 받으려면 웹훅 URL을 저장하세요.

서명 확인하기

전송마다 TgPayCrypto-API-Signature 헤더가 붙어요(TgCryptoPay-API-Signature와 Crypto-Pay-API-Signature도 값이 같은 호환용 별칭이에요). 원본 요청 본문을 HMAC-SHA256으로 서명한 16진수 값이고, 키는 앱의 기본 API 토큰을 SHA-256으로 해시한 값이에요. Crypto Bot과 같은 방식이라 쓰던 검증 코드를 그대로 써도 돼요.

import hashlib, hmac

secret = hashlib.sha256(API_TOKEN.encode()).digest()
expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, headers["TgPayCrypto-API-Signature"])

받은 원본 바이트로 확인해야 해요. 다시 직렬화한 값은 바이트가 달라질 수 있어서 검증이 실패해요. 서명하는 건 기본 토큰뿐이고, 권한 제한 토큰은 서명하지 않아요. 기본 토큰을 새로 발급하면 웹훅 서명 키도 그 순간 바뀌니까, 서버의 시크릿도 같이 바꿔 주세요.

전송과 재시도

  • 10초 안에 2xx 응답을 받으면 성공으로 처리해요. 빠르게 응답하고 오래 걸리는 작업은 별도로 처리하세요. 리디렉션은 따라가지 않아요. 3xx는 실패로 처리하니 최종 URL을 등록하세요.
  • 오류 상태, 시간 초과, 연결 실패가 발생하면 다시 보내요. 최대 17번이고, 간격은 10초부터 시작해 8시간까지 두 배씩 늘어나며(약간의 무작위 편차 포함) 모두 합쳐 약 3일이에요. 전달을 보장하지는 않으므로 장애 후에는 getInvoices로 결제 내역을 대조하세요.
  • 각 시도는 그 시점에 저장된 웹훅 URL로 가요. URL을 바꾸면 대기 중인 재전송이 새 URL로 가고, URL을 지우면 재전송이 취소돼요.
  • 마지막 시도까지 실패하면 그 전송은 버려요. 웹훅 주소 자체가 자동으로 꺼지지는 않아요. 엔드포인트가 불안정하다고 해서 앱의 이벤트 구독이 자동으로 해제되지는 않아요.
  • 다시 보내는 일이 있으니 핸들러는 같은 이벤트를 여러 번 받아도 중복 처리하지 않아야 해요. 처리하기 전에 update_id로 중복을 걸러 주세요.

⚠️ 처리하기 전에 확인하세요

내 웹훅 주소로는 누구나 POST를 보낼 수 있어요. 서명 확인이 통과하기 전까지는 본문을 믿지 마세요. 주문을 보내지도, 사용자 잔액에 반영하지도, 결제됨으로 표시하지도 마세요. 안전한 순서는 서명 확인 → update_id로 중복 제거 → 처리예요.