청구서 만들기
POST 한 번입니다. 금액은 사람이 쓰는 방식 그대로 적고 서버가 해당 자산의 소수 자릿수로 환산하므로, 기본 단위로 손수 변환할 일이 없습니다.
curl -X POST https://api.unheld.io/api/v1/invoices \
-H "Authorization: Bearer $UNHELD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chainId": "11155111",
"assetType": "native",
"amountDecimal": "0.05",
"expiresInSeconds": 900,
"metadata": { "orderId": "A-1043" }
}'응답
{
"id": "e2b0…",
"status": "CREATED",
"receiveAddress": "0x…",
"expectedAmount": "50000000000000000",
"minConfirmations": 3,
"expiresAt": "2026-08-10T14:15:00.000Z",
"instructions": {
"chainId": "11155111",
"to": "0x…",
"amount": "50000000000000000",
"assetType": "native"
}
}receiveAddress는 판매자의 지갑에서 파생되어 이 청구서에 묶입니다. expectedAmount는 기본 단위입니다. minConfirmations는 저희가 만들어 낸 설정이 아니라 그 체인 자체의 기준값입니다.
인증과 스코프
키는 베어러 토큰이며, 대시보드에서 만들어 한 번만 표시됩니다. 모든 키는 명시적인 스코프 목록을 지니고, 키에 해당 경로의 스코프가 없으면 요청이 거부됩니다.
사용 가능한 스코프
- invoices:write
- invoices:read
- webhooks:write
- webhooks:read
- balances:read
- customers:write
- customers:read
- subscriptions:write
- subscriptions:read
동작하는 가장 좁은 조합만 부여하세요. 청구서만 만드는 키는 고객을 읽을 수 없고, 유출된 읽기 키는 아무것도 움직일 수 없습니다. 이 API에서는 애초에 그럴 수 있는 것이 없기 때문입니다.
검증할 수 있는 웹훅
모든 전달에는 서명이 붙습니다. 서명은 타임스탬프와 원본 본문을 점으로 이은 값을 판매자의 웹훅 비밀 키로 계산한 HMAC-SHA256이며, 16진수 다이제스트로 보내집니다.
전달 검증하기
const signature = crypto
.createHmac("sha256", webhookSecret)
.update(`${req.headers["x-timestamp"]}.${rawBody}`)
.digest("hex");
crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(req.headers["x-signature"])
);다이제스트는 일정 시간 비교로 대조하고, 허용 범위를 벗어난 타임스탬프는 거부하며, X-Request-Id를 멱등 키로 다루세요. 재시도는 그 값을 그대로 쓰므로 같은 이벤트가 두 번 도착하는 것은 정상이며, 두 번 처리되어서는 안 됩니다.
청구서 이벤트
결제는 미납에서 완납으로 툭 뒤집히는 것이 아니라 여러 상태를 거쳐 움직이고, 모든 전이가 웹훅입니다. 부족 결제와 초과 결제는 오류가 아니라 그 자체로 하나의 이벤트입니다.
| 이벤트 | 무슨 일이 있었는가 |
|---|---|
| invoice.created | 청구서가 존재하고 수취 주소가 감시되고 있습니다. |
| invoice.receiving | 결제하는 거래가 체인에서 보였지만 아직 확정되지 않았습니다. |
| invoice.confirming | 결제가 승인을 쌓는 중이며 아직 체인의 기준값에 이르지 못했습니다. |
| invoice.paid | 체인이 요구하는 깊이까지 완전히 확정되었습니다. 이행해도 안전합니다. |
| invoice.underpaid | 예상보다 적게 도착했습니다. 부분 결제가 누적되므로 두 번째 송금으로 마무리할 수 있습니다. |
| invoice.overpaid | 예상보다 많이 도착했으며, 반올림해 없애지 않고 정확히 기록되었습니다. |
| invoice.expired | 완납되지 않은 채로 접수 기간이 닫혔습니다. |
전체 흐름을 무료로 시험하세요
실제 자금을 쓰지 않고 청구서 생성, 감지, 승인, 웹훅을 시험할 수 있습니다. 테스트넷 토큰에는 금전적 가치가 없습니다. 테스트넷에서의 활동은 실제 결제가 아닙니다.
적용 범위
- 자금을 결코 보관하지 않습니다. 출금할 잔액 엔드포인트가 없습니다. 결제가 판매자의 주소에 도착해 그대로 머물기 때문입니다.
- 법정화폐로 환전하거나 정산하지 않습니다. 결제는 보내진 자산 그대로, 보내진 체인 위에 도착합니다.
- 아직 공식 SDK가 없습니다. 여기 있는 것은 모두 평범한 HTTP이며, 저희가 형편없이 관리하게 될 패키지 대신 예제를 공개합니다.
개발자 질문
샌드박스가 있나요?
실제 자금을 쓰지 않고 청구서 생성, 감지, 승인, 웹훅을 시험할 수 있습니다. 테스트넷 토큰에는 금전적 가치가 없습니다. 테스트넷에서의 활동은 실제 결제가 아닙니다.
같은 웹훅을 두 번 처리하지 않으려면 어떻게 하나요?
X-Request-Id를 멱등 키로 쓰세요. 재시도는 같은 값을 그대로 쓰므로, 그것을 기록해 두고 반복은 무시하시면 됩니다. 재시도는 예정된 동작입니다. 실패한 전달은 버려지는 것이 아니라 정해진 일정에 따라 다시 시도됩니다.
금액을 기본 단위로 직접 변환해야 하나요?
아닙니다. amountDecimal을 사람이 쓰듯 보내면 API가 해당 자산의 소수 자릿수로 환산합니다. 응답은 expectedAmount를 기본 단위로 돌려주므로 두 표현이 모두 명시되어 서로 어긋날 수 없습니다.
예제를 가동 중인 API와 대조해 확인한 날짜 .