개발자 문서
Kem Business API로 USDT 결제를 받기 위해 필요한 모든 것: 인증, 결제 엔드포인트, 그리고 주문 상태를 이끄는 서명된 웹훅.
인증
대시보드의 「API 키」에서 키를 만들고, 모든 요청에 시크릿 키를 Bearer token으로 보내세요. 테스트 키(sk_test_…)는 테스트 결제를 만들고, 라이브 키(sk_live_…)는 실제 돈을 움직입니다.
Authorization: Bearer sk_live_... # or sk_test_...
Base URL: https://api-business.kemapp.io결제
결제를 생성하고 고객을 호스팅된 checkout_url로 보내세요 — 지갑·네트워크·확인은 Kem이 처리합니다. 결제를 조회하거나, 더 좋게는 웹훅으로 상태를 추적하세요.
/v1/paymentscurl -X POST https://api-business.kemapp.io/v1/payments \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{
"amount": "25.000000",
"currency": "USDT",
"merchant_reference": "order-1042",
"customer_email": "[email protected]",
"description": "Pro plan",
"return_url": "https://yourshop.example/thanks",
"metadata": {"order_id": "1042"}
}'{
"id": "pay_01J...",
"status": "pending",
"mode": "test",
"amount": "25.000000",
"amount_minor": 25000000,
"currency": "USDT",
"merchant_reference": "order-1042",
"checkout_url": "https://pay.kemapp.io/pay_01J...",
"checkout_expires_at": "2026-07-13T13:30:00+00:00",
"created_at": "2026-07-13T13:00:00+00:00"
}Idempotency-Key — 선택이지만 권장 — 같은 키로 재시도하면 중복 생성 대신 원래 결제가 반환됩니다.
/v1/payments— query: status, limit (1–100, default 20), cursor · returns has_more, next_cursor/v1/payments/{id}amount — decimal string with 6 places ("25.000000"); amount_minor — the same value as an integer in micro-USDT.
결제 라이프사이클
pending → detected → succeeded · failed · expired
pending — 이체 대기 · detected — 온체인에서 감지, 확인 중 · succeeded — 지갑에 자금 확정(이 이벤트에 주문 처리) · failed / expired — 종결. 환불 변화는 웹훅 이벤트로 도착합니다.
checkout window: 30 minutes, then → expired
오류와 한도
모든 오류는 하나의 envelope을 씁니다. 코드는 안정적입니다 — 메시지가 아니라 error.code로 분기하세요.
HTTP 4xx/5xx
{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key reused with a different request"
}
}| 401 unauthorized | missing or invalid API key |
| 404 not_found | unknown payment id (or a live id with a test key) |
| 409 idempotency_conflict | Idempotency-Key reused with a different body — identical retries replay the original response |
| 422 (validation) | malformed body — bad amount, unsupported currency |
| 429 rate_limited | over the per-key budget: 100 writes/min, 1000 reads/min |
상품, 결제 링크, 웹훅 엔드포인트, 환불은 대시보드에서 관리합니다 — 아직 공개 REST 엔드포인트가 없습니다.
웹훅
대시보드 「Webhooks」에서 엔드포인트를 추가하고 필요한 이벤트를 고르세요. 엔드포인트마다 whsec_… 서명 시크릿이 발급되며 생성 시 한 번만 표시됩니다.
| payment.created | A payment intent was created (API or checkout link). |
| payment.detected | The transfer was seen on-chain / in the KEM ledger; confirming. |
| payment.succeeded | Funds are confirmed in your business wallet. Fulfill on this event. |
| payment.failed | The payment failed. |
| payment.expired | The checkout window closed without payment. |
| payment.refund.created | A refund was initiated from the dashboard. |
| payment.refund.succeeded | The refund settled back to the customer. |
이벤트 페이로드
{
"id": "payment_event_01J...",
"type": "payment.succeeded",
"created": "2026-07-13T13:04:12+00:00",
"data": {
"payment": {
"id": "pay_01J...",
"status": "succeeded",
"amount_minor": 25000000,
"currency": "USDT",
"merchant_reference": "order-1042",
"network": "TRON",
"product_id": null,
"payment_link_id": null
}
}
}서명 검증
모든 전송에는 Kem-Signature 헤더가 있습니다. 엔드포인트 시크릿으로 「<t>.」+원문 바디에 대한 HMAC을 재계산해 상수 시간으로 비교하고, 불일치는 거부하세요.
Kem-Signature: t=<unix_timestamp>,v1=<hmac_sha256_hex>
signed_input = "<t>." + raw_request_body
v1 = HMAC_SHA256(endpoint_secret, signed_input) # secret: whsec_...
# reject replays: drop the delivery when |now - t| > 300simport crypto from "node:crypto";
export function verifyKemSignature(rawBody, header, secret) {
// header: "t=1720000000,v1=hex"
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", secret) // secret: whsec_...
.update(`${parts.t}.`)
.update(rawBody) // the RAW request bytes
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}import hashlib, hmac
def verify_kem_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=") for p in header.split(","))
expected = hmac.new(
secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])전송과 재시도
2xx로 빠르게 응답하세요. 실패한 전송은 백오프로 재시도된 뒤 데드레터 처리됩니다:
1m · 5m · 30m · 2h · 12h · 24h
테스트
테스트 키로는 돈을 움직이지 않고 결제 전체 흐름을 진행할 수 있습니다 — 전환을 시뮬레이션하고 웹훅이 도착하는 것을 확인하세요:
curl -X POST https://api-business.kemapp.io/v1/payments/{id}/_simulate/succeeded \
-H "Authorization: Bearer sk_test_..."