tgpay cryptoAPI
crypto-payapiwebhookssignature

API 레퍼런스: 웹훅

읽는 데 3분마지막 수정 2026년 10월 9일

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

이벤트 형식

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

update_id는 같은 이벤트를 다시 보낼 때도 그대로예요. 중복 제거는 이 값으로 하세요. request_date는 보낼 때마다 새로 찍혀요. payload에는 이벤트 종류에 맞는 객체가 통째로 들어가요. 청구서 이벤트면 청구서 객체, 송금 링크 객체, 송금 객체, 구독 객체 같은 식이에요(각각의 모양은 해당 레퍼런스에 있어요).

이벤트 종류

update_type언제 오나요페이로드
invoice_paid청구서가 결제됐을 때 — 별도 구독 설정이 필요 없어요청구서 객체
invoice_expired결제되지 않은 채 기한이 지났을 때청구서 객체
check_activated송금 링크 전체나 명단의 한 사람이 자기 몫을 받았을 때송금 링크 객체. 명단형 수령에는 activated_slot도 포함돼요
check_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로 구분하세요. 다운로드용 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"])

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

전송과 재시도

  • 시도 제한 시간 안에 2xx 응답을 받으면 성공으로 처리해요. 빠르게 응답하고 오래 걸리는 작업은 별도로 처리하세요.
  • 오류 상태, 시간 초과, 연결 실패가 발생하면 플랫폼의 시도 한도까지 간격을 늘려 재전송해요. 전달을 보장하지는 않으므로 장애 후에는 getInvoices로 결제 내역을 대조하세요.
  • 마지막 시도까지 실패하면 그 전송은 버려요. 웹훅 주소 자체가 자동으로 꺼지지는 않아요. 엔드포인트가 불안정하다고 해서 앱의 이벤트 구독이 자동으로 해제되지는 않아요.
  • 다시 보내는 일이 있으니 핸들러는 같은 이벤트를 여러 번 받아도 중복 처리하지 않아야 해요. 처리하기 전에 update_id로 중복을 걸러 주세요.

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

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