webhook으로 주문 결제를 자동 확인하는 방법
인보이스를 주문에 연결하고 웹훅 서명을 검증하여 중복 이벤트, 부족한 결제 및 되돌림으로 인한 잘못된 제공을 방지합니다.
수정일:
사이트가 주문을 자동으로 결제 완료 처리하려면 Cryptanio에서 주문용 인보이스를 만들고 webhook을 연결합니다. webhook은 결제 상태가 바뀔 때 서비스가 내 서버로 보내는 알림입니다. 서버가 메시지의 진위를 검증하고 주문을 찾은 뒤 인보이스 상태를 대조하면, 구매자에게 유료 이용 권한을 부여하거나 주문을 이행 단계로 넘길 수 있습니다.
달러를 계산 단위로 인보이스를 만들고 구매자가 결제 페이지에서 사용 가능한 암호화폐와 네트워크를 선택하는 상점을 예로 설명합니다. 자금은 상점의 주소로 들어옵니다. 주문과 상품 제공은 상점 애플리케이션에서 관리하며, Cryptanio는 인보이스를 송금과 연결하고 상태를 알려 줍니다. 이 연동에는 개발자 또는 사이트의 서버 코드를 수정할 수 있는 환경이 필요합니다.
결제용 프로젝트와 주소 준비하기
Cryptanio 대시보드에서 프로젝트를 선택하고 본인 소유의 수신 주소를 추가합니다. 주소를 Payments에 연결하세요. 단순 주소 모니터링만으로는 인보이스를 발급할 수 없습니다. 주소가 활성화되어 있고 선택한 네트워크에 맞는지 확인합니다. 실제 결제에는 운영 모드의 주소가 필요합니다.
여러 고객이 동시에 결제할 수 있다면 주소도 여러 개 준비하세요. 열린 인보이스가 주소를 점유하거나 인보이스 종료 후 한동안 주소를 사용할 수 없을 수 있습니다. 사용 가능한 주소가 없을 때는 이해하기 쉬운 오류를 표시하고 나중에 다시 시도할 수 있어야 합니다.
API 키를 만들어 상점 서버에 저장합니다. https://api.cryptanio.com/v1 호출에 필요합니다. 키를 페이지 JavaScript, 모바일 앱, 공개 저장소에 넣어서는 안 됩니다.
알림 수신 주소 설정하기
선택한 프로젝트의 「알림」을 엽니다. 「계정 알림 설정 사용」이 켜져 있다면 「계정 설정」에서 Webhook을 설정하거나 상속을 끄고 프로젝트 자체 수신 주소를 지정합니다.
Webhook 카드의 「엔드포인트 URL」에 처리기의 HTTPS 주소를 넣고 「저장」을 누릅니다. 처리기는 POST 요청을 직접 받아야 하며 다른 URL로 리다이렉트하는 방식은 사용하지 않습니다. 같은 카드에서 「서명 비밀 키」를 받아 서버에서 API 키와 별도로 보관하세요.
Webhook이 일시 중지되지 않았는지 확인합니다. 처리기에서 payment.confirmed와 payment.rolled_back을 처리하고, 필요하면 payment.pending을 중간 상태 표시에 사용합니다. 주문 이행 전에 실제 모드와 아래의 나머지 조건을 확인하세요. 인보이스 주소에 별도의 Webhook 채널을 켤 필요는 없습니다.
주문별 인보이스 만들기
먼저 상점 데이터베이스에 주문을 저장합니다. 그다음 서버가 POST /v1/invoices로 인보이스를 만듭니다. reference에 내 주문 식별자를 전달하세요. 이 예시의 주문 가격은 12.50 USD입니다. invoice_currency와 invoice_amount가 가격을 지정하고, 구매자가 결제 페이지에서 사용 가능한 네트워크와 자산을 선택합니다. 따라서 이 요청에는 chain, asset, amount, address, tolerance를 보내지 않습니다. USD 표시는 법정화폐를 받거나 수신한 암호화폐를 달러로 환전한다는 뜻이 아닙니다.
#!/usr/bin/env bash
set -euo pipefail
: "${CRYPTANIO_API_KEY:?Set CRYPTANIO_API_KEY}"
: "${CRYPTANIO_OPERATION_ID:?Set CRYPTANIO_OPERATION_ID}"
: "${CRYPTANIO_PROJECT_ID:?Set CRYPTANIO_PROJECT_ID}"
json_string() {
local text="$1" char code index output='"'
for ((index=0; index<${#text}; index++)); do
char="${text:index:1}"
case "$char" in
'"'|'\') output+="\\$char" ;;
*) printf -v code '%d' "'$char"
if ((code < 32)); then printf -v char '\\u%04x' "$code"; fi
output+="$char" ;;
esac
done
printf '%s"' "$output"
}
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
transfer_status=0
http_status="$(curl \
--silent --show-error --max-time 30 --request POST \
--header "Authorization: Bearer $CRYPTANIO_API_KEY" \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header "Idempotency-Key: $CRYPTANIO_OPERATION_ID" \
--data-binary '{"project":'"$(json_string "$CRYPTANIO_PROJECT_ID")"',"invoice_currency":"USD","invoice_amount":"12.50","reference":"order-42"}' \
"https://api.cryptanio.com/v1/invoices" \
--output "$response_file" --write-out '%{http_code}')" || transfer_status=$?
if [[ "$http_status" == 2?? ]]; then
cat "$response_file"
exit "$transfer_status"
fi
printf 'HTTP %s\n' "$http_status" >&2
cat "$response_file" >&2
exit 1
실행하기 전에 서버에 CRYPTANIO_API_KEY, CRYPTANIO_PROJECT_ID, CRYPTANIO_OPERATION_ID를 설정합니다. 인보이스 생성 권한이 있는 운영용 API 키를 사용하세요. CRYPTANIO_PROJECT_ID는 준비한 Payments 프로젝트를 가리켜야 합니다. CRYPTANIO_OPERATION_ID는 해당 주문의 인보이스 생성 작업을 위해 저장한 식별자로, 영문자·숫자·하이픈·밑줄로 구성된 16~80자입니다. 예시는 이를 JSON 필드가 아닌 Idempotency-Key 헤더로 전달합니다. 새 주문에는 다른 값을 만들고, 같은 요청을 재시도할 때는 기존 값을 유지하세요.
생성에 성공하면 API가 HTTP 201을 반환합니다. 인보이스 id와 반환된 checkout_url을 주문과 함께 저장하고 구매자를 그 링크로 안내합니다. 결제 수단을 선택하기 전에는 네트워크, 주소, 암호화폐 금액이 아직 정해지지 않습니다. 구매자는 결제 페이지에서 사용 가능한 수단을 선택하고 현재 유효한 송금 정보와 기한을 받습니다. 자체 화면에 송금 정보를 표시한다면 해당 결제 시도의 데이터, amount의 정확한 금액, expires_at 기한을 사용하세요. 금액을 반올림하거나 asked 값으로 바꾸지 마세요. 이후 GET 요청과 인보이스 목록에서 얻는 링크는 조회용입니다. 생성 응답의 상호작용 가능한 결제 링크는 별도로 보관하고 해당 구매자에게만 전달하세요.
요청 전송 후 연결이 끊겼다면 같은 본문을 같은 Idempotency-Key로 다시 보냅니다. 성공적인 재시도는 HTTP 201과 동일한 id를 반환합니다. 이미 사용한 키로 요청 매개변수를 바꾸면 HTTP 409가 발생합니다. reference는 인보이스와 주문을 연결하지만 생성을 한 번만 수행하기 위한 키를 대신하지는 않습니다. 단순한 시간 초과 때문에 키를 바꾸지 마세요.
수신 webhook의 진위 검증하기
Cryptanio는 X-Timestamp, X-Signature, X-Event-Id, X-Event-Type, X-Attempt를 전달합니다. 서명 계산식은 다음과 같습니다.
hex(HMAC-SHA256(secret, timestamp + "." + raw_body))
raw_body는 요청의 원본 바이트입니다. JSON을 파싱하기 전에 확보하세요. 다시 직렬화하면 공백이나 필드 순서가 달라져 검증에 실패할 수 있습니다. 본문 크기를 제한하고 유효한 서명이 없는 요청은 거부합니다.
아래 예시는 유효 시간 범위를 5분으로 정합니다. 이는 내 처리기의 설정이며 정확한 검증을 위해 서버 시계가 동기화되어 있어야 합니다.
import hashlib
import hmac
import re
import time
def verify_signature(secret, timestamp, raw_body, signature):
if not secret or not timestamp or not re.fullmatch(r"[0-9]{1,12}", timestamp):
return False
if not signature or not re.fullmatch(r"[0-9a-f]{64}", signature):
return False
if abs(time.time() - int(timestamp)) > 300:
return False
signed = timestamp.encode("ascii") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
이 함수는 서명과 요청 시각을 검증합니다. 검증 성공 후 JSON을 파싱하고 schema_version="v1", 예상한 project_id, mode="live", sandbox=false인지 확인하세요.
판단에는 서명된 본문의 event_id와 event_type을 사용합니다. 같은 이름의 헤더와 대조할 수는 있지만 그 헤더 자체는 서명으로 보호되지 않습니다. 예상한 payment 객체이며 방향이 in인 경우에만 결제 로직을 실행하세요.
송금 확정과 주문 결제 완료 구분하기
상점에서 다룰 주요 이벤트는 다음과 같습니다.
payment.pending: 송금이 감지되었으나 아직 확정을 기다립니다.payment.confirmed: 필요한 확정 수에 도달했습니다.payment.rolled_back: 이전에 감지한 송금이 블록체인 변경으로 취소되었습니다.
payment.confirmed만으로 특정 주문이 전액 결제되었다고 판단해서는 안 됩니다. invoice 객체를 확인하고 invoice.id와 invoice.reference를 저장된 주문과 대조하세요. 인보이스가 연결되지 않은 메시지는 금액이 비슷하다는 이유만으로 구매에 자동 배정할 수 없습니다.
상품 제공 전에는 GET /v1/invoices/{id}로 현재 상태를 조회합니다. id, project_id, reference, invoice_currency와 invoice_amount의 주문 가격, sandbox=false를 확인하세요. 네트워크, 자산, 주소와 선택된 결제 시도도 확인합니다. 시도가 여러 개라면 settled_attempt_id와 settled_attempt를 고려하세요. 결제 성공 상태는 인보이스의 paid이며, pending, seen, partial에서는 아직 제공하면 안 됩니다. 구매자가 사이트로 돌아오거나 결제 링크를 열었다는 사실은 결제를 확인하지 않습니다.
부족 결제는 underpaid, 추가 입금을 기다리는 부분 결제는 partial, 늦은 입금은 late입니다. 초과 결제 시 프로젝트 정책에 따라 인보이스가 paid로 유지되거나 needs_decision=true인 overpaid가 될 수 있습니다. underpaid, overpaid, late에서는 주문을 제공하기 전에 별도의 판단이 필요합니다. amount_raw와 paid 필드는 문자열로 전달됩니다. 비교할 때는 자산 최소 단위의 정확한 정수 값으로 파싱해야 하며, decimals가 배율을 지정합니다. 이진 부동소수점은 사용하지 마세요. 차액 처리 규칙을 미리 정해야 하며, 이것이 자동 환불을 의미하지는 않습니다.
중복과 롤백 처리하기
이벤트는 반복 전달될 수 있습니다. 검증한 이벤트를 영속 저장소에 저장하고 event_id를 고유 키로 사용합니다. 이미 받은 이벤트의 재전달에는 상품을 다시 제공하지 않고 성공 응답을 반환해야 합니다.
거래는 여러 상태를 거치므로 거래 식별자 하나만으로 이후 메시지를 모두 제거해서는 안 됩니다. chain, tx_id, index, address, 자산 식별 정보의 조합마다 마지막으로 적용한 payment.revision을 저장합니다. 이미 처리한 값보다 revision이 큰 경우에만 상태 전이를 적용하세요.
주문 변경과 상품 제공 작업 등록은 동시 처리에 대한 보호를 두고 일관되게 저장합니다. 작업에는 주문의 고유 키가 있어야 합니다. 실행 직전에 그 주문이 여전히 제공 가능한 상태인지 다시 확인하세요. 롤백이 오면 상태를 갱신하고 미실행 제공 작업을 중단합니다. 이미 부여한 접근 권한은 재검토하거나 일시 중지하는 절차를 마련하세요.
전체 흐름 확인하기
실제 결제 전에 준비한 데이터로 본문 파싱, 서명, 중복 방지를 확인합니다. 서비스 이벤트 webhook.test는 구매를 확인하지 않으며 전체 흐름 검증을 대신하지 않습니다.
그다음 본인의 확인용 주문으로 인보이스 생성, 송금, 확정, 주문 갱신까지 전체 과정을 점검합니다. 준비한 데이터로 이벤트 중복, 부족 결제, 롤백도 별도 확인하세요. HTTP 2xx는 메시지를 안전하게 저장한 후 반환합니다. 일시적으로 저장할 수 없다면 서버 오류를 반환해 재전달될 수 있게 합니다.
대시보드의 이벤트 및 전달 기록을 내 애플리케이션 기록과 대조합니다. 확정된 주문은 한 번만 이행되고, 중간 상태·다른 주문의 이벤트·중복 이벤트가 잘못된 제공을 일으키지 않아야 검사가 끝난 것입니다.
참고: API v1 인보이스, webhook 서명 검증.