API 레퍼런스: 청구서와 환불
청구서 메서드는 createInvoice, getInvoices, deleteInvoice,
refundInvoice예요. 공통 규칙(인증, 응답 형식, 금액, spend_id)은
Merchant API 레퍼런스에 있고, 흐름을 따라가는
설명은 청구서로 결제 받기에 있어요.
createInvoice
POST /pay/api/createInvoice — invoices 권한, 분당 60회.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
currency_type | string | 아니요 | crypto(기본값) 또는 fiat |
asset | string | crypto 모드 | 청구할 자산, 예: USDT. fiat과 함께 쓸 수 없어요 |
fiat | string | fiat 모드 | 가격을 매길 법정화폐(getCurrencies에서 is_fiat인 행). 코인 모드에서는 무시돼요. 법정화폐로 가격을 매기려면 currency_type: "fiat"를 지정하세요 |
accepted_assets | string / array | 아니요 | fiat 모드 전용. 내는 사람이 결제에 쓸 수 있는 자산이에요 — 쉼표로 이은 문자열("USDT,GRAM")이나 JSON 배열. 비워 두면 지원하는 모든 자산 |
amount | string | open_amount를 안 쓰면 필수 | 양수 소수점 문자열. crypto 모드에서는 자산 단위, fiat 모드에서는 법정화폐 단위(소수점 2자리까지) |
open_amount | boolean | 아니요 | 확장 기능, crypto 모드 전용. 금액을 정해 두지 않고 내는 사람이 결제할 때 넣어요(후원, 팁). amount와 함께 쓸 수 없어요 |
description | string | 아니요 | 1024자까지, 내는 사람에게 보여요 |
hidden_message | string | 아니요 | 2048자까지, 결제한 뒤에만 보여요 |
payload | string | 아니요 | 내가 넣는 데이터 4096자까지. 청구서와 웹훅에 그대로 돌아와요 |
allow_comments | boolean | 아니요 | 내는 사람이 코멘트를 남길 수 있게 해요(기본값 true) |
allow_anonymous | boolean | 아니요 | 내는 사람이 자기를 숨길 수 있게 해요(기본값 true) |
paid_btn_name | string | 아니요 | 결제 후 버튼: viewItem, openChannel, openBot, callback |
paid_btn_url | string | 아니요 | 버튼이 열 http(s) 주소, 최대 512자. paid_btn_name을 쓰면 꼭 넣어야 하고, paid_btn_name 없이 보내면 무시돼요 |
swap_to | string | 아니요 | 받은 결제를 이 자산으로 자동 교환해요. 가능할 때만 바꿔 주는 방식이라, 결제 시점에 교환이 안 되면 교환 없이 결제만 끝나요 |
expires_in | integer | 아니요 | 청구서가 만료될 때까지의 시간(초), 최대 2678400(31일). 생략하거나 0이면 기본 유효 기간이 적용되는데, 지금은 이것도 31일이에요. 결과의 expiration_date를 확인하세요 |
rate_lock_seconds | integer | 아니요 | 확장 기능, fiat 모드 전용. 만들 때의 환산 시세를 이 시간 동안 묶어 둬요(아래 참고) |
설명, 결제 후 콘텐츠, 버튼은 검토 중이거나 승인되지 않으면 결제자에게 숨겨질 수 있어요. 청구서 금액은 계속 결제할 수 있어요.
결과는 — 그리고 invoice_paid 웹훅의 페이로드도 — 청구서 객체예요.
주요 필드는 이래요.
- 식별자와 상태:
invoice_id,hash(pay_url안에 들어가는 공개 id),status(active/paid/expired),pay_url— 내는 사람에게 보내는t.me링크예요(bot_invoice_url,mini_app_invoice_url,web_app_invoice_url은 같은 값의 별칭이에요). - 금액:
amount(액면 금액 — fiat 청구서면 법정화폐 단위, 아니면 코인 단위예요. 금액을 정하지 않은 청구서가 아직 결제 전이면null),amount_minor(항상 코인의 최소 단위예요. 코인 청구서는 액면 금액, fiat 청구서는 결제 전까지null이고 결제 후에는paid_asset의 최소 단위, 금액을 정하지 않은 청구서는 결제 전까지null), 그리고 결제된 fiat 청구서에는paid_asset/paid_amount/paid_fiat_rate— 실제로 청구한 코인과 쓰인 시세예요. fiat 청구서의asset은 결제 후에도null이에요. - 수수료:
fee_asset/fee_amount. 결제할 때 찍히고, 장부에 쓸 값은 이거예요(fee와usd_rate는 이제 쓰지 않는 Crypto Bot 호환 별칭이에요). 수수료와 한도를 보세요. - 결제한 사람:
paid_by_user_id(익명을 골랐으면null),paid_anonymously,comment. - 환불(확장 기능): 누적으로 쌓이는
refunded_amount/refunded_minor, 그리고 전액 환불되면 찍히는refunded_at. - 시세 고정(확장 기능):
rate_lock_until과rate_lock_rates— 자산별로 찍어 둔 시세예요. 고정을 요청하지 않았으면null이에요. - 시각과 결제 정보:
created_at,paid_at,paid_usd_rate(결제 때 기록한 USD 시세). - 만들 때 넣은 값:
currency_type,accepted_assets,open_amount,is_anonymous,description,hidden_message,payload,paid_btn_name/paid_btn_url,expiration_date(expires_at은 별칭), 교환 관련 필드(swap_to,is_swapped,swapped_to,swapped_rate,swapped_output, …).
오류: 400 invalid_currency(코인 모드에서 asset이 없을 때, fiat 모드에서
fiat이 없거나 asset을 함께 보냈거나 open_amount를 지정했을 때),
400 invalid_amount(자산 정밀도로 반올림하면 0이 되는 금액 포함),
400 unknown_asset, 400 unsupported_fiat,
400 paid_btn_url_required. 시세 고정을 요청했다면
400 rate_lock_fiat_only, 409 ratelock_disabled,
409 rate_unavailable(받기로 한 자산의 최신 시세가 없을 때 — 다시
시도하세요)도 있어요. 400 invalid_request는 파라미터 형식이 잘못됐다는
뜻이에요. 알 수 없는 currency_type이나 paid_btn_name, 길이 제한을 넘은
텍스트, 0–2678400 범위를 벗어난 expires_in, JSON 숫자로 보낸 금액이 여기에
해당해요.
청구서는 이렇게 결제돼요
결제자는 미니 앱에서 지갑 잔액으로 즉시 결제하며 네트워크 수수료는 없어요. 충분한 잔액과 결제 확인 조건을 충족해야 해요. 외부 지갑에서 충전할 수도 있어요. 해당 옵션이 제공되면 앱이 입금 주소와 결제 기한을 표시해요. 입금은 결제자 본인의 지갑에 반영돼요. 자동 결제에는 해당 주소로의 입금, 충분한 사용 가능 잔액, 나가는 결제 제한이 없는 상태, 만료되지 않은 결제 요청과 청구서가 필요해요. 늦게 도착한 입금은 결제자의 지갑에 남으며 청구서 결제를 증명하지는 않아요. 어느 경로든 일반적인 paid 청구서가 되고, 웹훅 URL을 설정했다면 invoice_paid 이벤트가 발생해요. 별도로 처리할 파라미터나 필드는 없어요.
fiat 청구서는 결제 시점에 코인 금액을 계산하고 선택한 자산의 최소 단위로 올림해요. 그 코인 금액에서 가맹점 수수료를 차감해요. 유효한 고정 시세나 최신 시세가 없으면 오래된 시세로 처리하지 않고 결제자 쪽에서 결제가 실패해요.
fiat 청구서의 시세 고정하기
rate_lock_seconds를 넘기면 받기로 한 모든 자산의 현재 시세를 만들 때 찍어 둬요. 환산 금액이 고정된 동안 내는 사람은 찍힌 금액 그대로 보고, 결제도 그 시세로 환산돼요. 그 시간 동안의 시세 변동은 내가 떠안아요. 매개변수는 0~86400초를 받아요. 0이면 시세를 고정하지 않아요. 나머지 값은 최소 60초부터 플랫폼 상한까지 지원 범위에 맞춰 조정해요. 0~86400 밖의 값은 400 invalid_request로 거절돼요. 실제 만료 시각은 결과의 rate_lock_until에서 확인하세요.
시세 고정이 끝나도 청구서가 만료되지 않았다면 결제 시점의 시세로 결제할 수 있어요. 두 기한을 맞추려면 지원되는 고정 시간 범위 안에서 expires_in을 설정하고 응답의 expiration_date와 rate_lock_until을 비교하세요.
getInvoices
GET /pay/api/getInvoices — read 권한. 필터: asset, fiat,
invoice_ids(쉼표로 구분), status(active / paid / expired —
expired는 확장 기능이고, active에는 기한이 지난 청구서가 빠져요), 그리고
offset / count(기본 100, 최대 1000). {"items": [invoice, …]}를 최신순으로 돌려줘요.
deleteInvoice
POST /pay/api/deleteInvoice — invoices 권한. 파라미터는 invoice_id
하나예요. 아직 결제되지 않은 청구서를 활성·만료 여부와 관계없이 삭제하고
true를 돌려줘요. 표시만 바뀌는 게 아니라 청구서 자체가 지워져요.
getInvoices에서 사라지고, pay_url도 더 이상 열리지 않으며, 웹훅도 가지
않아요. 삭제 기록이 필요하면 직접 시스템에 남기세요. 오류:
404 invoice_not_found, 409 invoice_already_paid — 이미 결제된 청구서는 돈이
움직인 뒤라 지울 수 없어요.
refundInvoice
POST /pay/api/refundInvoice — refunds 권한, 분당 30회. Crypto Bot에는 없는
확장 기능이에요. 실제로 결제한 코인 금액의 전부 또는 일부를 앱 잔액에서
결제한 사람에게 돌려줘요. 익명으로 결제한 사람에게도 보낼 수 있고, 누구인지는
드러나지 않아요. fiat 청구서에서는 paid_asset과 paid_amount를 사용해요. 환불할 때 법정화폐를 새로 환산하지 않아요.
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
invoice_id | integer | 예 | 결제된 청구서 |
amount | string | 아니요 | 원래 결제 자산(fiat 청구서는 paid_asset)으로 돌려줄 코인 금액. 생략하면 아직 환불하지 않은 나머지 전액이에요. 부분 환불은 원래 결제한 코인 금액까지 누적돼요 |
spend_id | string | 아니요 | 멱등키 — 응답이 늦어 다시 보내도 두 번 환불되지 않게 하나 넣어 주세요 |
결과는 누적 refunded_amount / refunded_minor가 반영된 청구서 객체예요.
전액 환불되면 refunded_at이 찍혀요. 상태는 paid 그대로예요. 서비스
수수료는 돌려주지 않아요. swap_to로 다른 자산으로 교환했더라도 앱에 원래 결제 자산의 잔액이 충분해야 해요. 환불할 때마다 원할 때만 켜는 refund_completed
웹훅이 발생해요.
오류: 404 invoice_not_found, 409 invoice_not_paid,
409 already_refunded(더 환불할 게 없을 때), 409 amount_too_big(남은
금액보다 클 때), 409 insufficient_funds, 400 invalid_amount,
400 invalid_request(invoice_id가 없거나 spend_id가 64자를 넘을 때),
403 verification_required(소유자의 자금 지급 거래가 제한된 경우),
503 merchant_payouts_paused, 그리고
spend_id 관련 오류인 409 idempotency_conflict / 409 idempotency_in_progress.
이 문서가 도움이 됐나요?
의견 고마워요.