개발자 문서
API 문서
청구서, 결제 화면, 결제 알림을 연동하세요.
검색 결과
결과가 없어요. 엔드포인트, 필드, 가이드 이름으로 검색하세요.
빠른 시작
첫 청구서를 만드세요.
- 스토어 준비
결제 수단을 켜고 제공업체를 설정한 뒤 프로젝트 지갑을 백업하세요.
- API 인증 정보 생성
콘솔의 설정 → API 접근에서 읽기·쓰기를 선택하고 프로젝트를 지정하세요.
- 요청 보내기
API 호스트와 다음을 사용하세요: 프로젝트와 스토어 ID를 복사하세요. 십진수 금액은 문자열로 보내세요.
- 결제 화면 열기
다음으로 이동:
links.checkout응답의 값을 사용하세요. 주문 이행 전에 정산을 검증하세요.
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new invoice.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: order-1042-attempt-1' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"exchange_rate_spread_percent": "0.5"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new invoice.
const body = `{
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"exchange_rate_spread_percent": "0.5"
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "order-1042-attempt-1",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new invoice.
$body = <<<'JSON'
{
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"exchange_rate_spread_percent": "0.5"
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: order-1042-attempt-1", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new invoice.
headers = {
"Idempotency-Key": "order-1042-attempt-1",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"exchange_rate_spread_percent": "0.5"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))예제는 자리표시자를 사용하며 이 페이지에서 요청을 보내지 않아요. 모든 청구서 필드 및 응답 형식 보기 →
프로젝트 및 스토어 ID
YOUR_PROJECT_ID와 YOUR_STORE_ID를 찾는 곳.
콘솔의 UUID를 사용하세요. 프로젝트·스토어 이름이나 읽기 쉬운 식별자는 쓰지 마세요.
| 자리표시자 | 찾는 위치 | 용도 |
|---|---|---|
| YOUR_PROJECT_ID | 프로젝트 → 설정 → API ID → 프로젝트 API ID → 복사. 스토어의 기본 탭에도 표시돼요. | 프로젝트 및 스토어 수준 요청. |
| YOUR_STORE_ID | 프로젝트 → 스토어 → 스토어 선택 → 기본 → API ID → 스토어 API ID → 복사. | 청구서 생성 및 스토어 결제 수단 요청. |
- 기본 스토어라도 청구서 생성에는 ID 두 개가 모두 필요해요. 스토어는 해당 프로젝트에 속해야 하며 API 인증 정보도 그 프로젝트의 접근 권한이 있어야 해요.
- 청구서 생성, 목록, 상세, 결제 화면은 IPN/웹훅과 같은 UUID인 invoice_id를 반환해요. 청구서 경로에는 내부 id나 order_id가 아닌 이 값을 사용하세요. 판매자 버전 4.0.0부터 기존 public_id 응답 필드가 제거됐으니 업그레이드 전에 연동을 업데이트하세요.
- REST API에는 프로젝트·스토어 목록 경로가 없어요. 콘솔에서 ID를 복사하거나 판매자 버전 5.0.0+의 범위가 제한된 list_projects 및 list_stores MCP 도구를 사용하세요.
- 스토어 → 기본 → 스토어 도메인에서 활성 판매자, 결제, API 호스트 이름을 선택해요. 반환되는 결제 링크와 새 콜백 링크는 해당 스토어, 기본 스토어, 시스템 기본값 순으로 선택해요. 폐기되거나 활성화되지 않은 이름은 선택하지 않아요. SDK에 원하는 API 호스트 이름을 설정하세요. 기본값을 바꿔도 다른 활성 별칭은 리디렉션되지 않아요.
인증 및 범위
인증 정보는 서버에 보관하고 필요한 접근 권한만 부여하세요.
| 기본 호스트 | 용도 |
|---|---|
| merchant.example.com | 판매자 콘솔 및 설정 |
| pay.example.com | 고객 결제 화면 |
| api.example.com | 판매자 API 요청 |
바꿀 항목: example.com 을 내 도메인으로 바꾸세요. 기존 설치는 설정된 이름을 유지하며 설정 → 시스템에서 별칭을 관리할 수 있어요.
Authorization: Bearer YOUR_MERCHANT_API_TOKEN| 설정 | 이용 방법 |
|---|---|
| 접근 수준 | 읽기 전용 인증 정보로 목록과 상세를 조회할 수 있어요. 읽기·쓰기 인증 정보는 청구서 생성과 문서에 명시된 자산 정책 업데이트도 할 수 있어요. |
| 프로젝트 | 인증 정보가 접근할 프로젝트를 지정하세요. 스토어와 청구서 ID는 지정된 프로젝트에 속해야 해요. |
| IP 제한 | 설정 → API 접근에서 정확한 공인 IPv4 또는 IPv6 송신 주소만 허용할 수 있어요. |
| 인증 정보 보관 | 토큰은 백엔드 설정에 보관하세요. Bearer 인증 정보를 브라우저나 결제 링크에 넣지 마세요. |
공개 결제 경로는 청구서의 공개 ID를 사용하며 결제용으로 안전한 데이터만 노출해요. 콘솔 세션과 관리 기능은 판매자 API 인증 정보와 별개예요.
자산 및 지갑
스토어별로 결제 수단을 따로 선택하세요.
- 조회 프로젝트 결제 자산 과 준비 상태를 확인하세요.
- 기본 체인을 켜고 지갑과 제공업체를 설정하세요.
- 둘러보기 토큰 후보 및 컨트랙트 또는 민트 검증 후 토큰을 활성화하세요.
- 스토어에서 순서대로 선택할 항목: 결제 수단. 새 청구서는 준비된 선택지를 사용해요.
토큰은 기본 체인의 지갑을 공유해요. 지갑 잔액 은 정확한 최소 단위 금액과 참고 법정화폐 가치를 반환해요. 반환된 준비 상태 필드로 어떤 수단이 결제를 받을 수 있는지 판단하세요.
검증된 ERC-20 토큰은 지원 EVM 네트워크를, 검증된 SPL 토큰은 Solana를 사용해요. 통합된 30개 네트워크에 기본 결제 수단이 있어요. Monero는 프로젝트에 연결된 외부 조회 전용 지갑 연결을 사용해요.
수신 API 및 기본 코인·토큰 지원 범위
| 결제 경로 | 지원 | 증거 | 요구 사항 |
|---|---|---|---|
| 기본 코인 결제 경로 | 지원됨 | 거래 스캔 | BTC, SOL, ETH(Ethereum/Base/Arbitrum/OP), BNB, HYPE, AVAX, POL. Bitcoin 출력, 정규 EVM 거래·영수증, 파싱된 Solana 전송이 청구서 증거를 제공해요. |
| ERC-20 토큰 결제 경로 | 지원됨 | 거래 스캔 | Ethereum, Base, BNB Chain, HyperEVM, Avalanche, Polygon, Arbitrum, Optimism에는 온체인 검증이 필요해요. 인덱싱된 Transfer 로그로 결제를 연결해요. |
| SPL 토큰 결제 경로 | 지원됨 | 거래 스캔 | Solana 후보는 메인넷과 민트 검증이 필요해요. 파싱된 거래의 정확한 토큰 잔액 변화로 결제를 연결해요. |
| 추가 UTXO 기본 코인 경로 | 지원됨 | 거래 스캔 | BCH/LTC/DOGE는 Esplora를 사용해요. BCH/DOGE는 Bitcore, LTC/DOGE/DASH는 BlockCypher, Dash는 Insight, 투명 ZEC는 zcash-explorer도 지원해요. 모두 완전히 보관된 Core 호환 node-rpc 블록도 지원해요. 원시 모드는 멤풀 감지가 아닌 1~48회 확인이 필요해요. 차폐 Zcash는 지원하지 않아요. |
| 인덱스 기반 계정형 기본 코인 경로 | 지원됨 | 거래 스캔 | TRON은 tron-indexer 또는 확정된 node-rpc, XRP는 xrpl-jsonrpc, Stellar는 stellar-horizon 또는 보관된 Stellar node-rpc 원장, Cosmos Hub는 cometbft-jsonrpc, Algorand는 algorand-indexer 또는 algod node-rpc를 사용해요. Hedera는 EVM 릴레이가 아닌 hedera-mirror가 필요해요. |
| 기본 원장 결제 경로 | 지원됨 | 거래 스캔 | Aptos는 aptos-rest, Sui는 sui-graphql, NEAR는 near-jsonrpc, Kaspa는 kaspa-rest를 사용해요. Polkadot Asset Hub는 substrate-rest 또는 메타데이터를 인식하는 최종 확정 node-rpc를 지원하고, Tezos는 tezos-tzkt 또는 전체 Octez node-rpc 작업을 지원해요. 기본 코인 수신만 가능하며 오래된 청구서에는 아카이브 보관이 필요해요. |
| Cardano 및 TON 기본 코인 경로 | 지원됨 | 거래 스캔 | Cardano에는 cardano-koios, TON에는 toncenter-v3가 필요해요. XRP 태그, Stellar 메모 ID, TON 청구서 코멘트는 destination_tag로 반환되며 정확히 그대로 보내야 해요. |
| 정산 무결성 | 지원됨 | 독립 검증 | 기본적으로 최종 정산에는 독립된 제공업체 두 곳이 정확한 거래·이벤트, 금액, 정규 블록이나 슬롯, 최종 확정에 동의해야 해요. 원시 및 공유 EVM 구간도 전체 범위 확인을 거쳐요. 관리자는 특정 체인을 신뢰하는 제공업체 하나만 쓰도록 명시적으로 선택할 수 있어요. 독립 교차 검증만 없어지며 식별, 완전성, 최종 확정 검사는 유지돼요. |
| Monero 기본 코인 경로 | 지원됨 | 프로젝트에 연결된 조회 전용 지갑 RPC | HTTPS 메서드 허용 목록 게이트웨이 뒤의 전용 외부 조회 전용 wallet-RPC 하나가 account-0 하위 주소를 만들어요. 설정된 메인넷 데몬 기준(기본 독립 소스 2개, 선택적으로 1개)이 정산 증거를 제공해요. 기본 --restricted-rpc는 create_address와 호환되지 않아요. 운영자가 지갑 백업과 지출 키 부재를 명시적으로 확인하며, Wholly Crypto에는 키 자료를 보내지 않아요. |
거래소 잔액과 자산별 지갑 또는 거래소 자금 모으기 선택은 콘솔에서 사용할 수 있으며 공개 v1 API에는 없어요. 거래소 설정 보기.
청구서 수명 주기
결제 증거, 정산, 주문 이행.
| 상태 | 의미 |
|---|---|
| new | 결제 대기 중 |
| processing | 결제 발견, 수락 금액 또는 최종 확정 대기 |
| settled | 청구서 정산 정책 또는 수동으로 승인 |
| expired | 기한이 지났지만 지연 결제 모니터링은 계속될 수 있어요 |
| invalid | 자동으로 승인할 수 없는 결제 |
| cancelled | 취소됨. 명시적인 대사만 다시 열 수 있어요 |
amount_status 기록 none, partial, paid 또는 overpaid. timing_status 는 기한 내 결제와 지연 결제를 구분해요. 필요한 확인 횟수와 미달 결제 허용 오차는 스토어 규칙으로 정해요.
청구서의 다음을 사용하세요: invoice_id 함께 사용할 항목: 청구서 상세 경로. 결제 화면 리디렉션만으로 정산을 입증할 수 없어요. 예외는 다음으로 검토하세요: 대사.
안전한 재시도
청구서 생성에는 다음이 필요해요: Idempotency-Key. 시간 초과 후에는 같은 인증 정보, 키, 정확히 같은 요청 본문으로 재시도하세요. 새 청구서에만 새 키를 쓰세요.
EVM 결제 스캔
공유 기본 블록 및 ERC-20 탐색은 최근 청구서와 오래된 기록 따라잡기를 나눠 처리해요. 청구서마다 영구 기록 커서를 유지해요. 토큰 조회는 요청당 최대 100개 블록이며 제공업체 제한이 더 엄격하면 범위를 줄여요. 기본적으로 독립된 제공업체 두 곳이 각 구간을 검증해요. 설정 → 체인 연결 → 상세에서 신뢰하는 소스 하나만 선택하면 독립 교차 검증은 없어지지만 정규 거래, 금액, 확인 검사는 유지돼요. 연결 상세는 스캐너 지연, 기록 제한, 할당량 대기 시간을 기본 노드 상태와 구분해요. 공개 RPC 용량은 보장되지 않아요.
IPN 및 웹훅
결제 이벤트를 받고 검증하세요.
IPN은 청구서의 유효한 ipn_url로 생성된 모든 청구서 이벤트를 받아요. 웹훅은 활성 스토어 엔드포인트별로 선택한 이벤트만 받아요. 둘 다 같은 JSON 스냅샷을 POST하지만 서로 독립적이므로 둘 다 켜면 앱에 두 번 알릴 수 있어요.
설정 ipn_url 를 청구서 생성 시 지정하거나 스토어 기본값을 이어받으세요. IPN이 사용하는 항목: 스토어 → IPN 비밀 키예요. 각각의 스토어 → 웹훅 엔드포인트에는 자체 비밀 키가 있어요. 둘 다 API 키가 아니에요.
주문은 언제 이행해야 하나요?
이벤트 기반 처리에서는 event_type = invoice.settled와 status = settled를 함께 사용해 주문 확인을 시작하세요. 현재 청구서를 검증하고 주문마다 한 번만 이행하세요.
status는 이벤트 생성 시 청구서 상태이고 event_type은 발생한 일을 알려줘요. payment.received에는 processing 또는 settled가 올 수 있어요. 두 번째 결제를 뜻하지 않으며 독립적인 주문 이행 신호도 아니에요.
어떤 이벤트와 상태가 전송되나요?
| 설정·기록의 이벤트 | 본문 상태 | 의미 |
|---|---|---|
| invoice.created | new | 청구서가 생성되어 결제를 기다려요. 통제된 재개로 청구서가 new로 돌아갈 때도 사용해요. |
| payment.received | Resulting invoice status | 결제가 기록되거나 받은 금액이 늘었어요. 보통 processing 또는 settled이며 이 이벤트만으로 정산을 입증할 수 없어요. |
| invoice.processing | processing | 결제는 감지됐지만 수락 금액이나 필요한 최종 확정 조건을 아직 충족하지 못했어요. 부분 결제도 포함돼요. |
| invoice.settled | settled | 정산 정책을 충족했거나 수동 승인됐어요. 주문 이행 전에 resolution과 주문을 확인하세요. |
| invoice.expired | expired | 결제 기한이 지났어요. 모니터링이 계속되는 동안 지연 결제로 상태가 바뀔 수 있어요. |
| invoice.invalid | invalid | 자동 승인할 수 없거나 결제 증거를 잃었거나 판매자가 거절했어요. 청구서를 검토하세요. |
| invoice.cancelled | cancelled | 청구서가 취소됐어요. 주문을 이행하지 마세요. 취소해도 온체인 결제는 환불되지 않아요. |
Ethereum과 Solana의 이벤트 흐름이 다를 수 있는 이유
확인이 나중에 도착(Ethereum 예시)
| 순서 | event_type | status |
|---|---|---|
| 1 | invoice.created | new |
| 2 | payment.received | processing |
| 2 | invoice.processing | processing |
| 3 | invoice.settled | settled |
감지 시 이미 최종 확정(Solana 예시)
| 순서 | event_type | status |
|---|---|---|
| 1 | invoice.created | new |
| 2 | payment.received | settled |
| 2 | invoice.settled | settled |
이는 이벤트 생성 순서이며 전송 순서를 보장하지 않아요. 다른 체인도 감지 시점과 정산 정책에 따라 어느 흐름이든 발생할 수 있어요. settled 전에 processing 이벤트가 반드시 있어야 한다고 요구하지 마세요.
한 번만 이행: 수신기 예제 및 중복 방지
| 방식 | 처리 방법 |
|---|---|
| 이벤트 기반 수신기 | 서명된 event_id로 서로 다른 이벤트를 보관한 다음 status = settled인 invoice.settled를 선택하세요. 같은 sequence의 payment.received가 먼저 왔다고 이 이벤트를 버리지 마세요. |
| SDK 주문 상태 수신함 | 제공된 PHP, Python, Node 수신기 예제는 프로젝트 + invoice_id + sequence로 묶어요. event_type에 관계없이 저장된 상태를 처리하고 현재 청구서를 조회한 뒤 정산됐다면 한 번만 이행하세요. 묶은 뒤 invoice.settled만 허용하는 필터를 추가하지 마세요. |
재시도는 event_id와 원본 본문을 유지해요. 다른 이벤트는 sequence가 같아도 event_id는 달라요. 이벤트 기반 처리에서는 서명된 event_id로 중복 전송을 제거하고, 설정된 설치·프로젝트 + invoice_id 및 주문 기준으로 이행을 별도 보호하세요. 나중에 재정산돼도 주문에 두 번 반영하면 안 돼요.
HTTP receiver:
Verify raw-body signature, timestamp and configured project/store scope.
Save to a durable inbox; deduplicate the signed event_id.
Return HTTP 2xx only after persistence succeeds.
Event-based background worker:
Other events go to status/reconciliation handling, not fulfilment.
Continue here only for event_type = invoice.settled and status = settled.
Fetch the current invoice from your configured API origin.
Check settled status, project/store, order, amount, currency and review policy.
In one database transaction:
Lock the order and check the scoped invoice has not been fulfilled.
Credit/complete once and save the fulfilment record.
Queue any external fulfilment with the same business idempotency key.
SDK order-state worker:
Use the same current-invoice checks and fulfil-once transaction.
Do not filter event_type after collapsing events by invoice revision.의사 코드이며 바로 사용할 수 있는 수신기는 아니에요.
모든 청구서 상태 및 결제 예외
| 필드 | 값 | 의미 |
|---|---|---|
| status | new, processing, settled, expired, invalid, cancelled | 이벤트 생성 시 청구서 상태이며 전송 시의 현재 상태와는 다를 수 있어요. |
| amount_status | none, partial, paid, overpaid | 수락된 허용 오차를 포함한 수신 금액이에요. paid는 최종 확정을 뜻하지 않아요. |
| timing_status | on_time, late | 결제가 청구서 기한 안에 도착했는지 여부예요. |
| resolution | automatic, manually_settled, manually_invalidated | 일반 규칙 또는 수동 승인·거절 중 무엇이 결과를 결정했는지 나타내요. |
| requires_review | false, true | 예외 힌트이며 별도의 청구서 상태나 자동 주문 이행·환불 허가가 아니에요. |
| 상황 | 처리 |
|---|---|
| 미달 결제 / 허용 오차 | 자동 규칙에서는 partial이 정산되지 않아요. paid에는 허용된 부족액이 포함될 수 있지만 최종 확정은 여전히 필요해요. 금액 비교만 하지 말고 청구서 상태를 사용하세요. |
| 초과 결제 | overpaid는 settled 및 requires_review = true와 함께 나타날 수 있어요. 초과 결제 정책을 적용하세요. 주문에 중복 반영하거나 검증하지 않은 주소로 자동 환불하지 마세요. |
| 지연 결제 | 모니터링 중에는 expired가 나중에 바뀔 수 있어요. timing_status = late는 검토가 필요하다는 표시예요. 취소한 주문을 자동으로 다시 열거나 발송하지 마세요. |
| 수동 승인 | invoice.settled는 조건에 맞는 온체인 자금 없이 resolution = manually_settled일 수 있어요. 연동에서 이 수동 변경을 허용할지 정하세요. 결제 요약 필드는 null일 수 있어요. |
| 체인 재구성 / 무효화 | 새 리비전은 이전 결제 증거를 무효화할 수 있어요. 현재 상태를 다시 조회하고 대사로 취소를 처리하세요. 주문이 한때 정산됐다는 이유로 무시하지 마세요. |
| 확인 0회 / 금액 0 | 확인 0회 정산은 감지 시 발생할 수 있고 체인 재구성 위험이 있어요. 명시적으로 허용한 금액 0 청구서는 결제 없이 정산돼요. 둘 다 payment.received 이벤트가 먼저 필요하지 않아요. |
주문 이행에는 amount_status = paid나 결제 화면 리디렉션이 아니라 status = settled를 사용하세요. 필요한 확인 횟수가 0이면 감지 시 정산될 수 있으며 체인 재구성 위험이 있어요.
미달 결제는 amount_status = partial, 초과 결제는 overpaid예요. paid는 청구서의 미달 허용 오차를 포함한 수락 최소 금액이 도착했다는 뜻이에요. 이는 금액 상태이지 청구서 상태가 아니에요. late는 별도 이벤트가 아닌 timing_status예요.
일반적인 흐름은 new → processing → settled지만 중간 상태가 생략될 수 있어요. 명시적으로 허용한 금액 0 청구서는 결제 없이 정산되고 amount_status = none을 유지해요. 수동 승인은 manually_settled로 표시해요.
콜백은 변경 불가능한 스냅샷이며 실시간 상태 응답이 아니에요. 늦게, 순서가 바뀌어, 또는 여러 번 도착할 수 있어요. 결제 이벤트와 상태 이벤트는 청구서 sequence 및 상태 필드가 같아도 서명된 event_id와 event_type은 달라요. 확인 횟수가 바뀐다고 블록마다 콜백을 보장하지는 않아요.
받는 내용
{
"invoice_id": "11111111-2222-4333-8444-555555555555",
"status": "settled",
"amount_status": "paid",
"timing_status": "on_time",
"resolution": "automatic",
"sequence": 3,
"amount": "49.9",
"currency": "EUR",
"order_id": "order-1042",
"payload_version": 2,
"event_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"event_type": "invoice.settled",
"occurred_at": "2026-09-14T12:05:00Z",
"project_id": "11111111-1111-4111-8111-111111111111",
"store_id": "22222222-2222-4222-8222-222222222222",
"description": "Annual plan",
"email": "ada@example.test",
"customer": {
"firstname": "Ada",
"lastname": "Lovelace",
"countryiso2": "GB"
},
"metadata": {
"firstname": "Ada",
"lastname": "Lovelace",
"countryiso2": "GB",
"cart_id": "cart-681"
},
"created_at": "2026-09-14T12:00:00Z",
"updated_at": "2026-09-14T12:05:00Z",
"expires_at": "2026-09-14T12:15:00Z",
"monitoring_expires_at": "2026-09-21T12:15:00Z",
"settled_at": "2026-09-14T12:05:00Z",
"paid_chain": "ethereum",
"paid_asset": "USDC",
"paid_asset_amount": "58.17342",
"paid_asset_amount_received": "58.17342",
"paid_payment_method_id": "33333333-3333-4333-8333-333333333333",
"settlement_exchange_rate": {
"rate": "1.17",
"units": "asset_per_invoice_currency",
"currency": "EUR",
"symbol": "USDC",
"observed_at": "2026-09-14T12:05:00Z",
"as_of": "2026-09-14T12:04:30Z",
"pricing_provider": "kraken",
"asset_provider": "kraken",
"pricing_fetched_at": "2026-09-14T12:04:30Z",
"asset_fetched_at": "2026-09-14T12:04:30Z",
"stale": false,
"is_fixed": false,
"reference_currency": "USD",
"uses_reference_proxy": false
},
"cancelled_at": null,
"exchange_rate_spread_percent": "0.5",
"underpayment_tolerance_percent": "1",
"reason_code": "payment_confirmed",
"requires_review": false,
"links": {
"checkout": "https://pay.example.com/invoice/11111111-2222-4333-8444-555555555555",
"invoice": "https://api.example.com/v1/projects/11111111-1111-4111-8111-111111111111/invoices/11111111-2222-4333-8444-555555555555",
"payments": "https://api.example.com/v1/projects/11111111-1111-4111-8111-111111111111/invoices/11111111-2222-4333-8444-555555555555/payments"
},
"payment_info": {
"active_payment_method_id": "33333333-3333-4333-8333-333333333333",
"method_count": 1,
"methods_truncated": false,
"methods": [
{
"payment_method_id": "33333333-3333-4333-8333-333333333333",
"payment_rail": "onchain",
"chain_slug": "ethereum",
"network": "mainnet",
"caip_network_id": "eip155:1",
"asset_id": "44444444-4444-4444-8444-444444444444",
"asset_key": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"caip_asset_id": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"asset_name": "USD Coin",
"symbol": "USDC",
"asset_kind": "token",
"asset_decimals": 6,
"contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"token_standard": "erc20",
"destination_address": "0x1111111111111111111111111111111111111111",
"destination_tag": null,
"status": "paid",
"amounts": {
"expected_amount": "58.17342",
"expected_amount_atomic": "58173420",
"received_amount": "58.17342",
"received_amount_atomic": "58173420",
"confirmed_amount": "58.17342",
"confirmed_amount_atomic": "58173420",
"unconfirmed_amount": "0",
"unconfirmed_amount_atomic": "0",
"minimum_payment_amount": "57.591686",
"minimum_payment_amount_atomic": "57591686",
"remaining_amount": "0",
"remaining_amount_atomic": "0",
"remaining_to_full_amount": "0",
"remaining_to_full_amount_atomic": "0",
"overpaid_amount": "0",
"overpaid_amount_atomic": "0"
},
"acceptance": {
"finality_mode": "confirmations",
"required_confirmations": 2,
"observed_confirmations": 2,
"underpayment_tolerance_percent": "1"
},
"quote": {
"effective_rate": "1.1658",
"reference_rate": "1.16",
"units": "asset_per_invoice_currency",
"currency": "EUR",
"symbol": "USDC",
"exchange_rate_spread_percent": "0.5",
"quote_expires_at": "2026-09-14T12:15:00Z",
"provenance_available": true,
"rounding": "up",
"unrounded_payment_amount": "58.17342",
"rounding_adjustment": "0",
"pricing_provider": "kraken",
"asset_provider": "kraken",
"pricing_fetched_at": "2026-09-14T11:59:30Z",
"asset_fetched_at": "2026-09-14T11:59:30Z"
},
"market_rate_at_event": {
"rate": "1.17",
"units": "asset_per_invoice_currency",
"currency": "EUR",
"symbol": "USDC",
"observed_at": "2026-09-14T12:05:00Z",
"pricing_provider": "kraken",
"asset_provider": "kraken",
"pricing_fetched_at": "2026-09-14T12:04:30Z",
"asset_fetched_at": "2026-09-14T12:04:30Z",
"as_of": "2026-09-14T12:04:30Z",
"stale": false,
"is_fixed": false,
"reference_currency": "USD",
"uses_reference_proxy": false
},
"payment_count": 1,
"payments_truncated": false,
"payments": [
{
"payment_id": "55555555-5555-4555-8555-555555555555",
"payment_method_id": "33333333-3333-4333-8333-333333333333",
"transaction_id": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"payment_hash": null,
"event_index": 0,
"payment_rail": "onchain",
"chain_slug": "ethereum",
"network": "mainnet",
"asset_id": "44444444-4444-4444-8444-444444444444",
"asset_key": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"caip_asset_id": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"symbol": "USDC",
"asset_decimals": 6,
"amount": "58.17342",
"amount_atomic": "58173420",
"status": "final",
"counts_towards_received": true,
"confirmations": 2,
"block_height": 26000000,
"observed_at": "2026-09-14T12:04:30Z",
"chain_time": "2026-09-14T12:04:20Z",
"finalized_at": "2026-09-14T12:05:00Z",
"explorer_name": "Etherscan",
"explorer_url": "https://etherscan.io/tx/0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"links": {
"payments": "https://api.example.com/v1/projects/11111111-1111-4111-8111-111111111111/invoices/11111111-2222-4333-8444-555555555555/payments?payment_method_id=33333333-3333-4333-8333-333333333333"
}
}
]
}
}amount 은 원래 청구서 총액이에요. payment_info 는 관찰된 암호화폐 전송, 아직 부족한 금액, 고정 환율을 설명해요. 버전 2는 이벤트 이름, 이벤트 ID, 프로젝트·스토어 범위에도 서명해요.
모든 콜백 필드 및 추가 청구서 데이터
| 필드 | 유형 | 의미 |
|---|---|---|
| invoice_id | UUID | 인증된 청구서 상세 경로에서 사용하는 공개 청구서 UUID |
| status | string | 청구서 상태 스냅샷: new, processing, settled, expired, invalid, cancelled |
| amount_status | string | none, partial, paid, overpaid. paid는 허용된 부족액을 포함하며 최종 확정은 뜻하지 않아요 |
| timing_status | string | on_time 또는 late |
| resolution | string | automatic, manually_settled 또는 manually_invalidated |
| sequence | integer | 증가하는 청구서 리비전. 다른 이벤트가 같은 리비전을 공유할 수 있어요. 정수 정밀도를 유지해 비교하세요 |
| amount | decimal string | 받은 암호화폐 금액이 아닌 원래 청구서 총액. 십진수 정밀도를 유지하세요 |
| currency | string | amount의 통화. 예: USDC로 결제한 EUR 청구서는 EUR |
| order_id | string | null | 판매자 주문 참조 |
| payload_version | integer | 새로 생성한 4.1.0+ 이벤트는 2, 보관된 이전 이벤트에는 없음 |
| event_id | UUID | 서명된 이벤트 식별자. 재시도와 수동 재전송에서 유지됨 |
| event_type | string | 구독 이벤트 7개 중 하나 |
| occurred_at | timestamp | 전송 시간이 아닌 이 변경 불가능한 이벤트의 생성 시간 |
| project_id | UUID | 판매자 프로젝트 범위. 설정된 수신기와 일치해야 함 |
| store_id | UUID | 판매자 스토어 범위. 설정된 수신기와 일치해야 함 |
| description | string | null | 원래 청구서 설명 |
| string | null | 이벤트 생성 시 선택적인 고객 이메일 | |
| customer | object | 인식된 선택적 고객 메타데이터 필드. 개인 정보를 추측하거나 보강하지 않음 |
| metadata | object | 이벤트 생성 시 존재한 원래 판매자 메타데이터 |
| created_at | timestamp | 청구서 생성 시간 |
| updated_at | timestamp | 청구서 상태 업데이트 시간 |
| expires_at | timestamp | 청구서 결제 기한 |
| monitoring_expires_at | timestamp | 지연 결제 모니터링 기한 |
| settled_at | timestamp | null | 정산 시간 |
| paid_chain | string | null | 4.1.2+: 검증된 정산 수단의 체인 슬러그(예: ethereum). 저장된 적격 정산이 없으면 null |
| paid_asset | string | null | 4.1.2+: 기본 코인 또는 토큰 티커(예: BTC, ETH, USDC). 표시 이름이며 고유 자산 식별자가 아님 |
| paid_asset_amount | decimal string | null | 5.0.1+: 허용 오차 차감 전 paid_asset 단위의 전체 고정 요청 금액. 정산 시 저장 |
| paid_asset_amount_received | decimal string | null | 5.0.1+: 정산 시 최종 결제 수단의 유효 수신 총액. 수락된 미달·초과 금액 포함. 고정값이며 실시간 잔액이 아님 |
| paid_payment_method_id | UUID | null | 4.1.2+: 정산 인텐트 ID. payment_info.methods[].payment_method_id 및 정확한 네트워크·컨트랙트와 일치 |
| settlement_exchange_rate | object | null | 4.1.2+: 정산 시 저장한 스프레드 적용 전 시장 스냅샷. 명시적 단위, 통화, 출처 시간, 품질 표시 포함. 전송 시 재평가하지 않음 |
| cancelled_at | timestamp | null | 취소 시간 |
| exchange_rate_spread_percent | decimal string | 현재 스토어 기본값이 아닌 고정된 스프레드 |
| underpayment_tolerance_percent | decimal string | 고정된 청구서 허용 오차. 각 수단도 실제 적용 허용 오차를 보고함 |
| reason_code | string | null | 기계가 읽을 수 있는 상태 전환 이유 |
| requires_review | boolean | 결제 예외 힌트. 자동 주문 이행이나 환불 허가가 아님 |
| links | object | 이벤트 생성 시 결제 화면, 인증된 청구서, 결제 URL. 스토어 → 기본의 도메인 설정, 기본 스토어, 전역 기본값 순으로 적용하며 역할에 맞는 활성 도메인만 사용해요. 재시도는 원래 서명된 링크를 유지하고 활성 호스트 기록이 없으면 null이에요. |
| payment_info | object | 실제로 관찰된 수단, 정확한 금액, 고정 견적, 참고 시장 스냅샷, 개수 제한이 있는 결제 관찰 기록. 아래 필드 그룹 참고 |
정산 요약: settlement_exchange_rate
| 필드 | 유형 | 의미 |
|---|---|---|
| rate / units / currency / symbol | strings | 스프레드 전 청구서 통화 1단위당 자산 단위 수. 십진수 문자열이며 결제 금액이나 체결된 거래가 아니에요. |
| observed_at / as_of | timestamps | 정산 캡처 시간 / 더 오래된 출처 시간. 캐시 데이터를 실시간 시세로 취급하지 마세요. |
| pricing_provider / asset_provider / pricing_fetched_at / asset_fetched_at | strings / timestamps | 정산 시 저장한 법정화폐·자산 가격 출처와 조회 시간. |
| stale / is_fixed / uses_reference_proxy / reference_currency | booleans / string | market_rate_at_event와 같은 품질 표시. 고정 프로젝트 가격은 표시되며 기준 통화는 USD예요. |
| Missing snapshot or price | null | 과거 환율을 추측하지 않아요. 정산 전에는 모든 요약 필드가 null이고, 가격만 없으면 검증된 paid_* 식별자는 유지돼요. |
결제 수단: payment_info
| 필드 | 유형 | 의미 |
|---|---|---|
| active_payment_method_id | UUID | null | 최종 또는 선택된 관찰 수단. 감지 전이나 무효화 후에는 null이며 기본 수단을 추측하지 않아요. |
| method_count / methods_truncated | integer / boolean | 관찰된 수단 총수와 포함된 수단 목록이 불완전한지 여부. |
| methods[] | object[] | 관찰된 수단 최대 8개이며 활성 수단이 먼저예요. 서로 다른 자산은 합산하지 않아요. |
| payment_method_id / payment_rail | UUID / string | 청구서 인텐트 식별자 및 onchain 또는 lightning 전송 방식. |
| chain_slug / network / caip_network_id | string | 네트워크 식별자. 토큰 식별자는 항상 네트워크와 함께 사용하세요. |
| asset_id / asset_key / caip_asset_id | UUID / string / nullable string | 검증된 레지스트리 식별자. 기호만으로는 고유하지 않아요. |
| asset_name / symbol / asset_kind | string | 자산 표시 이름, 티커, 기본 코인 또는 토큰 유형. |
| contract_address / token_standard | string | null | 토큰 컨트랙트 또는 민트와 표준. 기본 자산은 null. |
| asset_decimals | integer | 최소 단위 정밀도. Lightning BTC는 11. |
| destination_address / destination_tag | string | null | 공개 수신 주소와 필수 memo/tag. Lightning 주소는 null이며 개인 키는 절대 포함하지 않아요. |
| status | string | 수단 상태: pending, partial, paid, overpaid, expired, invalid. paid만으로 청구서 정산을 뜻하지 않아요. |
| payment_count / payments_truncated / payments[] | integer / boolean / object[] | 관찰 기록 총수와 최근 최대 5개. 각 기록은 아래에서 설명해요. |
| links.payments | HTTPS URL | null | 설정된 API 오리진에서 인증 후 조회하는 이 수단의 페이지별 기록. |
정확한 금액: methods[].amounts
| 필드 | 유형 | 의미 |
|---|---|---|
| expected_amount | decimal string | 스프레드와 올림 적용 후의 전체 고정 견적. |
| received_amount / confirmed_amount | decimal strings | 유효한 감지 자금 / 이 수단의 확인 또는 최종 확정 정책을 충족한 자금. |
| unconfirmed_amount | decimal string | max(received - confirmed, 0). 추가로 보내야 할 금액이 아니에요. |
| minimum_payment_amount | decimal string | 허용 오차 적용 후 수락 기준. 전체 견적보다 낮을 수 있어요. |
| remaining_amount | decimal string | max(minimum accepted - received, 0). 수락 기준에 도달하려면 필요한 추가 금액이며 확인 진행 상태가 아니에요. |
| remaining_to_full_amount | decimal string | max(full quote - received, 0), 허용 오차를 무시해요. |
| overpaid_amount | decimal string | max(received - full quote, 0). 자동 환불을 허용하는 것은 아니에요. |
| Every amount's *_atomic companion | integer string | 정확한 최소 단위 표현. 십진수나 정수 라이브러리를 사용하고 돈에는 부동소수점이나 JavaScript Number를 쓰지 마세요. |
확인 정책: methods[].acceptance
| 필드 | 유형 | 의미 |
|---|---|---|
| finality_mode / required_confirmations | string / integer | 고정 확인 횟수 또는 최종 확정 정책. 확인 0회는 판매자 정책이 명시적으로 허용한 것이며 보편적인 네트워크 확정이 아니에요. |
| observed_confirmations | integer | null | 최신 전송만이 아닌 유효 관찰 기록의 최솟값. Lightning 또는 유효 기록이 없으면 null. |
| underpayment_tolerance_percent | decimal string | 수단의 실제 허용 오차. 청구서의 온체인 허용 오차가 0이 아니어도 Lightning은 0을 사용해요. |
환율: methods[].quote 및 market_rate_at_event
| 필드 | 유형 | 의미 |
|---|---|---|
| quote.effective_rate / units / currency / symbol | strings | 스프레드를 포함한 고정 asset_per_invoice_currency 환율. 통화와 기호가 방향을 명시해요. |
| quote.exchange_rate_spread_percent / quote_expires_at | decimal string / timestamp | 고정 스프레드와 견적 기한. 현재 스토어 설정으로 바꾸지 않아요. |
| quote.reference_rate / unrounded_payment_amount / rounding_adjustment | decimal string | null | 스프레드 전 기준값, 올림 전 결제 금액, 자산 단위로 표시한 상향 조정액. |
| quote.pricing_provider / asset_provider / pricing_fetched_at / asset_fetched_at | string or timestamp | null | 원래 통화·자산 가격의 출처와 시간. API 키나 제공업체 인증 정보는 포함하지 않아요. |
| quote.provenance_available / rounding | boolean / string | 출처 스냅샷이 없는 이전 청구서는 false. 올림을 적용해요. |
| market_rate_at_event | object | null | 이벤트 생성 시 참고용 캐시 시장 스냅샷. 없는 데이터는 null이며 청구서 금액을 바꾸거나 네트워크 조회를 기다리며 지연하지 않아요. |
| market_rate_at_event.rate / units / currency / symbol | strings | quote와 같은 명시적 방향의 스프레드 전 시장 환율. |
| market_rate_at_event.observed_at / as_of / pricing_fetched_at / asset_fetched_at | timestamps | 이벤트 스냅샷 시간 / 두 출처 중 오래된 시간 / 각 출처 시간. |
| market_rate_at_event.pricing_provider / asset_provider | strings | 설정된 사용자 지정 토큰 가격을 포함한 캐시 통화·자산 출처. |
| market_rate_at_event.stale / is_fixed / uses_reference_proxy / reference_currency | booleans / string | 캐시가 오래됐는지, 토큰 가격이 고정인지, USD 기준에 스테이블코인 대용값을 쓰는지 표시해요. 기준 통화는 USD예요. 오래된 데이터는 참고용이며 새 견적이 아니에요. |
전송 기록: methods[].payments[] 및 GET …/payments
| 필드 | 유형 | 의미 |
|---|---|---|
| payment_id / payment_method_id | UUID | 관찰 식별자 / 상위 인텐트 식별자. 기록 중복 제거에는 payment_id를 사용하세요. |
| transaction_id / payment_hash / event_index | string | null / integer | 온체인 해시와 전송·로그·출력 인덱스 또는 Lightning 해시. Lightning에는 거래나 탐색기 링크가 없어요. |
| payment_rail / chain_slug / network / asset_id / asset_key / caip_asset_id / symbol / asset_decimals | strings / UUID / integer | 상위 수단과 같은 자산·네트워크 식별자. |
| amount / amount_atomic | decimal / integer strings | 이 전송의 정확한 값이며 법정화폐 환산이 아니에요. |
| status / counts_towards_received | string / boolean | detected, confirming, final은 집계하고 reorged, replaced, invalid는 제외해요. 무효화된 기록도 대사를 위해 보관하세요. |
| confirmations / block_height | integer | null | 관찰 기록의 블록 데이터. Lightning의 confirmations는 null. |
| observed_at / chain_time / finalized_at | timestamp | null | 로컬 최초 관찰 시간, 제공되는 경우 신뢰할 수 있는 체인 시간, 정책상 최종 확정에 도달한 시간. |
| explorer_name / explorer_url | string | null | 지원되는 경우 검증된 공개 블록 탐색기 참조. |
판매자 버전 5.13.3은 검증된 내부 가스 충전 전송을 고객 결제 총액, payment_info, 청구서 결제 API, 환불 한도, payment.received 이벤트에서 제외해요. 체인·자금 관리 기록은 지갑 회계에 계속 사용할 수 있어요. 일반 전송과 실제 초과 결제는 여전히 집계해요. 기존 서명된 콜백 본문은 다시 쓰지 않아요. 과거 정산이 고객 자금 대신 내부 충전에 의존했다면 대사에서 reason_code가 internal_gas_funding_excluded인 invoice.invalid를 보내니 다시 이행하지 말고 검토하세요.
판매자 버전 4.1.0은 원래 필드 9개를 옮기거나 바꾸지 않고 payload_version 2를 추가해요. 이전에 대기열에 들어간 이벤트는 원본 본문을 유지하며 payload_version이 없을 수 있어요. event_id, event_type, 프로젝트·스토어 ID는 이제 서명된 본문 안에 있고 전송 이벤트·전달 헤더에는 여전히 서명이 없어요.
payment_info는 관찰된 결제를 설명하며 제공하는 모든 결제 옵션은 아니에요. 감지 전에는 active_payment_method_id가 null이고 methods는 비어 있어요. 활성 수단이 null이 된 뒤에도 재구성·무효 관찰 기록이 methods에 남을 수 있어요. 서로 다른 자산이나 네트워크의 금액을 합치지 마세요.
모든 금액, 최소 단위 정수, 환율, 비율은 문자열이에요. received_amount는 확인 대기 중인 유효 자금을 포함하고 confirmed_amount는 해당 수단의 최종 확정 정책을 충족해요. remaining_amount는 max(minimum_payment_amount - received_amount, 0), remaining_to_full_amount는 max(expected_amount - received_amount, 0)이에요. 예: 예상 100 USDC, 수신 99, 허용 오차 1%이면 remaining_amount는 0, remaining_to_full_amount는 1이에요. 최종 확정은 여전히 필요해요.
quote는 청구서 통화 1단위당 자산 단위 수인 고정 계산값이에요. 스프레드 적용 후 올림해요. 정확한 결제 비교에는 expected_amount_atomic을 쓰세요. 표시 환율만으로는 올림을 재현하지 못할 수 있어요. 출처를 저장하지 않은 이전 청구서는 출처·기준·올림 필드가 null이고 provenance_available이 false이며 오늘 데이터를 과거 견적처럼 표시하지 않아요.
market_rate_at_event는 스프레드 전 참고용 캐시 데이터이며 이벤트 생성 시 고정돼요. 출처 시간, 오래됨, 기준 대용값 표시가 있고 사용 가능한 캐시 쌍이 없으면 null이에요. 실시간 환율 요청으로 알림을 막지 않으며 이 시장 값은 지불할 금액을 바꾸지 않아요. 고정 사용자 지정 토큰에는 is_fixed 표시를 하고, DEX 토큰은 같은 기호의 토큰이 아닌 프로젝트별 출처를 사용해요.
최상위 paid_chain, paid_asset, paid_payment_method_id, settlement_exchange_rate(4.1.2+)는 정산 후 검증된 최종 수단을 식별하며 선택한 결제 옵션이나 다른 수단의 합계가 아니에요. 정산 전, 무효화 후, 스냅샷 없는 과거 정산, 정책상 최종 확정 자금 없이 수동 승인한 경우 요약 필드는 null이에요. 기호는 표시용이니 수단 ID로 정확한 네트워크·자산·컨트랙트를 확인하세요.
판매자 버전 5.0.1은 paid_asset 단위의 정확한 십진수 문자열 paid_asset_amount와 paid_asset_amount_received를 추가하며 payload_version은 2로 유지돼요. paid_asset_amount는 스프레드와 올림을 포함한 전체 고정 견적이며 허용 오차 기준이나 남은 잔액이 아니에요. paid_asset_amount_received는 정산 시 최종 수단의 유효 수신 총액으로 확인 대기 자금과 허용된 미달·초과 금액도 포함해요. 예: 견적 100 USDC, 수신 99를 허용 오차로 승인했다면 99와 99가 아닌 100과 99예요. 둘 다 정산 스냅샷에 고정돼요. 이벤트별 수신은 payment_info.methods[].amounts, 현재 기록은 결제 API를 사용하세요. 적격 스냅샷이 없거나 5.0.1 이전 스냅샷이면 null이며 이전 대기 이벤트 본문은 바뀌지 않아요. 회계용 정확한 십진수 문자열을 부동소수점으로 바꾸지 마세요.
settlement_exchange_rate는 정산과 함께 저장한 스프레드 전 캐시 시장 값이며 고정 청구서 견적이나 체결된 거래소 거래가 아니에요. 구조는 market_rate_at_event와 같아요. EUR/USDC의 asset_per_invoice_currency가 1.17이면 1 EUR = 1.17 USDC예요. 출처 시간과 오래됨·고정·대용값 표시로 품질을 알려줘요. 쌍이 없으면 환율은 null이지만 검증된 수단의 paid_* 필드는 남아요. 지불 금액을 바꾸거나 실시간 제공업체 호출을 기다리지 않아요. 같은 수단의 후속 결제, 재시도, 재전송은 저장된 null 환율을 포함한 스냅샷을 바꾸지 못해요. 실제 재정산이나 정산 수단 변경은 새 스냅샷을 만들어요. observed_at은 캡처 시간을 나타내고 settled_at은 최초 정산 시간을 유지할 수 있어요. 이전 이벤트 본문은 그대로예요.
관찰 수단 최대 8개와 수단별 최근 결제 관찰 최대 5개를 개수·잘림 표시와 함께 포함해요. 페이로드 크기 제한으로 배열이 더 줄어들 수 있어요. 관찰 하나는 전송·로그·UTXO 출력 하나이며 반드시 고유 거래 해시 하나는 아니에요. GET /v1/projects/YOUR_PROJECT_ID/invoices/{invoice_id}/payments에 payment_method_id, limit, offset을 사용해 전체 현재 기록을 조회하세요. 청구서 상세에는 모든 견적 수단과 quote_details가 남아요. API 링크에는 설정한 호스트와 인증 정보가 필요하며 콜백이 제공한 임의 URL로 Bearer 토큰을 전달하지 마세요.
Lightning은 transaction_id 대신 payment_hash를 사용해요. 수신 주소, 탐색기, 관찰된 확인 수는 null이에요. 정확한 BTC 금액은 소수 11자리(밀리사토시)이며 실제 허용 오차는 0이에요. BOLT11 결제 프리이미지, 지갑 키, 서명 비밀 키, 제공업체 인증 정보는 포함하지 않아요. 고객·메타데이터 필드는 판매자 응답과 서명된 콜백에만 넣고 공개 결제 화면에는 넣지 마세요. 메타데이터에 인증 정보를 넣지 마세요.
안전하게 수신하기
- 파싱 전에 맞는 비밀 키로 정확한 원본 본문을 검증하세요. 스토어 → IPN은 사용자 지정 ipn_url 전송을 포함한 IPN 비밀 키를 제공해요. 스토어 → 웹훅의 각 엔드포인트에는 자체 비밀 키가 있어요. 둘 다 API 토큰이 아니며 하나를 교체해도 나머지는 바뀌지 않아요.
- 서명된 타임스탬프를 확인하고(SDK 기본 허용 범위는 앞뒤 5분), 서명된 프로젝트·스토어 ID가 있으면 수신기 설정과 대조하세요. HTTP 2xx를 반환하기 전에 영구 대기열에 저장하세요. 이벤트별 처리에서는 v2 event_id에 서명이 있어요. 헤더는 서명되지 않아 헤더 ID만으로는 재전송 공격을 막지 못해요. 주문 상태 수신함은 invoice_id와 sequence로 중복 제거하고 전체 v2 본문이 아닌 원래 청구서 상태 필드를 비교하세요. 다른 이벤트 유형·ID가 같은 리비전을 공유할 수 있어요.
- 작업자에서 임의 콜백 링크가 아닌 설정된 API 오리진으로 현재 청구서를 조회하세요. 저장된 주문, 프로젝트·스토어, 금액, 통화를 대조하고 현재 settled 상태를 요구하며 수동 승인·예외 정책을 적용하세요. 이벤트 중복 제거와 별도로 주문을 잠그고 데이터베이스 트랜잭션에서 한 번만 이행하세요.
- 새 sequence 위에 오래된 값을 적용하지 마세요. 다른 이벤트가 같은 리비전을 공유할 수 있으므로 리비전 중복 제거와 invoice.settled 전용 필터를 함께 쓰지 마세요. 재개·대사로 상태가 바뀔 수 있어 업데이트 순서는 고정 상태 순위가 아닌 sequence로 정해요. 취소는 검토용으로 기록하고 다시 이행하지 마세요.
수신기 예제: PHP · Python · Node.js / TypeScript.
서명 검증 및 전송 규칙
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWhollySignature(rawBody, header, signingSecret, toleranceSeconds = 300) {
const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || "");
if (!match) return false;
const timestamp = Number(match[1]);
if (!Number.isSafeInteger(timestamp)) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > toleranceSeconds) return false;
// rawBody must be the exact request Buffer, before JSON parsing.
const expected = createHmac("sha256", signingSecret)
.update(String(timestamp))
.update(".")
.update(rawBody)
.digest();
const presented = Buffer.from(match[2], "hex");
return timingSafeEqual(expected, presented);
}| 전송 규칙 | 상세 |
|---|---|
| 헤더 | Wholly-Signature, Wholly-Event-Id, Wholly-Delivery-Id. Content-Type은 application/json이에요. |
| 서명 | <unix timestamp>.<exact raw body>에 대한 HMAC-SHA256. 헤더 형식은 t=<timestamp>,v1=<64 lowercase hex>예요. |
| 성공 | 모든 HTTP 2xx 응답. 리디렉션은 따라가지 않으며 2xx가 아닌 응답은 실패예요. |
| 시간 초과 | 연결 제한 시간 5초, 전체 요청 제한 시간 10초. |
| 재시도 일정 | 재시도 가능한 실패는 최대 8번 시도해요. 즉시 한 번, 이후 이전 시도가 끝난 뒤 10초, 1분, 5분, 15분, 1시간, 6시간, 24시간을 기다려요. IPN은 자동 재시도하고 웹훅은 엔드포인트별로 자동 재시도를 끌 수 있어요. |
| 대상 안전 | 공개 HTTPS만 허용해요. 전송 시 DNS를 재검증하고 고정하며 로컬·사설·예약 대상은 거부해요. |
| 이벤트 보관 | 알림 이벤트 페이로드와 전송 기록은 90일 보관 예정이며 상세 데이터는 크기가 제한된 묶음으로 삭제해요. |
| 중복 제거 | 설정한 프로젝트 범위에 서명된 invoice_id와 sequence를 영구 저장하세요. Wholly-Event-Id는 이벤트, Wholly-Delivery-Id는 전송 기록을 식별해요. 재시도는 재사용하고 수동 재전송은 새 기록을 만들어요. 두 ID 헤더 모두 서명되지 않아요. |
| 이벤트 이름 | 버전 2는 본문의 event_id와 event_type에 서명해요. 이전 대기 이벤트에는 둘 다 없어요. 다른 이벤트 유형이 같은 청구서 sequence를 공유할 수 있어요. 리비전으로 청구서 상태를 대조하거나 서명된 event_id로 개별 이벤트 중복을 제거하세요. |
| 비밀 키 교체 | 교체에는 중복 허용 기간이나 버전 헤더가 없으며 대기 중, 재시도, 수동 전송의 서명이 즉시 바뀌어요. |
| 일시 중지된 전송 | 처리 크레딧이 부족하면 재시도를 포함한 IPN·웹훅이 멈춰요. 입금은 계속되고 충전 후 페이로드 보관 기간 안의 대기 알림이 재개돼요. |
AI 도우미 · MCP
도우미를 판매자 설치에 연결하세요.
판매자 버전 5.0.0에는 설정된 API 도메인에서 직접 켤 수 있는 MCP 서버가 포함돼요. 공유 Wholly Crypto 릴레이가 아니라 내 설치 안에서 실행돼요.
- 설정 → API 접근을 여세요. 전용 인증 정보를 만들고 도우미에게 필요한 프로젝트만 지정해 읽기 전용부터 시작하세요. 운영자가 호스팅하는 계정은 운영자가 먼저 설치의 MCP 서비스를 켜야 해요. 자신의 인증 정보와 허용 사항만 관리할 수 있어요.
- AI 연결 · MCP에서 MCP를 켜고 인증 정보를 선택해 MCP 접근을 저장하세요. 기존 인증 정보는 명시적으로 켜기 전에는 MCP 접근 권한이 없어요.
- MCP 서버 URL을 클라이언트의 원격 HTTP 서버 설정에 복사하세요. OAuth는 판매자 콘솔에 로그인해 클라이언트 이름과 반환 주소를 확인하고 인증 정보를 선택한 뒤 승인해요. 기존 Basic Auth와 TOTP 보호도 적용돼요.
- 청구서 생성에는 읽기·쓰기 인증 정보, MCP 정책의 읽기 + 청구서 생성, mcp:invoice:create OAuth 범위, 명시적 승인이 추가로 필요해요. 승인 후 인증 정보에 추가한 프로젝트는 기존 OAuth 연결에 자동 부여되지 않아요.
{
"mcpServers": {
"whollycrypto": {
"url": "https://api.example.com/mcp"
}
}
}| 도구 | 접근 | 용도 |
|---|---|---|
| list_projects | 조회 | 연결에 지정된 활성 프로젝트. limit/offset 페이지 구분. |
| list_stores | 조회 | project_id 안의 스토어, ID, 활성 상태. limit/offset 페이지 구분. |
| list_payment_methods | 조회 | project_id + store_id에 설정된 체인, 토큰, Lightning 수단. |
| get_wallet_balances | 조회 | 최신성·가용성 필드를 포함한 수신 주소와 캐시 잔액. 지갑 비밀 정보는 포함하지 않아요. |
| list_invoices | 조회 | 프로젝트 청구서. 스토어·상태·검색으로 필터링하고 limit/offset으로 페이지를 나눠요. |
| get_invoice | 조회 | project_id + invoice_id로 전체 청구서 상세와 결제 링크 조회. |
| get_delivery_history | 조회 | 스토어 IPN·웹훅 상태, 시도 횟수, HTTP 결과. 선택적 invoice_id/kind 필터. 비밀 정보와 콜백 본문은 없어요. |
| convert_amount | 조회 | from, to, 십진수 문자열 amount를 사용하는 캐시 참고 환산. 청구서 견적이 아니에요. |
| create_invoice | 명시적 쓰기 | project_id, store_id, idempotency_key, invoice(기존 청구서 생성 본문). invoice.payment_methods는 활성 스토어 수단을 필터링해요. 5.4.0+는 비활성·미허용 선택지를 무시하고 일치 항목이 없으면 스토어 기본값을 써요. 체인만 지정하면 활성 수락 자산을 모두 선택해요. 체인 범위의 asset_tickers는 5.3.0부터 지원해요. 일반 청구서 응답을 반환해요. |
프로토콜, OAuth, 보안
HTTPS에서 Streamable HTTP를 사용하세요. 제공되는 프로토콜 버전을 협상하고 이후 POST에 MCP-Protocol-Version을 포함하세요. Content-Type: application/json과 Accept: application/json, text/event-stream을 보내세요. 응답은 종료되는 JSON이며 재연결에 MCP 세션 ID는 필요 없어요.
OAuth는 짧은 접근 토큰(15분), 일회용 S256 PKCE 코드(5분), 교체되는 갱신 토큰(연결 수명 30일)을 사용해요. 사용한 갱신 토큰을 재사용하면 연결이 철회돼요. 만료, 인증 정보 교체, 정책 변경, 기본 API 도메인 변경 후 다시 연결하세요.
OAuth 검색은 MCP가 켜져 있을 때만 공개돼요. resource 매개변수는 /mcp를 포함해 검색에서 반환한 기본 URL과 같아야 해요. 동적 등록은 지원하지만 원격 클라이언트 ID 메타데이터 문서와 클라이언트 비밀 키는 지원하지 않아요.
사용자 지정 Authorization 헤더를 지원하는 클라이언트는 MCP가 활성화된 판매자 API 토큰을 Bearer로 쓸 수 있어요. 별도 REST 권한을 유지하므로 MCP 전용 연결에는 OAuth를 권장해요. 인증 정보를 채팅, URL, 도구 인수, 소스 관리에 넣지 마세요.
MCP는 인증 정보의 분당 REST 할당량과 정확한 원본 IP 제한을 공유하고 API 호스트 IP 제한도 적용돼요. OAuth는 허용 목록을 우회하지 않아요. 원격 AI 클라이언트에는 문서화된 송신 IP를 허용하거나 의도적으로 이 제한을 꺼 두세요. MCP/OAuth 경로에는 웹 보안 인증이나 캐시를 적용하지 마세요.
HTTP 오류: 401은 인증 필요, 403은 오리진·IP·권한 거부, 404는 MCP 비활성 또는 잘못된 호스트, 405는 POST 필요, 413은 32 KiB 본문 제한 초과, 429에는 Retry-After가 있어요. JSON-RPC 오류는 error.code를 사용하고 도구 실패는 HTTP 200이어도 result.isError=true예요. 성공 결과에는 content와 structuredContent가 있어요.
목록은 기본 25행, 최대 100행이며 offset은 1000000까지예요. 도구 응답은 최대 2 MiB예요. 만료된 허용, 인증 요청, 요청 제한 버킷은 자동 정리하고 설정에는 활성 OAuth 연결을 최대 100개 표시해요.
비활성 프로젝트·스토어는 MCP로 작업할 수 없어요. 연결에서 스토어 활성 상태를 조회할 수는 있지만 결제 수단·전송 내역 조회나 청구서 생성에는 활성 스토어가 필요해요. 일반 콘솔 프로젝트 사용자는 MCP를 관리할 수 없어요.
새 청구서에는 새 idempotency_key를 쓰고 시간 초과 후에는 같은 인증 정보, 키, 동일한 invoice 객체로 재시도하세요. 십진수 금액, 스프레드, 허용 오차, 확인 횟수, 결제 화면 모양은 REST 청구서 규약을 따라요. MCP는 판매자 결제나 크레딧 정책을 우회하지 않아요.
초기 도구는 개인 키·복구 구문 공개, 송금·자금 모으기, 환불, 콜백 재전송, 결제 수단 변경, 계정·도메인 편집, 청구 관리를 할 수 없어요. 청구서 설명, 고객 필드, 메타데이터는 에이전트 지시가 아닌 신뢰할 수 없는 데이터로 취급하세요. 연결된 AI 제공업체는 읽기를 허용한 데이터를 받아요.
| 메서드 | 경로 | 규약 |
|---|---|---|
| POST | /mcp | 인증된 JSON-RPC: initialize, ping, tools/list, tools/call. 알림 요청은 202를 반환하며 배치는 거부해요. |
| GET / DELETE | /mcp | 인증 후 405: 종료되는 JSON 응답이며 별도 SSE 스트림이나 서버 측 MCP 세션이 없어요. |
| GET | /.well-known/oauth-protected-resource/mcp | 기본 리소스 URL과 인증 서버 검색. /.well-known/oauth-protected-resource에서도 제공해요. |
| GET | /.well-known/oauth-authorization-server | OAuth 엔드포인트, authorization_code/refresh_token, S256 PKCE, 지원 범위. |
| POST | /mcp/oauth/register | 공개 클라이언트 등록: client_name과 정확한 redirect_uris. HTTPS 또는 루프백 HTTP만 허용해요. 클라이언트 비밀 키나 원격 메타데이터 조회는 없어요. |
| GET | /mcp/oauth/authorize | client_id, redirect_uri, response_type=code, resource, code_challenge, code_challenge_method=S256, 선택적 scope/state. 콘솔 승인으로 이동해요. |
| POST | /mcp/oauth/token | 폼 인코딩된 authorization_code + code + code_verifier + redirect_uri 또는 refresh_token + refresh_token. 항상 client_id와 resource를 포함하세요. |
| POST | /mcp/oauth/revoke | 폼 인코딩된 client_id와 token. 일치하는 접근·갱신 토큰 연결을 철회해요. |
직접 도구 요청 예제
먼저 MCP 클라이언트로 초기화하고 프로토콜을 협상하세요. 이는 이후 요청의 예시예요.
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/mcp" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'MCP-Protocol-Version: 2025-11-25' \
--header 'Accept: application/json, text/event-stream' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_invoices",
"arguments": {
"project_id": "YOUR_PROJECT_ID",
"limit": 10
}
}
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_invoices",
"arguments": {
"project_id": "YOUR_PROJECT_ID",
"limit": 10
}
}
}`;
const response = await fetch("https://api.example.com/mcp", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"MCP-Protocol-Version": "2025-11-25",
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_invoices",
"arguments": {
"project_id": "YOUR_PROJECT_ID",
"limit": 10
}
}
}
JSON;
$ch = curl_init("https://api.example.com/mcp");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "MCP-Protocol-Version: 2025-11-25", "Accept: application/json, text/event-stream", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"MCP-Protocol-Version": "2025-11-25",
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_invoices",
"arguments": {
"project_id": "YOUR_PROJECT_ID",
"limit": 10
}
}
}""".encode("utf-8")
request = Request("https://api.example.com/mcp",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))운영자 API
범위가 제한된 별도 서버 키로 호스팅 판매자를 구성하세요.
여러 사업을 호스팅하고 api.example.com/v1/operator로 설정을 자동화하세요. 7.4.0부터 운영자 모드에서만 사용할 수 있어요. 일반 판매자 API는 그대로예요.
- 운영자 → 설정 → 운영자 API를 열고 켜세요. 기본은 꺼짐이에요. 필요한 권한과 호스팅 판매자만 지정한 별도 인증 정보를 만드세요.
- wc_operator_ 키는 서버에 보관하세요. 운영자 패널 호스트 이름이나 판매자 키가 아닌 API 호스트 이름을 사용하세요.
- 모든 운영자 POST 전에 Idempotency-Key와 정확한 요청 본문을 영구 저장하세요. 결과가 불확실하면 계정을 다시 읽어 확인하세요. 재시도만을 위해 키를 바꾸지 마세요.
- onboarding: direct와 비밀번호 또는 onboarding: invitation과 비밀번호 없이 판매자를 만드세요. 그다음 프로젝트·스토어를 만들고 결제 연동용 프로젝트 범위 판매자 키를 발급하세요.
| 범위 | 접근 |
|---|---|
| merchants.read / merchants.write | 호스팅 판매자 목록·조회 및 생성·업데이트. |
| users.read / users.write / users.security | 사용자 조회·생성·업데이트. 비밀번호 변경이나 세션 철회는 별도예요. 운영자 관리자는 만들지 않아요. |
| invitations.read / invitations.write | 일회용 초대·재설정 링크의 목록·조회, 생성, 교체, 철회. 새 사용자는 users.write, 재설정은 users.security도 필요해요. |
| credits.read / credits.write / fees.write | 잔액·원장 조회, 로컬 크레딧 지급·정정, 향후 수수료 설정. 0이 아닌 시작 크레딧에는 credits.write가 필요해요. |
| topups.read / topups.write | 호스팅 판매자의 크레딧 결제 요청 조회·생성. 어떤 API 작업도 결제 완료로 표시할 수 없어요. |
| projects.read / projects.write / reports.read | 판매자 프로젝트, 스토어, 모양, 결제 설정 구성. 청구서, 지갑 잔액, 재무 보고서 조회. |
| merchant_credentials.read / merchant_credentials.write | 일반 범위 제한 판매자 키 관리. 발급 후 독립적으로 작동하는 강력한 권한이에요. |
| events.read / webhooks.write / audit.read / health.read | 수명 주기 기록 조회, 서명된 수명 주기 콜백 설정, 감사·기능·노드 상태 조회. |
가입, 크레딧, 권한, 안전한 재시도
| 주제 | 규칙 |
|---|---|
| 인증 정보 | 선택적 만료와 정확한 IPv4/IPv6 허용 목록. 기본 분당 60요청이며 1~600으로 설정할 수 있어요. 요청마다 발급 관리자와 현재 범위를 확인해요. HTTP 429에는 Retry-After가 있어요. |
| 격리 | 키는 지정된 호스팅 판매자만 접근해요. 판매자 생성과 설치 전체 보고서 조회에는 전체 판매자 접근이 필요해요. 운영자 자신의 사업은 제외돼요. |
| 첫 로그인 | 직접 만든 계정은 호스트가 지갑 키에 접근할 수 있음을 확인해요. require_password_change는 첫 로그인 비밀번호 변경을 요구해요. 초대 수락에는 명시적 자산 보관 동의 후 일반 로그인이 필요해요. Basic Auth와 기존 TOTP는 유지돼요. |
| 초대 | 새 사용자 링크는 48시간, 비밀번호 재설정 링크는 1시간 유효해요. 토큰은 일회용이며 재발급하면 이전 링크가 철회돼요. SMTP 수락이 받은편지함 도착을 보장하지 않으니 email_delivery를 확인하세요. |
| 안전한 재시도 | 모든 운영자 POST에는 16~128자 키가 필요해요. 문자, 숫자, -, _, .을 사용할 수 있어요. 같은 키와 정확히 같은 URL·본문은 저장된 결과를 반환하고 바이트가 다르면 409예요. 재시도 응답에는 비밀 정보·링크를 제외하므로 필요하면 명시적인 새 작업으로 교체·재발급하세요. |
| 불확실한 결과 | operator_request_in_progress는 작업이 진행 중이거나 결과 기록 전 중단됐다는 뜻이에요. 리소스와 감사를 확인하고 무작정 새 키를 제출하지 마세요. 완료 결과는 30일 후 압축되지만 이전 키는 다시 실행할 수 없어요. |
| 크레딧 및 수수료 | 최대 소수 6자리의 십진수 문자열을 사용해요. starting_credit은 일회성 로컬 지급이에요. 조정에는 부호 있는 금액, 메모, request_id와 HTTP 재시도 키가 필요해요. fee_bps=100은 1%이며 변경은 향후 청구서에 적용돼요. 지급으로 설치 자체의 선불 잔액이 충전되지는 않아요. |
| 일시 중지 | enabled=false는 호스팅 계정을 끄고 콘솔 세션을 철회해요. payments_paused=true는 새 청구서를 막아요. 기존 결제 모니터링은 계속돼요. 프로젝트·스토어 생성과 자동화에는 설치의 크레딧 정책이 유지돼요. |
| 제공하지 않는 기능 | 지갑 비밀 정보, 서명, 전송, 환불, 영구 삭제, TOTP 재설정, 도메인 변경, 서버 수준 설정은 없어요. 일반 청구서 요청은 여전히 판매자 키와 판매자 API를 사용해요. |
운영자 수명 주기 웹훅
| 이벤트 | 데이터 |
|---|---|
| merchant.created / merchant.updated | merchant_id, enabled, payments_paused, fee_bps. |
| user.created / user.updated | merchant_id, user_id, enabled. 업데이트 이벤트는 이메일, 활성 상태, 관리자 역할 변경을 포함해요. |
| invitation.accepted / password_reset.completed | merchant_id, user_id, invitation_id. |
| topup.settled / credit.balance_changed | merchant_id, ledger_id, kind, amount, balance. 대사 시 판매자 크레딧 통화나 원장 상세를 읽으세요. |
운영자 수명 주기 이벤트는 청구서 IPN·스토어 웹훅과 별개예요. 구독은 생성한 운영자 인증 정보에 속하며 키당 엔드포인트는 최대 10개예요. 향후 일치하는 이벤트만 대기열에 넣으므로 보관 기록은 GET /events로 조회하세요.
본문에는 event_id, event_type, merchant_id, occurred_at, data가 있어요. 엔드포인트에서 한 번 표시한 signing_secret으로 정확한 원본 본문의 Wholly-Signature를 검증하세요: HMAC-SHA256(secret, timestamp + '.' + raw_body), 헤더 t=...,v1=.... 짧은 타임스탬프 허용 범위를 적용하세요.
SDK의 청구서 알림 파서가 아닌 일반 서명 검증기를 사용하세요. 그다음 merchant_id와 event_type을 검증하고 트랜잭션 안에서 event_id를 영구 저장·중복 제거한 뒤 확실히 수락했을 때만 2xx를 반환하세요. Wholly-Event-Id는 서명된 본문과 같아야 해요. 서명되지 않은 헤더를 업무 데이터로 취급하지 마세요.
전송은 최소 한 번 방식이며 순서가 바뀔 수 있고 최대 8번 시도해요. 현재 리소스를 읽어 대조하세요. occurred_at은 단조 증가 순번이 아니에요. 전송 전에 범위와 활성·만료 설정을 다시 확인해요. 구독을 끄면 기존 대기 작업은 멈추고 꺼진 동안 새 이벤트는 쌓지 않아요.
이벤트와 전송 기록은 30일 보관해요. 설치 자동화 정책으로 전송이 멈출 수 있어요. GET /webhooks/{id}/deliveries는 결과와 변경 불가능한 페이로드를 보여주며 공개 API로 만료 기록을 강제 전송할 수 없어요.
{
"event_id": "55555555-5555-4555-8555-555555555555",
"event_type": "merchant.created",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"occurred_at": "2026-10-01T12:00:00Z",
"data": {
"merchant_id": "11111111-1111-4111-8111-111111111111",
"enabled": true,
"payments_paused": false,
"fee_bps": 300
}
}오류 및 제한
검증, 할당량, 재시도를 예측 가능하게 처리하세요.
HTTP 상태와 다음을 확인하세요: Content-Type 응답을 파싱하기 전에 확인하세요. 다음 응답이면: 429, 최소한 다음 시간만큼 기다리세요: Retry-After 시간이 지난 뒤 재시도하세요.
| 제한 | 상세 |
|---|---|
| 요청 속도 | 인증 정보별 할당량은 기본 UTC 분당 120회이며 설정 → API에서 1~6000으로 바꿀 수 있어요. 멱등 재시도와 인증 후 권한·검증 실패를 포함한 모든 인증된 v1 읽기·쓰기가 도메인, 프로젝트, 프로세스 전체에서 한도를 공유해요. 잘못된 인증 정보, 콘솔 경로, 공개 결제 화면은 소모하지 않아요. |
| 요청 제한 헤더 | 인증된 v1 응답에는 X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset(다음 UTC 분 경계의 Unix 초)이 있어요. 초과 요청은 JSON 429 rate_limit_exceeded와 정수 초 Retry-After를 반환해요. 최소 그 시간만큼 기다리고 재시도 지연에 무작위 차이를 더하세요. 고정 구간은 분 경계에서 몰린 요청을 허용하며 초당 요청 수 보장이 아니에요. |
| 판매자 요청 본문 | 앱 라우터의 최대 크기는 32 KiB예요. 앞단에서 JSON 오류 응답을 만들기 전에 큰 요청을 거부할 수 있어요. |
| 청구서 목록 | limit은 기본 50, 허용 범위 1~100이고 offset은 0~1,000,000이에요. 검색은 최대 100자예요. 최신순으로 정렬하며 total/has_more 메타데이터를 포함해요. |
| 스토어 수단 | 스토어당 자산을 최대 64개 선택할 수 있어 30개 기본 체인과 한도가 있는 검증 토큰 목록을 담을 수 있어요. 청구서 생성에는 프로젝트 정책, 스캐너 기능, 준비되고 백업된 체인 지갑 요건이 여전히 적용돼요. |
| 토큰 탐색 | 후보 limit은 기본 50이며 1~100을 허용해요. 탐색 결과는 온체인 검증이 성공해야 결제 자산이 돼요. |
| 등록된 프로젝트 토큰 | 프로젝트당 영구 토큰 자산은 최대 20개예요. 이미 등록한 자산은 추가 슬롯 없이 다시 사용할 수 있어요. |
| 멱등성 | 청구서 생성에 필수예요. 공백 없는 표시 가능한 ASCII 1~128자이며 키는 스토어별로 고유해요. 재시도에는 원래 인증 정보와 정확히 같은 원본 본문을 사용해야 해요. |
| 메타데이터 | JSON 객체만 허용하며 인코딩 후 최대 4,096바이트, 중첩 최대 5단계예요. |
| 콜백 | 최대 2,048바이트의 공개 HTTPS URL. 알림 본문은 최대 256 KiB이며 보관 청구서 이벤트 페이로드는 최대 64 KiB로 결제 기록 수에 한도가 있어요. |
| 결제 화면 자료 | QR SVG 응답은 미달 결제로 정확한 잔액이 바뀌므로 private 및 no-store예요. 리비전이 있는 PNG 로고는 공개적으로 1년 캐시되며 변경 불가능해요. |
| API 앞단 | 관리되는 API 업스트림 요청의 읽기 제한 시간은 30초예요. 호출 측은 작업 시간 한도보다 짧은 명시적 제한 시간을 설정하세요. |
| JSON이 아닌 실패 응답 | 잘못된 UUID·쿼리 추출, 잘못된 메서드, 32 KiB 크기 제한은 프레임워크 텍스트나 빈 응답을 반환할 수 있어요. 알 수 없는 /v1 경로는 현재 404 콘솔 HTML을 반환하니 파싱 전에 상태와 Content-Type을 확인하세요. |
오류 레퍼런스
| HTTP | 오류 코드 | 의미 |
|---|---|---|
| 400 | invalid_reconciliation_action | 예외 상태, 이유, 검색, 기록 페이지 필터가 유효하지 않아요. |
| 500 | reconciliation_unavailable | 예외 대기열이나 증거를 불러오지 못했어요. 지연을 늘리며 읽기를 재시도하세요. |
| 402 | billing_required | 새 청구서마다 검증된 연결 크레딧 계정과 유효한 승인이 필요해요. 선불 크레딧 부족은 생성이나 입금을 막지 않고 IPN, 웹훅, 자금 모으기를 멈추며 수수료는 계속 쌓여요. 계정 정지, 만료·무효한 청구 검증, 크레딧 서비스 연결 불가, 미승인 청구서 법정화폐 기준은 생성을 막아요. 수수료는 받은 암호화폐, 스프레드, 초과 입금, 네트워크 수수료가 아닌 원래 법정화폐 청구액을 사용해요. 이 금액과 독립 환산을 결제 화면 생성 전에 등록해요. 장애 중에도 기존 모니터링과 청구서 조회는 계속돼요. 충전 후 일반 보관 기간 내의 대기 알림과 활성 자금 모으기 규칙이 재개돼요. 설정 → 수수료를 확인하고 같은 Idempotency-Key로 실패한 생성을 재시도하세요. |
| 400 | invalid_json | 잘못된 JSON, 알 수 없는 필드, 문서의 요청과 맞지 않는 본문이에요. |
| 400 | idempotency_key_required | 청구서 생성에 Idempotency-Key가 빠졌어요. |
| 400 | invalid_idempotency_key | 키가 비어 있거나 128바이트 초과, 비 ASCII, 공백 또는 제어 바이트를 포함해요. |
| 400 | invalid_payment_request | 검증 필드나 선택한 활성 수단이 실패했어요. 정확한 원인은 error.message와 error.details.payment_methods(PaymentMethodIssue[])를 확인하세요. SDK 2.4.0+에는 안전하고 실행 가능한 예외 요약·도우미가 있고 이전 PHP SDK는 getApiMessage()를 제공해요. |
| 400 | invalid_invoice_status | 목록 상태가 문서의 청구서 상태 6개에 속하지 않아요. |
| 400 | invalid_callback_url | 유효 IPN 대상이 HTTPS, 공개 주소, DNS, SSRF 검증에 실패했어요. |
| 400 | invalid_wallet_request | 지갑·주소 준비 입력이 유효하지 않아요. |
| 400 | invalid_token_asset | 토큰 체인, 후보 쿼리, CoinGecko 식별자, 목록 메타데이터, 컨트랙트·민트 입력이 유효하지 않아요. |
| 401 | authentication_required | Bearer 토큰이 없거나 형식 오류, 비활성, 교체됨, 알 수 없는 상태예요. |
| 403 | source_ip_denied | 인증 정보 IP 제한에 요청의 정확한 공인 출처 주소가 없어요. |
| 403 | source_ip_not_allowed | 호스트 이름의 원본 IP 제한이 이 클라이언트를 제외해요. 관리자는 설정 → 시스템에서 활성 호스트 허용 목록을 관리할 수 있고 인증 정보 IP 제한과 함께 적용돼요. |
| 503 | source_access_unavailable | 호스트 이름 접근 검증을 일시적으로 사용할 수 없어요. 나중에 재시도하세요. 검증 실패 시 접근은 차단돼요. |
| 403 / 409 / 500 | merchant_api_access_denied | 권한 실패: 권한·프로젝트 범위는 403, 비활성 프로젝트·스토어는 409, 인증 백엔드 실패는 500일 수 있어요. 운영자 수신 지갑은 운영자 패널 전용이며 이전의 명시적 프로젝트 허용이 있어도 판매자 API 인증 정보나 MCP로 접근할 수 없어요. |
| 403 | project_access_denied | 생성 시 트랜잭션 재검사에서 인증 정보가 더 이상 프로젝트에 접근할 수 없음을 확인했어요. |
| 404 | invoice_not_found | 허용된 프로젝트에 해당 공개 ID의 청구서가 없거나 결제 화면에서 노출할 수 없어요. |
| 404 | payment_resource_not_found | 청구서 준비에 필요한 프로젝트, 스토어, 자산, 지갑이 더 이상 없어요. |
| 404 | token_candidate_not_found | 프로젝트를 사용할 수 없거나 현재 일치한 탐색 목록에 토큰이 더 이상 없어요. |
| 409 | idempotency_conflict | 스토어 범위 키가 이미 있으며 인증 정보나 정확한 원본 요청 바이트가 달라요. |
| 409 | store_unavailable | 프로젝트·스토어가 비활성 상태이거나 사용할 수 없어요. |
| 409 | no_ready_payment_methods | 준비된 스토어 수단이 없어요. error.message와 error.details.payment_methods에서 chain_slug, asset_ticker, reason_code를 읽으세요. 지갑 백업·활성화, 설치된 어댑터, 가격이 유효해야 해요. 6.0.6부터 스캐너 대기 시간, 실패·오래된 상태 검사, 부족한 제공업체 정족수는 생성을 막지 않아요. |
| 409 | payment_method_unavailable | 생성 시 원자적 재검사에서 선택한 수단을 사용할 수 없게 됐어요. |
| 409 | store_payment_method_not_selected | 스토어가 현재 선택하지 않은 자산에 확인 정책 변경을 요청했어요. |
| 409 | wallet_unavailable | 생성 시 원자적 재검사에서 결제 지갑을 사용할 수 없게 됐어요. |
| 409 | ipn_secret_required | 유효한 IPN URL은 있지만 스토어에 IPN 서명 비밀 키가 없어요. |
| 409 | payment_resource_not_ready | 필요한 결제 자산·지갑이 비활성, 미백업, 공유 계정 활성화 증명 대기, 소진 상태이거나 다른 이유로 준비되지 않았어요. |
| 409 | account_activation_unverified | 설정한 수의 정상 메인넷 엔드포인트(기본 2개, 선택적으로 1개)로 XRP Ledger 또는 Stellar 계정 활성화를 증명하지 못했어요. 정확한 계정에 자금을 넣고 다시 검증하세요. |
| 400 | invalid_monero_wallet_rpc | HTTPS 엔드포인트, 정확한 메인넷 기본 주소, 라벨, 전체 Digest/Basic/header 인증 입력이 유효하지 않아요. |
| 404 | monero_wallet_rpc_not_found | 프로젝트 범위의 Monero wallet-RPC 연결이 없어요. |
| 409 | monero_wallet_rpc_not_ready | Monero 자산, 데몬 2개 정족수, 변경 불가 연결, 명시적 백업·조회 전용 확인이 준비되지 않았어요. |
| 409 | monero_wallet_rpc_unavailable | 청구서 생성에는 유효한 서버 인증 정보와 함께 활성화·검증·확인된 프로젝트 Monero wallet-RPC 연결이 필요해요. |
| 503 | lightning_unavailable | 스토어의 유일한 준비된 수단이 Lightning인데 지갑이나 견적을 검증하지 못했어요. 같은 멱등성 키로 재시도하세요. 다른 준비된 온체인 수단이 있으면 사용 불가 Lightning 수단만 생략해요. |
| 422 | monero_wallet_rpc_verification_failed | 정확한 지갑, HTTPS 고정, 동기화, 메인넷 데몬 정족수, 게이트웨이 메서드 거부 증명이 실패했어요. |
| 503 | monero_wallet_rpc_failed | 외부 조회 전용 wallet-RPC가 청구서 하위 주소를 안전하게 만들고 다시 읽지 못했어요. 대체 주소를 임의로 만들지 않아요. |
| 409 | token_chain_not_ready | 기본 체인 자산이 꺼졌거나 검증 중 탐색 매핑이 바뀌었거나 프로젝트가 현재 등록 토큰 한도 20개에 도달했어요. |
| 503 | dex_price_unavailable | DEX 제공업체가 사용 불가, 혼잡, 요청 제한, 오래된 응답, 잘못된 데이터 상태예요. 1분 후 재시도하세요. 고정 가격은 계속 사용할 수 있어요. |
| 422 | invalid_dex_price | 가격 모드 조합이 잘못됐거나 선택한 풀이 정확한 컨트랙트의 적격 가격을 제공하지 못해요. 다른 풀이나 고정 USD 가격을 선택하세요. |
| 422 | token_verification_failed | 모든 적격 노드가 체인 식별, 컨트랙트 코드, 소수 자릿수, 잔액 조회, 민트 검증에 실패했어요. |
| 422 | invalid_store_confirmation_policy | 스토어 재정의가 이 최종 확정 모드에서 사용 불가이거나 반환된 체인별 범위를 벗어나거나 지원하지 않는 확인 0회 수락을 요청했어요. |
| 409 | invoice_not_payable | 결제 청구서가 최종 상태이거나 결제 기한이 지났어요. |
| 409 | invoice_payment_method_locked | 유효 결제가 이미 다른 자산을 선택했어요. active_payment_method_id로 계속하세요. |
| 409 | payment_method_not_payable | 선택한 수단이 완료됐거나 더 이상 추가 결제를 받지 않아요. |
| 422 | payment_qr_unavailable | 결제 요청이 너무 커 SVG QR 이미지로 인코딩할 수 없어요. |
| 503 | payment_rates_unavailable | 준비된 결제 수단에 최신의 신뢰할 수 있는 견적이 없어요. |
| 500 | authentication_unavailable | Bearer 인증에서 저장된 인증 정보를 안전하게 읽거나 검증하지 못했어요. |
| 429 | rate_limit_exceeded | 이 인증 정보는 현재 UTC 분 한도를 다 썼어요. 최소 Retry-After초 기다린 뒤 청구서 생성은 같은 멱등성 키로 재시도하세요. |
| 500 | database_error / internal_error | 일시적인 서버 오류예요. 같은 멱등성 키로 안전하게 재시도하세요. |
API 개요
엔드포인트를 선택해 필드, 예제, 응답을 확인하세요.
청구서
POST청구서 생성/v1/projects/{project_id}/stores/{store_id}/invoicesGET청구서 목록/v1/projects/{project_id}/invoicesGET청구서 조회/v1/projects/{project_id}/invoices/{invoice_id}GET청구서 결제 목록/v1/projects/{project_id}/invoices/{invoice_id}/payments결제 수단
GET프로젝트 결제 자산 목록/v1/projects/{project_id}/payment-assetsPUT프로젝트 자산 정책 업데이트/v1/projects/{project_id}/payment-assets/{asset_id}GET결제 토큰 후보 둘러보기/v1/projects/{project_id}/payment-token-candidatesPOST토큰 검증 및 등록/v1/projects/{project_id}/payment-token-assetsGET사용자 지정 토큰 DEX 풀 찾기/v1/projects/{project_id}/payment-token-dex-poolsPOST사용자 지정 토큰 추가 또는 가격 재설정/v1/projects/{project_id}/payment-token-assets/customGET스토어 결제 수단 목록/v1/projects/{project_id}/stores/{store_id}/payment-assetsPUT스토어 결제 수단 교체/v1/projects/{project_id}/stores/{store_id}/payment-assetsPUT스토어 확인 정책 설정/v1/projects/{project_id}/stores/{store_id}/payment-assets/{asset_id}/confirmation-policy지갑
GET프로젝트 지갑 및 잔액 목록/v1/projects/{project_id}/wallets대사
GET결제 예외 목록/v1/projects/{project_id}/reconciliationGET대사 증거 조회/v1/projects/{project_id}/reconciliation/{invoice_id}운영자 API
GET기능/v1/operator/capabilitiesGET상태/v1/operator/healthGET판매자 목록/v1/operator/merchantsPOST판매자 생성/v1/operator/merchantsGET판매자 조회/v1/operator/merchants/{merchant_id}POST판매자 업데이트/v1/operator/merchants/{merchant_id}GET사용자 목록/v1/operator/merchants/{merchant_id}/usersPOST사용자 생성/v1/operator/merchants/{merchant_id}/usersGET사용자 조회/v1/operator/merchants/{merchant_id}/users/{user_id}POST사용자 업데이트/v1/operator/merchants/{merchant_id}/users/{user_id}POST사용자 비밀번호 설정/v1/operator/merchants/{merchant_id}/users/{user_id}/passwordPOST사용자 세션 철회/v1/operator/merchants/{merchant_id}/users/{user_id}/revoke-sessionsGET초대 목록/v1/operator/merchants/{merchant_id}/invitationsPOST초대 생성/v1/operator/merchants/{merchant_id}/invitationsGET초대 조회/v1/operator/invitations/{invitation_id}POST초대 다시 보내기/v1/operator/invitations/{invitation_id}/resendPOST초대 철회/v1/operator/invitations/{invitation_id}/revokeGET크레딧 조회/v1/operator/merchants/{merchant_id}/creditsGET크레딧 원장 목록/v1/operator/merchants/{merchant_id}/credits/ledgerPOST크레딧 조정/v1/operator/merchants/{merchant_id}/credits/adjustmentsGET충전 목록/v1/operator/merchants/{merchant_id}/topupsPOST충전 생성/v1/operator/merchants/{merchant_id}/topupsGET충전 조회/v1/operator/merchants/{merchant_id}/topups/{topup_id}GET보고서/v1/operator/reportsGET감사 기록 목록/v1/operator/auditGET이벤트 목록/v1/operator/eventsGET웹훅 목록/v1/operator/webhooksPOST웹훅 생성/v1/operator/webhooksPOST웹훅 업데이트/v1/operator/webhooks/{webhook_id}POST웹훅 비밀 키 교체/v1/operator/webhooks/{webhook_id}/rotateGET웹훅 전송 목록/v1/operator/webhooks/{webhook_id}/deliveriesGET프로젝트 목록/v1/operator/merchants/{merchant_id}/projectsPOST프로젝트 생성/v1/operator/merchants/{merchant_id}/projectsGET프로젝트 조회/v1/operator/merchants/{merchant_id}/projects/{project_id}POST프로젝트 업데이트/v1/operator/merchants/{merchant_id}/projects/{project_id}GET스토어 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/storesPOST스토어 생성/v1/operator/merchants/{merchant_id}/projects/{project_id}/storesGET스토어 조회/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}POST스토어 업데이트/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}GET스토어 모양 조회/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/checkout-appearancePOST스토어 모양 업데이트/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/checkout-appearanceGET스토어 결제 자산 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/payment-assetsPOST스토어 결제 자산 업데이트/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/payment-assetsGET스토어 웹훅 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooksPOST스토어 웹훅 생성/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooksPOST스토어 웹훅 업데이트/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooks/{webhook_id}GET청구서 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/invoicesGET청구서 조회/v1/operator/merchants/{merchant_id}/projects/{project_id}/invoices/{invoice_id}GET지갑 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/walletsGET지갑 주소 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/wallets/{wallet_id}/addressesGET판매자 인증 정보 목록/v1/operator/merchants/{merchant_id}/api-credentialsPOST판매자 인증 정보 생성/v1/operator/merchants/{merchant_id}/api-credentialsPOST판매자 인증 정보 업데이트/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}POST판매자 인증 정보 교체/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}/rotatePOST판매자 인증 정보 철회/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}/revokePOST초대 토큰 확인/v1/onboarding/invitations/checkPOST초대 또는 비밀번호 재설정 수락/v1/onboarding/invitations/accept결제 화면
GET결제 화면 셸/GET호스팅 결제 페이지/invoice/{invoice_id}GET결제 화면용 안전한 청구서 데이터/checkout-api/invoices/{invoice_id}GET스토어 결제 미리보기/invoice/preview/{project_id}GET결제 미리보기 데이터/checkout-api/previews/{project_id}GET스토어 결제 이미지/checkout-api/invoices/{invoice_id}/appearance-images/{kind}/{revision}/image.pngGET스토어 미리보기 이미지/checkout-api/previews/{project_id}/stores/{store_id}/appearance-images/{kind}/{revision}/image.pngGET리비전별 미리보기 로고/checkout-api/previews/{project_id}/logo/{revision}/image.pngGET결제 QR 이미지/checkout-api/invoices/{invoice_id}/payment-methods/{intent_id}/qr.svgGET리비전별 결제 로고/checkout-api/invoices/{invoice_id}/logo/{revision}/image.png서비스
GETAPI 서비스 검색/GET서비스 상태/healthzGET기능/v1/operator/capabilities읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- health.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/capabilities" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/capabilities", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/capabilities");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/capabilities",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"api_version": "v1",
"operator_version": "7.4.0",
"scopes": [
"health.read"
],
"all_merchants": false,
"merchant_ids": [
"11111111-1111-4111-8111-111111111111"
],
"onboarding": [
"direct",
"invitation"
],
"write_methods": [
"POST"
],
"idempotency_required": true
}GET상태/v1/operator/health읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- health.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/health" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/health", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/health");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/health",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"version": "7.4.0",
"nodes": []
}GET판매자 목록/v1/operator/merchants읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- merchants.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}POST판매자 생성/v1/operator/merchants읽기 + 쓰기
비밀번호를 직접 지정하거나 초대해서 호스팅 판매자와 첫 관리자를 원자적으로 만들어요.
- merchants.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 전체 판매자 접근이 필요해요. 수수료 명시적 변경에는 fees.write, 0이 아닌 starting_credit에는 credits.write, 초대 가입에는 invitations.write도 필요해요. 자동 로그인, Basic Auth 우회, 재시도 시 소급 크레딧 지급은 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| name, email | string · required | 판매자 이름과 전체에서 고유한 첫 관리자 이메일. |
| onboarding | direct | invitation · required | direct는 password가 필요하며 초대 이메일을 보내지 않아요. invitation은 password를 생략해요. |
| password | string · direct only | 12~128자, 최대 512 UTF-8바이트. 응답이나 이메일로 반환하지 않아요. 임시 비밀번호에는 require_password_change를 쓰세요. |
| require_password_change | boolean · default false | 첫 로그인에서 새 비밀번호가 필요해요. 직접 만든 모든 계정은 호스팅 지갑 보관에 동의해야 해요. |
| currency | fiat code · optional | 선불 계정 통화. 기본은 지역 통화이며 나중에 바꿀 수 없어요. |
| fee_bps | integer · optional | 0~10000, 100은 1%예요. 생략하면 운영자 기본값을 써요. fees.write가 필요해요. |
| starting_credit | decimal string · default 0 | 정확한 일회성 로컬 지급. 0이 아니면 credits.write가 필요해요. 운영자의 설치 잔액을 충전하지 않아요. |
| external_id | string · optional | 고유 연동 참조, 1~120자. |
| default_timezone | IANA timezone · optional | 기본값은 설치의 지역 시간대예요. |
| send_invitation_email | boolean · default false | 초대 전용. SMTP 설정이 필요하며 응답은 릴레이 수락과 계정 생성을 구분해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Example shop",
"email": "admin@example.test",
"onboarding": "direct",
"password": "REPLACE_WITH_A_UNIQUE_TEMPORARY_PASSWORD",
"require_password_change": true,
"currency": "EUR",
"starting_credit": "0",
"external_id": "customer-1042"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Example shop",
"email": "admin@example.test",
"onboarding": "direct",
"password": "REPLACE_WITH_A_UNIQUE_TEMPORARY_PASSWORD",
"require_password_change": true,
"currency": "EUR",
"starting_credit": "0",
"external_id": "customer-1042"
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Example shop",
"email": "admin@example.test",
"onboarding": "direct",
"password": "REPLACE_WITH_A_UNIQUE_TEMPORARY_PASSWORD",
"require_password_change": true,
"currency": "EUR",
"starting_credit": "0",
"external_id": "customer-1042"
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Example shop",
"email": "admin@example.test",
"onboarding": "direct",
"password": "REPLACE_WITH_A_UNIQUE_TEMPORARY_PASSWORD",
"require_password_change": true,
"currency": "EUR",
"starting_credit": "0",
"external_id": "customer-1042"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 201 또는 200 application/json
{
"merchant_id": "11111111-1111-4111-8111-111111111111",
"user_id": "22222222-2222-4222-8222-222222222222",
"merchant": {
"id": "11111111-1111-4111-8111-111111111111",
"name": "Example shop",
"currency": "EUR",
"balance": "0"
},
"onboarding": "direct",
"access_link": null,
"email_delivery": {
"status": "not_requested"
},
"custody_acceptance_required": true
}GET판매자 조회/v1/operator/merchants/{merchant_id}읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- merchants.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "11111111-1111-4111-8111-111111111111",
"name": "Example shop",
"currency": "EUR",
"balance": "10",
"fee_bps": 300,
"enabled": true,
"payments_paused": false,
"external_id": "customer-1042"
}POST판매자 업데이트/v1/operator/merchants/{merchant_id}읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- merchants.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| name, enabled, payments_paused, fee_bps, external_id | optional fields | 비활성화하면 세션을 철회해요. payments_paused는 새 청구서를 막지만 기존 결제 스캔은 계속돼요. 수수료 변경은 fees.write가 필요하며 향후 청구서에만 적용돼요. 계정 통화는 바꿀 수 없어요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"payments_paused": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"payments_paused": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"payments_paused": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"payments_paused": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "11111111-1111-4111-8111-111111111111",
"name": "Example shop",
"currency": "EUR",
"balance": "10",
"fee_bps": 300,
"enabled": true,
"payments_paused": false
}GET사용자 목록/v1/operator/merchants/{merchant_id}/users읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- users.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}POST사용자 생성/v1/operator/merchants/{merchant_id}/users읽기 + 쓰기
판매자 관리자 또는 선택한 프로젝트로 제한된 사용자를 추가해요.
- users.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| email, display_name | strings · required | 이메일은 설치 전체에서 고유해요. |
| onboarding, password, require_password_change, send_invitation_email | same as merchant creation | 초대 생성에는 invitations.write도 필요해요. |
| access_level | admin | projects · default admin | admin은 이 판매자의 관리자일 뿐 설치·운영자 관리자가 아니에요. |
| project_ids | UUID[] | 판매자가 소유한 프로젝트만 가능해요. 프로젝트 제한 접근에는 선택이 필요하며 테넌트를 넘을 수 없어요. |
| default_timezone | IANA timezone · optional | 생략하면 지역 기본값을 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "staff@example.test",
"display_name": "Store team",
"onboarding": "invitation",
"access_level": "projects",
"project_ids": [
"11111111-1111-4111-8111-111111111111"
],
"send_invitation_email": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"email": "staff@example.test",
"display_name": "Store team",
"onboarding": "invitation",
"access_level": "projects",
"project_ids": [
"11111111-1111-4111-8111-111111111111"
],
"send_invitation_email": false
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"email": "staff@example.test",
"display_name": "Store team",
"onboarding": "invitation",
"access_level": "projects",
"project_ids": [
"11111111-1111-4111-8111-111111111111"
],
"send_invitation_email": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"email": "staff@example.test",
"display_name": "Store team",
"onboarding": "invitation",
"access_level": "projects",
"project_ids": [
"11111111-1111-4111-8111-111111111111"
],
"send_invitation_email": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 201 또는 200 application/json
{
"user": {
"id": "22222222-2222-4222-8222-222222222222",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"email": "admin@example.test",
"display_name": "Shop administrator",
"access_level": "admin",
"project_ids": [],
"enabled": true,
"password_setup_required": false,
"custody_acceptance_required": true,
"password_change_required": true
},
"user_id": "22222222-2222-4222-8222-222222222222",
"access_link": null,
"email_delivery": {
"status": "not_requested"
},
"merchant_id": "11111111-1111-4111-8111-111111111111"
}GET사용자 조회/v1/operator/merchants/{merchant_id}/users/{user_id}읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- users.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| user_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "22222222-2222-4222-8222-222222222222",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"email": "admin@example.test",
"display_name": "Shop administrator",
"access_level": "admin",
"project_ids": [],
"enabled": true,
"password_setup_required": false,
"custody_acceptance_required": true,
"password_change_required": true
}POST사용자 업데이트/v1/operator/merchants/{merchant_id}/users/{user_id}읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- users.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| user_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| email, display_name, enabled, access_level, project_ids, default_timezone | optional fields | 제공한 필드를 업데이트하며 마지막 관리자 보호는 유지돼요. 비밀번호에는 별도의 users.security 작업을 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"display_name": "Store manager"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"display_name": "Store manager"
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"display_name": "Store manager"
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"display_name": "Store manager"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "22222222-2222-4222-8222-222222222222",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"email": "admin@example.test",
"display_name": "Shop administrator",
"access_level": "admin",
"project_ids": [],
"enabled": true,
"password_setup_required": false,
"custody_acceptance_required": true,
"password_change_required": true
}POST사용자 비밀번호 설정/v1/operator/merchants/{merchant_id}/users/{user_id}/password읽기 + 쓰기
호스팅 계정 비밀번호를 설정하고 세션을 철회해요. 기존 TOTP는 유지돼요.
- users.security가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| user_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| password | string · required | 비밀번호 변경과 세션 철회, TOTP 유지. users.security가 필요해요. |
| require_password_change | boolean · default true | 사용자가 다음 로그인 성공 시 직접 비밀번호를 설정해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/password" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"password": "REPLACE_WITH_A_NEW_UNIQUE_PASSWORD",
"require_password_change": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"password": "REPLACE_WITH_A_NEW_UNIQUE_PASSWORD",
"require_password_change": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/password", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"password": "REPLACE_WITH_A_NEW_UNIQUE_PASSWORD",
"require_password_change": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/password");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"password": "REPLACE_WITH_A_NEW_UNIQUE_PASSWORD",
"require_password_change": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/password",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"user_id": "11111111-1111-4111-8111-111111111111",
"sessions_revoked": true,
"totp_preserved": true
}POST사용자 세션 철회/v1/operator/merchants/{merchant_id}/users/{user_id}/revoke-sessions읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- users.security가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| user_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/revoke-sessions" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/revoke-sessions", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/revoke-sessions");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/users/YOUR_USER_ID/revoke-sessions",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"user_id": "11111111-1111-4111-8111-111111111111",
"sessions_revoked": true,
"totp_preserved": true
}GET초대 목록/v1/operator/merchants/{merchant_id}/invitations읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- invitations.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}POST초대 생성/v1/operator/merchants/{merchant_id}/invitations읽기 + 쓰기
일회용 초대 또는 비밀번호 재설정 링크를 만들거나 교체해요.
- invitations.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| user_id, send_email | UUID, boolean | 기존 계정에 일회용 링크를 발급·교체해요. 활성화된 사용자는 1시간 재설정 링크를 받으며 users.security가 필요해요. |
| new user fields | alternative to user_id | email, display_name, access_level, project_ids로 초대 사용자를 만들며 users.write가 필요해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"user_id": "11111111-1111-4111-8111-111111111111",
"send_email": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"user_id": "11111111-1111-4111-8111-111111111111",
"send_email": false
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"user_id": "11111111-1111-4111-8111-111111111111",
"send_email": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"user_id": "11111111-1111-4111-8111-111111111111",
"send_email": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/invitations",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 201 또는 200 application/json
{
"user_id": "22222222-2222-4222-8222-222222222222",
"email": "admin@example.test",
"kind": "invitation",
"expires_at": "2026-10-03T12:00:00Z",
"url": "https://merchant.example.com/account-access.html#token=YOUR_PRIVATE_INVITATION_TOKEN"
}GET초대 조회/v1/operator/invitations/{invitation_id}읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- invitations.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| invitation_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "44444444-4444-4444-8444-444444444444",
"user_id": "22222222-2222-4222-8222-222222222222",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"kind": "invitation",
"status": "pending",
"created_at": "2026-10-01T12:00:00Z",
"expires_at": "2026-10-03T12:00:00Z"
}POST초대 다시 보내기/v1/operator/invitations/{invitation_id}/resend읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- invitations.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| invitation_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| send_email | boolean · default false | 기존 토큰을 교체하며 크레딧을 추가하지 않아요. 새 링크는 한 번만 반환해요. 활성화된 계정은 users.security가 필요해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/resend" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"send_email": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"send_email": false
}`;
const response = await fetch("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/resend", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"send_email": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/resend");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"send_email": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/resend",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"user_id": "22222222-2222-4222-8222-222222222222",
"email": "admin@example.test",
"kind": "invitation",
"expires_at": "2026-10-03T12:00:00Z",
"url": "https://merchant.example.com/account-access.html#token=YOUR_PRIVATE_INVITATION_TOKEN"
}POST초대 철회/v1/operator/invitations/{invitation_id}/revoke읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- invitations.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| invitation_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/revoke" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{}`;
const response = await fetch("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/revoke", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/revoke");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/invitations/YOUR_INVITATION_ID/revoke",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"revoked": true,
"invitation_id": "44444444-4444-4444-8444-444444444444"
}GET크레딧 조회/v1/operator/merchants/{merchant_id}/credits읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- credits.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "11111111-1111-4111-8111-111111111111",
"name": "Example shop",
"currency": "EUR",
"balance": "10",
"fee_bps": 300,
"enabled": true,
"payments_paused": false,
"external_id": "customer-1042"
}GET크레딧 원장 목록/v1/operator/merchants/{merchant_id}/credits/ledger읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- credits.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| page, q | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/ledger" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/ledger", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/ledger");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/ledger",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}POST크레딧 조정/v1/operator/merchants/{merchant_id}/credits/adjustments읽기 + 쓰기
이 판매자의 선불 원장에 사유가 있는 지급 또는 정정을 추가해요.
- credits.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| amount | signed decimal string · required | 판매자 크레딧 통화의 양수 지급 또는 음수 정정이며 소수 최대 6자리예요. 온체인 전송이 아니에요. |
| note | string · required | 사유는 추가만 가능한 원장에 보관돼요. |
| request_id | UUID · required | HTTP Idempotency-Key와 별도로 금액·사유와 함께 영구 저장하세요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/adjustments" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": "10.00",
"note": "Promotional credit",
"request_id": "11111111-1111-4111-8111-111111111111"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"amount": "10.00",
"note": "Promotional credit",
"request_id": "11111111-1111-4111-8111-111111111111"
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/adjustments", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"amount": "10.00",
"note": "Promotional credit",
"request_id": "11111111-1111-4111-8111-111111111111"
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/adjustments");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"amount": "10.00",
"note": "Promotional credit",
"request_id": "11111111-1111-4111-8111-111111111111"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/credits/adjustments",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"balance": "25"
}GET충전 목록/v1/operator/merchants/{merchant_id}/topups읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- topups.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}POST충전 생성/v1/operator/merchants/{merchant_id}/topups읽기 + 쓰기
선불 크레딧 결제 화면을 만들어요. 수동으로 결제 완료 표시를 하지 않아요.
- topups.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| amount | decimal string · required | 판매자 크레딧 통화 최소 1단위. 준비된 운영자 수신 스토어가 필요해요. |
| request_id | UUID · required | 재시도 동안 유지하세요. 이미 만들었다면 기존 청구서를 반환해요. 정산이 관찰된 뒤에만 크레딧이 반영돼요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": "25.00",
"request_id": "11111111-1111-4111-8111-111111111111"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"amount": "25.00",
"request_id": "11111111-1111-4111-8111-111111111111"
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"amount": "25.00",
"request_id": "11111111-1111-4111-8111-111111111111"
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"amount": "25.00",
"request_id": "11111111-1111-4111-8111-111111111111"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 201 또는 200 application/json
{
"id": "44444444-4444-4444-8444-444444444444",
"invoice_id": "33333333-3333-4333-8333-333333333333",
"checkout_url": "https://pay.example.com/invoice/33333333-3333-4333-8333-333333333333"
}GET충전 조회/v1/operator/merchants/{merchant_id}/topups/{topup_id}읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- topups.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| topup_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups/YOUR_TOPUP_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups/YOUR_TOPUP_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups/YOUR_TOPUP_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/topups/YOUR_TOPUP_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "44444444-4444-4444-8444-444444444444",
"merchant_id": "11111111-1111-4111-8111-111111111111",
"invoice_id": "33333333-3333-4333-8333-333333333333",
"amount": "25",
"currency": "EUR",
"status": "pending",
"invoice_status": "new",
"checkout_url": "https://pay.example.com/invoice/33333333-3333-4333-8333-333333333333"
}GET보고서/v1/operator/reports읽기 전용
운영자 재무 개요를 읽어요. 모든 호스팅 판매자 접근이 필요해요.
- reports.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| period, start, end, currency, timezone, merchant_id | query · optional | 재무 필터. period 기본값은 last30이며 사용자 지정은 custom과 YYYY-MM-DD start/end를 사용해요. 전체 판매자 인증 정보만 가능해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/reports" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/reports", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/reports");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/reports",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"summary": {
"fees": "10",
"costs": "3",
"margin": "7",
"credits": "25",
"pending": 0,
"missing_rates": 0
},
"merchants": [],
"filters": {
"period": "last30",
"currency": "EUR",
"timezone": "UTC"
},
"basis": "first_settlement_latest_net_fees"
}GET감사 기록 목록/v1/operator/audit읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- audit.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id, event_type / search | query · optional | 허용된 판매자, 정확한 이벤트 유형(events), 작업 텍스트(audit)로 필터링해요. 이벤트 보관은 30일이에요. |
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/audit" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/audit", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/audit");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/audit",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}GET이벤트 목록/v1/operator/events읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- events.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id, event_type / search | query · optional | 허용된 판매자, 정확한 이벤트 유형(events), 작업 텍스트(audit)로 필터링해요. 이벤트 보관은 30일이에요. |
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/events" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/events", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/events");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/events",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}GET웹훅 목록/v1/operator/webhooks읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- events.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/webhooks" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/webhooks", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/webhooks",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}POST웹훅 생성/v1/operator/webhooks읽기 + 쓰기
향후 운영자 수명 주기 이벤트를 구독해요. 스토어 결제 웹훅이 아니에요.
- webhooks.write + events.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| url | public HTTPS URL · required | 인증 정보, 사설 IP, 리디렉션은 허용하지 않아요. 전송 시 DNS/IP를 다시 확인해요. |
| events | string[] · required | 청구서 콜백이 아닌 운영자 가이드의 수명 주기 이벤트를 선택하세요. |
| merchant_ids | UUID[] · optional | 비어 있으면 인증 정보가 허용하는 모든 판매자를 뜻해요. 현재 범위 제한을 다시 확인해요. |
| enabled | boolean · default true | 일시 중지된 엔드포인트는 대기 전송을 보관하고 다시 켜면 유효한 보관 작업을 재개해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/webhooks" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"url": "https://shop.example.test/operator-events",
"events": [
"merchant.created",
"topup.settled"
],
"merchant_ids": [],
"enabled": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"url": "https://shop.example.test/operator-events",
"events": [
"merchant.created",
"topup.settled"
],
"merchant_ids": [],
"enabled": true
}`;
const response = await fetch("https://api.example.com/v1/operator/webhooks", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"url": "https://shop.example.test/operator-events",
"events": [
"merchant.created",
"topup.settled"
],
"merchant_ids": [],
"enabled": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"url": "https://shop.example.test/operator-events",
"events": [
"merchant.created",
"topup.settled"
],
"merchant_ids": [],
"enabled": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/webhooks",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 201 또는 200 application/json
{
"id": "11111111-1111-4111-8111-111111111111",
"signing_secret": "wco_whsec_EXAMPLE_ONLY_SAVE_ONCE"
}POST웹훅 업데이트/v1/operator/webhooks/{webhook_id}읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- webhooks.write + events.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| webhook_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| url | public HTTPS URL · required | 인증 정보, 사설 IP, 리디렉션은 허용하지 않아요. 전송 시 DNS/IP를 다시 확인해요. |
| events | string[] · required | 청구서 콜백이 아닌 운영자 가이드의 수명 주기 이벤트를 선택하세요. |
| merchant_ids | UUID[] · optional | 비어 있으면 인증 정보가 허용하는 모든 판매자를 뜻해요. 현재 범위 제한을 다시 확인해요. |
| enabled | boolean · default true | 일시 중지된 엔드포인트는 대기 전송을 보관하고 다시 켜면 유효한 보관 작업을 재개해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"url": "https://shop.example.test/operator-events",
"events": [
"topup.settled"
],
"merchant_ids": [],
"enabled": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"url": "https://shop.example.test/operator-events",
"events": [
"topup.settled"
],
"merchant_ids": [],
"enabled": false
}`;
const response = await fetch("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"url": "https://shop.example.test/operator-events",
"events": [
"topup.settled"
],
"merchant_ids": [],
"enabled": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"url": "https://shop.example.test/operator-events",
"events": [
"topup.settled"
],
"merchant_ids": [],
"enabled": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "44444444-4444-4444-8444-444444444444",
"signing_secret": null
}POST웹훅 비밀 키 교체/v1/operator/webhooks/{webhook_id}/rotate읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- webhooks.write + events.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| webhook_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/rotate" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{}`;
const response = await fetch("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/rotate", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/rotate");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/rotate",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "11111111-1111-4111-8111-111111111111",
"signing_secret": "wco_whsec_EXAMPLE_ONLY_SAVE_ONCE"
}GET웹훅 전송 목록/v1/operator/webhooks/{webhook_id}/deliveries읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- events.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| webhook_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| page | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/deliveries" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/deliveries", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/deliveries");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/webhooks/YOUR_WEBHOOK_ID/deliveries",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}GET프로젝트 목록/v1/operator/merchants/{merchant_id}/projects읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}POST프로젝트 생성/v1/operator/merchants/{merchant_id}/projects읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| name, slug | strings · required | 이름과 고유한 고정 프로젝트 식별자. 기존 프로젝트 초기화로 로컬 지갑을 만들며 자금을 옮기지 않아요. |
| enabled, reporting_timezone, checkout_title, checkout_description, checkout_theme, checkout_accent_color | optional | enabled 기본값은 true예요. 일시 중지로 만들고 스토어부터 설정하는 것이 좋아요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Online shop",
"slug": "online-shop",
"enabled": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Online shop",
"slug": "online-shop",
"enabled": false
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Online shop",
"slug": "online-shop",
"enabled": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Online shop",
"slug": "online-shop",
"enabled": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 201 또는 200 application/json
{
"id": "33333333-3333-4333-8333-333333333333",
"slug": "example-shop",
"name": "Example shop",
"enabled": false,
"reporting_currency": "EUR",
"reporting_timezone": "Europe/Berlin",
"stores": []
}GET프로젝트 조회/v1/operator/merchants/{merchant_id}/projects/{project_id}읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "33333333-3333-4333-8333-333333333333",
"slug": "example-shop",
"name": "Example shop",
"enabled": false,
"reporting_currency": "EUR",
"reporting_timezone": "Europe/Berlin",
"stores": []
}POST프로젝트 업데이트/v1/operator/merchants/{merchant_id}/projects/{project_id}읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| name, enabled, reporting_currency, reporting_timezone, checkout_title, checkout_description, checkout_theme, checkout_accent_color | optional | 부분 업데이트. 식별자와 판매자 소유권은 바꿀 수 없어요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"enabled": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"enabled": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"enabled": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"enabled": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "33333333-3333-4333-8333-333333333333",
"slug": "example-shop",
"name": "Example shop",
"enabled": false,
"reporting_currency": "EUR",
"reporting_timezone": "Europe/Berlin",
"stores": []
}GET스토어 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}POST스토어 생성/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| name, slug | strings · required | 스토어 이름과 고정 식별자. |
| default_currency, invoice_expiry_minutes, exchange_rate_spread_percent, underpayment_tolerance_percent | optional | 백분율은 십진수 문자열을 쓰세요. 새 스토어는 프로젝트 기본 스토어의 모양을 이어받아요. |
| enabled, is_default, allow_zero_amount_invoices, allow_underpayments, allow_overpayments, allowed_chain_slugs | optional | payment-assets로 받을 자산을 설정해요. 금액 0 청구서는 기본적으로 꺼져 있어요. |
| ipn_enabled, default_ipn_url, default_redirect_url, default_cancel_url, redirect_automatically | optional | IPN과 반환 URL은 기존 URL 검증을 따라요. 임의 HTML/JavaScript는 허용하지 않아요. |
| checkout_language, embed_enabled, allowed_embed_origins, domains | optional | 지원 언어와 활성 역할 도메인을 사용하고 임베딩 오리진을 명시적으로 설정하세요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Web checkout",
"slug": "web-checkout",
"default_currency": "EUR",
"enabled": false
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Web checkout",
"slug": "web-checkout",
"default_currency": "EUR",
"enabled": false
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Web checkout",
"slug": "web-checkout",
"default_currency": "EUR",
"enabled": false
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Web checkout",
"slug": "web-checkout",
"default_currency": "EUR",
"enabled": false
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 201 또는 200 application/json
{
"id": "44444444-4444-4444-8444-444444444444",
"project_id": "33333333-3333-4333-8333-333333333333",
"slug": "online",
"name": "Online shop",
"enabled": false,
"default_currency": "EUR",
"invoice_expiry_minutes": 60
}GET스토어 조회/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| store_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "44444444-4444-4444-8444-444444444444",
"project_id": "33333333-3333-4333-8333-333333333333",
"slug": "online",
"name": "Online shop",
"enabled": false,
"default_currency": "EUR",
"invoice_expiry_minutes": 60
}POST스토어 업데이트/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| store_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| store fields | optional | slug를 제외하면 스토어 생성과 같은 변경 가능 설정이에요. 제공한 필드만 바뀌어요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"enabled": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"enabled": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"enabled": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"enabled": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "44444444-4444-4444-8444-444444444444",
"project_id": "33333333-3333-4333-8333-333333333333",
"slug": "online",
"name": "Online shop",
"enabled": false,
"default_currency": "EUR",
"invoice_expiry_minutes": 60
}GET스토어 모양 조회/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/checkout-appearance읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| store_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"revision": 1,
"settings": {
"inherit_default_store": true
},
"effective": {
"title": "Pay securely"
}
}POST스토어 모양 업데이트/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/checkout-appearance읽기 + 쓰기
검증되고 리비전 보호가 있는 스토어 디자인을 저장해요.
- projects.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| store_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| revision | integer · required | 먼저 GET으로 현재 리비전을 읽으세요. 오래된 리비전은 다른 편집자의 변경을 덮어쓰지 않고 실패해요. |
| settings | appearance object · required | inherit_default_store, 브랜드, 시작·마무리 문구, 글자 크기, 표시 여부를 포함한 검증된 결제 모양. 임의 HTML/JavaScript는 안 돼요. 이미지 바이트 업로드는 콘솔 전용이에요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"revision": 1,
"settings": {
"inherit_default_store": false,
"title": "Pay securely",
"theme": "light"
}
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"revision": 1,
"settings": {
"inherit_default_store": false,
"title": "Pay securely",
"theme": "light"
}
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"revision": 1,
"settings": {
"inherit_default_store": false,
"title": "Pay securely",
"theme": "light"
}
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"revision": 1,
"settings": {
"inherit_default_store": false,
"title": "Pay securely",
"theme": "light"
}
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/checkout-appearance",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"settings": {
"inherit_default_store": false,
"title": "Pay securely",
"theme": "light"
},
"revision": 2
}GET스토어 결제 자산 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/payment-assets읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| store_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": []
}POST스토어 결제 자산 업데이트/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/payment-assets읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| store_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| assets | array · required | 온체인 수단 전체 교체: asset_id UUID와 display_order. []는 받는 온체인 자산을 비워요. 검증된 프로젝트 자산만 가능하며 Lightning은 설정하지 않아요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"assets": [
{
"asset_id": "11111111-1111-4111-8111-111111111111",
"display_order": 0
}
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"assets": [
{
"asset_id": "11111111-1111-4111-8111-111111111111",
"display_order": 0
}
]
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"assets": [
{
"asset_id": "11111111-1111-4111-8111-111111111111",
"display_order": 0
}
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"assets": [
{
"asset_id": "11111111-1111-4111-8111-111111111111",
"display_order": 0
}
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": []
}GET스토어 웹훅 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooks읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| store_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}POST스토어 웹훅 생성/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooks읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| store_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| name, url, event_types | strings / array · required | 공개 HTTPS 수신기와 IPN·웹훅 문서의 청구서 이벤트 이름. |
| enabled, automatic_redelivery | booleans · default true | 생성 시 서명 비밀 키를 한 번만 반환해요. 스토어 결제 콜백이며 운영자 수명 주기 이벤트가 아니에요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": true,
"automatic_redelivery": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": true,
"automatic_redelivery": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": true,
"automatic_redelivery": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": true,
"automatic_redelivery": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 201 또는 200 application/json
{
"signing_secret": "EXAMPLE_ONLY_SAVE_ONCE",
"endpoint": {
"id": "44444444-4444-4444-8444-444444444444",
"name": "Orders",
"enabled": true
},
"secret_visible_once": true
}POST스토어 웹훅 업데이트/v1/operator/merchants/{merchant_id}/projects/{project_id}/stores/{store_id}/webhooks/{webhook_id}읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- projects.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| store_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| webhook_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| name, url, event_types | strings / array · required | 공개 HTTPS 수신기와 IPN·웹훅 문서의 청구서 이벤트 이름. |
| enabled, automatic_redelivery | booleans · default true | 생성 시 서명 비밀 키를 한 번만 반환해요. 스토어 결제 콜백이며 운영자 수명 주기 이벤트가 아니에요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks/YOUR_WEBHOOK_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": false,
"automatic_redelivery": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": false,
"automatic_redelivery": true
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks/YOUR_WEBHOOK_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": false,
"automatic_redelivery": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks/YOUR_WEBHOOK_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Orders",
"url": "https://shop.example.test/payments",
"event_types": [
"invoice.settled"
],
"enabled": false,
"automatic_redelivery": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/webhooks/YOUR_WEBHOOK_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "44444444-4444-4444-8444-444444444444",
"name": "Orders",
"enabled": true
}GET청구서 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/invoices읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- reports.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| limit, offset, search, status, store_id | query · optional | 프로젝트 청구서 목록과 같은 청구서 페이지 구분 및 필터. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"pagination": {
"limit": 25,
"offset": 0,
"total": 0,
"has_more": false
}
}GET청구서 조회/v1/operator/merchants/{merchant_id}/projects/{project_id}/invoices/{invoice_id}읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- reports.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
- invoice_id는 생성과 콜백에서 반환하는 공개 청구서 ID이며 내부 id가 아니에요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| invoice_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices/YOUR_INVOICE_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices/YOUR_INVOICE_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices/YOUR_INVOICE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/invoices/YOUR_INVOICE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"invoice_id": "44444444-4444-4444-8444-444444444444",
"project_id": "33333333-3333-4333-8333-333333333333",
"amount": "25",
"currency": "EUR",
"status": "new",
"metadata": {},
"payment_intents": []
}GET지갑 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/wallets읽기 전용
캐시된 공개 지갑 잔액을 읽고 개인 키나 복구 구문은 읽지 않아요.
- reports.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 잔액은 최신성 필드가 있는 캐시 관찰값이며 사용 가능 잔액을 보장하지 않아요. 이 API에는 전송이나 키 내보내기가 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}GET지갑 주소 목록/v1/operator/merchants/{merchant_id}/projects/{project_id}/wallets/{wallet_id}/addresses읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- reports.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| project_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| wallet_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| limit, before, search, has_balance, hide_small_balances | query · optional | limit은 1~50, 기본 25예요. 다음 페이지는 next_cursor를 before로 전달하고 1페이지는 before를 생략하세요. has_balance=false와 hide_small_balances=false는 빈 잔액과 소액 잔액을 포함해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets/YOUR_WALLET_ID/addresses" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets/YOUR_WALLET_ID/addresses", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets/YOUR_WALLET_ID/addresses");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/projects/YOUR_PROJECT_ID/wallets/YOUR_WALLET_ID/addresses",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"wallet": {
"id": "22222222-2222-4222-8222-222222222222",
"chain_slug": "ethereum"
},
"items": [],
"total": 0,
"total_pages": 1,
"next_cursor": null,
"reporting_currency": "EUR",
"has_balance": true,
"hide_small_balances": true,
"small_balance_threshold": {
"amount": "0.20",
"currency": "EUR"
}
}GET판매자 인증 정보 목록/v1/operator/merchants/{merchant_id}/api-credentials읽기 전용
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- merchant_credentials.read가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| page, search | query · optional | 페이지는 1부터 시작하고 페이지당 25개예요. 판매자, 사용자, 프로젝트, 스토어, 지갑, 인증 정보, 웹훅은 검색을 지원하며 기본 이벤트 목록은 전용 필터를 사용해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [],
"page": 1,
"page_size": 25,
"total": 0
}POST판매자 인증 정보 생성/v1/operator/merchants/{merchant_id}/api-credentials읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- merchant_credentials.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| name | string · required | 새 일반 판매자 키의 라벨. 운영자 키가 아니에요. |
| access_level | read_only | read_write · default read_only | 읽기·쓰기는 기존 판매자 API 규약을 활성화해요. |
| project_ids | UUID[] | 선택한 판매자가 소유한 프로젝트만 가능해요. 빈 목록은 기존 전체 판매자 프로젝트 정책을 따라요. |
| enabled, ip_restriction_enabled, allowed_ips, requests_per_minute | optional | 기존 판매자 키 제어. 비밀 값은 한 번만 반환하며 merchant_credentials.write가 필요해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Store integration",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"11111111-1111-4111-8111-111111111111"
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Store integration",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"11111111-1111-4111-8111-111111111111"
]
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Store integration",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"11111111-1111-4111-8111-111111111111"
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Store integration",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"11111111-1111-4111-8111-111111111111"
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 201 또는 200 application/json
{
"credential": {
"id": "22222222-2222-4222-8222-222222222222",
"name": "Checkout",
"access_level": "read_only",
"project_ids": [
"33333333-3333-4333-8333-333333333333"
],
"enabled": true
},
"token": "wc_live_EXAMPLE_ONLY_SAVE_ONCE"
}POST판매자 인증 정보 업데이트/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- merchant_credentials.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| credential_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| name, access_level, enabled, ip_restriction_enabled, allowed_ips | required fields | 변경을 포함한 전체 현재 인증 정보 설정을 보내세요. project_ids는 기본 [], requests_per_minute는 기본 판매자 API 할당량이에요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Checkout",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"33333333-3333-4333-8333-333333333333"
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{
"name": "Checkout",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"33333333-3333-4333-8333-333333333333"
]
}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{
"name": "Checkout",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"33333333-3333-4333-8333-333333333333"
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"name": "Checkout",
"access_level": "read_only",
"enabled": true,
"ip_restriction_enabled": false,
"allowed_ips": [],
"project_ids": [
"33333333-3333-4333-8333-333333333333"
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"id": "22222222-2222-4222-8222-222222222222",
"name": "Checkout",
"access_level": "read_only",
"project_ids": [
"33333333-3333-4333-8333-333333333333"
],
"enabled": true
}POST판매자 인증 정보 교체/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}/rotate읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- merchant_credentials.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| credential_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/rotate" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/rotate", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/rotate");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/rotate",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"credential": {
"id": "22222222-2222-4222-8222-222222222222",
"name": "Checkout",
"access_level": "read_only",
"project_ids": [
"33333333-3333-4333-8333-333333333333"
],
"enabled": true
},
"token": "wc_live_EXAMPLE_ONLY_SAVE_ONCE"
}POST판매자 인증 정보 철회/v1/operator/merchants/{merchant_id}/api-credentials/{credential_id}/revoke읽기 + 쓰기
별도의 운영자 인증 정보로 지정된 호스팅 판매자 리소스를 관리하거나 확인하세요.
- merchant_credentials.write가 필요하며 허용된 호스팅 판매자만 접근해요. 운영자 키는 소유자의 자체 사업 작업 공간에 접근할 수 없어요.
- 전송 전에 고유 Idempotency-Key와 정확한 본문을 영구 저장하세요. 재시도는 커밋된 작업을 반복하지 않아요. 재응답에는 비밀 필드를 빼므로 비밀 응답을 잃었다면 생성된 리소스를 확인하고 명시적으로 교체·재발급하세요. 409 operator_request_in_progress는 결과를 모르는 중단 요청일 수 있어요. 리소스·감사를 확인하고 무작정 새 키로 재시도하지 마세요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_OPERATOR_API_TOKEN |
| Idempotency-Key | 필수 | 문자, 숫자, -, _, .으로 된 16~128자. 이 작업용으로 영구 저장 |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| merchant_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
| credential_id | path UUID | 정규 소문자 리소스 UUID. 인증 정보의 판매자 범위에 속해야 해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new operation.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/revoke" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: operator-request-1042' \
--header 'Content-Type: application/json' \
--data-raw '{}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new operation.
const body = `{}`;
const response = await fetch("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/revoke", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new operation.
$body = <<<'JSON'
{}
JSON;
$ch = curl_init("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/revoke");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: operator-request-1042", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new operation.
headers = {
"Idempotency-Key": "operator-request-1042",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{}""".encode("utf-8")
request = Request("https://api.example.com/v1/operator/merchants/YOUR_MERCHANT_ID/api-credentials/YOUR_CREDENTIAL_ID/revoke",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"revoked": true
}POST초대 토큰 확인/v1/onboarding/invitations/check공개
토큰만 사용하는 가입이에요. 운영자 키를 받거나 자동 로그인하지 않아요. 콘솔 로그인에는 사이트 Basic Auth와 기존 TOTP가 여전히 필요해요.
- 초대는 48시간, 비밀번호 재설정 링크는 1시간이에요. 해시된 일회용 토큰이며 재발급 시 이전 링크를 철회해요. 수락 시 TOTP를 유지하고 이전 세션을 철회해요.
- 자동 재시도는 없어요. 수락이 시간 초과되면 링크 상태를 확인하고 로그인해 보세요. 실패로 단정하지 마세요. 관찰된 출처 IP로 요청을 제한해요. 수신자가 직접 자산 보관에 동의해야 해요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| token | string · required | 초대 URL 프래그먼트의 비밀 값. 로그에 남기지 마세요. |
요청
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/onboarding/invitations/check" \
--header 'Content-Type: application/json' \
--data-raw '{
"token": "YOUR_PRIVATE_INVITATION_TOKEN"
}'// Node.js 18+ · run on your server, never in browser code.
const body = `{
"token": "YOUR_PRIVATE_INVITATION_TOKEN"
}`;
const response = await fetch("https://api.example.com/v1/onboarding/invitations/check", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$body = <<<'JSON'
{
"token": "YOUR_PRIVATE_INVITATION_TOKEN"
}
JSON;
$ch = curl_init("https://api.example.com/v1/onboarding/invitations/check");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
body = """{
"token": "YOUR_PRIVATE_INVITATION_TOKEN"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/onboarding/invitations/check",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"kind": "invitation",
"email": "admin@example.test",
"merchant_name": "Example shop"
}POST초대 또는 비밀번호 재설정 수락/v1/onboarding/invitations/accept공개
토큰만 사용하는 가입이에요. 운영자 키를 받거나 자동 로그인하지 않아요. 콘솔 로그인에는 사이트 Basic Auth와 기존 TOTP가 여전히 필요해요.
- 초대는 48시간, 비밀번호 재설정 링크는 1시간이에요. 해시된 일회용 토큰이며 재발급 시 이전 링크를 철회해요. 수락 시 TOTP를 유지하고 이전 세션을 철회해요.
- 자동 재시도는 없어요. 수락이 시간 초과되면 링크 상태를 확인하고 로그인해 보세요. 실패로 단정하지 마세요. 관찰된 출처 IP로 요청을 제한해요. 수신자가 직접 자산 보관에 동의해야 해요.
- 응답 예제는 일부 필드를 보여줘요. 추가 응답 필드는 호환되는 확장으로 처리하세요.
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| token | string · required | 초대 URL 프래그먼트의 비밀 값. 로그에 남기지 마세요. |
| password | string · required | 새 비밀번호, 12~128자(최대 512 UTF-8바이트). |
| custody_acknowledged | boolean | 새 호스팅 지갑 초대를 수락할 때 true여야 해요. |
요청
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/onboarding/invitations/accept" \
--header 'Content-Type: application/json' \
--data-raw '{
"token": "YOUR_PRIVATE_INVITATION_TOKEN",
"password": "REPLACE_WITH_YOUR_OWN_UNIQUE_PASSWORD",
"custody_acknowledged": true
}'// Node.js 18+ · run on your server, never in browser code.
const body = `{
"token": "YOUR_PRIVATE_INVITATION_TOKEN",
"password": "REPLACE_WITH_YOUR_OWN_UNIQUE_PASSWORD",
"custody_acknowledged": true
}`;
const response = await fetch("https://api.example.com/v1/onboarding/invitations/accept", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$body = <<<'JSON'
{
"token": "YOUR_PRIVATE_INVITATION_TOKEN",
"password": "REPLACE_WITH_YOUR_OWN_UNIQUE_PASSWORD",
"custody_acknowledged": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/onboarding/invitations/accept");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
body = """{
"token": "YOUR_PRIVATE_INVITATION_TOKEN",
"password": "REPLACE_WITH_YOUR_OWN_UNIQUE_PASSWORD",
"custody_acknowledged": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/onboarding/invitations/accept",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"password_set": true
}GET결제 예외 목록/v1/projects/{project_id}/reconciliation읽기 전용
미달·초과·지연·재구성·모호한 결제, 전송 실패, 비활성·만료 수단을 위한 하나의 페이지별 검토 대기열이에요. 운영자가 확인한 사례도 새 증거가 오면 다시 열려요.
- 읽기 전용이고 프로젝트 범위이며 인증 정보 할당량이 적용돼요. 금전 결정과 환불은 계속 콘솔 전용이에요.
- 행에는 id(내부 UUID), invoice_id(공개 UUID, 콜백과 동일), 스토어 정보, 원래 법정화폐 금액·통화, invoice_status, 사례 상태, 이유, 리비전, updated_at이 있어요. 판매자 상세 엔드포인트에는 invoice_id를 사용하세요.
- 자동 감지는 원래 청구서 모니터링 기간을 따라요. 재스캔은 결제 화면을 켜지 않고 관찰을 1시간 연장해요. 정산·취소 후에도 해당 기간 안에서 수단을 계속 모니터링해요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 이 인증 정보에 지정된 프로젝트. |
| status | query string | open(기본값), resolved 또는 all. |
| reason | query string | underpaid, overpaid, late, reorged, ambiguous, delivery_failed, disabled_method 또는 expired_method. |
| search | query string | 최대 100자: 청구서 ID, 주문, 고객, 스토어. |
| store_id | query UUID | 선택적 스토어 필터. |
| page | query integer | 1~40001. 페이지당 사례 25개로 고정. |
예외 대기열 응답
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| data | ExceptionRow[] | 항상 | 최근 업데이트된 사례부터 표시해요. 판매자 상세 URL에는 내부 id가 아닌 invoice_id를 사용하세요. |
| pagination | object | 항상 | page(1~40001), per_page(25), 일치 행 총수 total, has_more. |
| counts | object | 항상 | 현재 필터와 무관한 프로젝트 전체의 open 및 resolved 총수. |
ExceptionRow
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id / invoice_id | UUID | 항상 | 내부 기록 ID / 고객용 청구서 UUID. invoice_id는 콜백 페이로드와 일치해요. |
| store_id / store_name | UUID / string | 항상 | 소속 스토어. |
| order_id / email | string | null | 항상 | 비공개 판매자 주문 참조 및 고객 이메일. |
| amount / currency | decimal string / string | 항상 | 원래 법정화폐 청구 금액과 통화. |
| invoice_status | invoice status | 항상 | 현재 결제 수명 주기 상태. |
| status / reasons | open|resolved / string[] | 항상 | 사례 상태와 reason 필터의 예외 유형. |
| revision / updated_at | integer / timestamp | 항상 | 현재 검토 리비전 및 업데이트 시간. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation?status=open&page=1" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation?status=open&page=1", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation?status=open&page=1");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation?status=open&page=1",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{"data":[],"pagination":{"page":1,"per_page":25,"total":0,"has_more":false},"counts":{"open":0,"resolved":0}}GET대사 증거 조회/v1/projects/{project_id}/reconciliation/{invoice_id}읽기 전용
청구서, 사례, 수단별 정확한 합계·환불 가능액, 관찰 거래, 전송 기록, 판매자 결정, 연결된 환불 전송을 반환해요. 서명 키나 콜백 비밀 정보는 노출하지 않아요.
- 청구서에 예외가 없으면 case는 null이에요. 최근 관찰 100개와 전송 50개를 반환하며 결정 기록은 페이지로 나눠요.
- refundable_atomic은 네트워크 확인이 최소 1회 필요하고 기존 환불 예약을 제외하며 실제 지갑 사용 가능 자금을 보장하지 않아요. 실시간 견적은 지갑 준비, 출처 잔액, 수수료도 검증해요.
- 환불 브로드캐스트는 체인 엔드포인트에 제출했다는 뜻이며 고객 수신을 독립 확인한 것은 아니에요. 수수료는 추가이고 환불했다고 법정화폐 처리 수수료가 자동 반환되지는 않아요.
- 콘솔 프로젝트 메뉴 → 확인 필요에서 취소, 승인, 거절, 재개, 검토, 메모, 재스캔, 전송 재시도, 지원 체인 환불을 할 수 있어요. 결정에는 CSRF 보호 세션, 고유 request_id, 현재 사례 리비전, 필수 메모, 명시적 확인이 필요해요. Bearer 토큰으로는 이런 변경을 호출할 수 없어요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 지정된 프로젝트. |
| invoice_id | path UUID | 내부 id가 아닌 공개 청구서 UUID. |
| page | query integer | 결정 기록 페이지. 1부터 시작하고 페이지당 25개예요. |
대사 응답
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| invoice | InvoiceDetail | 항상 | 전체 판매자 청구서: 요약 필드, 비공개 메타데이터, payment_intents. data로 감싸지 않아요. |
| case | object | null | 항상 | 상태, 이유, 리비전, 타임스탬프를 포함한 현재 사례. 예외가 없으면 null이며 내부 증거는 제외해요. |
| methods | object[] | 항상 | id, wallet_id, asset_id, symbol, chain, decimals, expected_atomic, received_atomic, confirmed_atomic, refundable_atomic, address, tag, monitor_error, last_checked_at, monitoring_expires_at, spending_supported. 최소 단위 금액은 문자열이에요. |
| history | object[] | 항상 | 이 페이지의 최신 결정 25개: id, action, note, actor, result, created_at. |
| history_pagination | object | 항상 | page, per_page(25), total. 결정 기록만 page로 페이지를 나눠요. |
| refunds | object[] | 항상 | 최신 환불 100개: id, payment_intent_id, amount_atomic, destination, status, request, treasury_intent_id, transfer_status, created_at, transactions(id/status). 환불 제출은 콘솔 전용이에요. |
| observations | object[] | 항상 | 최신 100개: payment_intent_id, transaction_id, event_index, amount, status, confirmations, observed_at, symbol, chain, disabled_at_detection. 지원 시 explorer_name/explorer_url도 포함해요. |
| deliveries | object[] | 항상 | 최신 50개: id, kind, status, attempts, response_status, error, next_attempt_at, event_type, created_at. 콜백 비밀 정보는 없어요. |
청구서 요약
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id | UUID | 항상 | 내부 청구서 UUID. 판매자 상세나 결제 경로에 사용하지 마세요. |
| invoice_id | UUID | 항상 | 판매자 상세 및 결제 경로에 쓰는 공개 청구서 UUID. |
| project_id | UUID | 항상 | 소속 프로젝트. |
| store_id | UUID | 항상 | 소속 스토어. |
| source | manual | api | 항상 | 청구서 생성 방식. |
| order_id | string | null | 항상 | 판매자 주문 참조. |
| string | null | 항상 | 판매자 전용 고객 이메일. 공개 결제 화면에서 반환하지 않아요. | |
| customer_name | string | null | 항상 | 비공개 firstname, lastname, company 메타데이터에서 만든 표시 이름. |
| customer_address | string | null | 항상 | 비공개 company, street, street2, zip, city, country, countryiso2, vatid 메타데이터에서 만든 한 줄 판매자 주소. |
| description | string | null | 항상 | 고객용 설명. |
| amount | decimal string | 항상 | 정규 청구서 금액. |
| currency | string | 항상 | 정규화된 청구서 통화·자산 코드. |
| exchange_rate_spread_percent | decimal string | 항상 | 고정 견적 스프레드. 생성 시 지정값 또는 생략 시 스토어 기본값이에요. 올림 전에 적용하고 이 청구서에서는 바뀌지 않아요. |
| underpayment_tolerance_percent | decimal string | 항상 | 청구서 생성 시 저장한 변경 불가능한 허용 미달 비율. |
| status | invoice status | 항상 | new, processing, settled, expired, invalid 또는 cancelled. |
| amount_status | amount status | 항상 | none, partial, paid 또는 overpaid. 명시적으로 허용한 금액 0 청구서는 결제 수단 없이 none으로 정산돼요. |
| timing_status | timing status | 항상 | on_time 또는 late. |
| resolution | resolution | 항상 | automatic, manually_settled 또는 manually_invalidated. |
| sequence | integer | 항상 | 1부터 시작하는 단조 증가 청구서 상태 순번. |
| winning_payment_intent_id | UUID | null | 항상 | 선택된 경우 청구서를 완료시킨 결제 수단. |
| expires_at | RFC 3339 timestamp | 항상 | 견적·결제 기한. |
| monitoring_expires_at | RFC 3339 timestamp | 항상 | 결제 수단에 설정된 가장 늦은 지연 모니터링 종료 시간. |
| settled_at | timestamp | null | 항상 | 정산된 경우 정산 시간. |
| cancelled_at | timestamp | null | 항상 | 취소된 경우 취소 시간. |
| archived_at | timestamp | null | 항상 | 보관 처리된 경우 보관 시간. |
| created_at | RFC 3339 timestamp | 항상 | 생성 시간. |
| updated_at | RFC 3339 timestamp | 항상 | 마지막 상태 업데이트 시간. |
청구서 상세 추가 필드
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| ipn_url | string | null | 항상 | 청구서별 유효 IPN 대상. 판매자 응답 전용이며 공개 결제 화면에서는 생략해요. |
| redirect_url | string | null | 항상 | 정산 후 사용하는 유효 성공 URL. |
| cancel_url | string | null | 항상 | 결제가 성공하지 않고 끝날 때 사용하는 유효 반환 URL. |
| redirect_automatically | boolean | 항상 | 성공 후 결제 화면의 자동 이동 여부. |
| checkout_language | string | 항상 | 유효 결제 화면 언어 태그. |
| metadata | object | 항상 | 판매자 메타데이터. 공개 결제 화면에는 반환하지 않아요. |
| payment_intents | PaymentIntent[] | 항상 | 견적된 결제 수단과 모니터링 상태. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation/YOUR_PUBLIC_INVOICE_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation/YOUR_PUBLIC_INVOICE_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation/YOUR_PUBLIC_INVOICE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/reconciliation/YOUR_PUBLIC_INVOICE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{"invoice":{"invoice_id":"YOUR_PUBLIC_INVOICE_ID","status":"processing"},"case":{"status":"open","reasons":["underpaid"],"revision":1},"methods":[],"history":[],"history_pagination":{"page":1,"per_page":25,"total":0},"refunds":[],"observations":[],"deliveries":[]}GETAPI 서비스 검색/공개
v1 공개 API 역할을 확인하는 관리 API 호스트 앞단 응답. 판매자 Axum 라우터가 아닌 관리 프록시가 만들어요.
- Bearer 토큰은 필요 없어요.
- 관리되는 API 호스트 이름만 이 정확한 루트 응답을 보장해요.
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/"// Node.js 18+ · run on your server, never in browser code.
const response = await fetch("https://api.example.com/", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://api.example.com/");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://api.example.com/",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"service": "Wholly Crypto API",
"status": "ready",
"version": "v1"
}GET서비스 상태/healthz공개
앱 접근 가능 여부와 2초 데이터베이스 ping을 확인해요. 모니터링용이며 청구서 상태를 대신하지 않아요.
- Bearer 토큰은 필요 없어요.
- version 값은 실행 중인 패키지 버전이며 API 경로 버전이 아니에요.
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/healthz"// Node.js 18+ · run on your server, never in browser code.
const response = await fetch("https://api.example.com/healthz", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://api.example.com/healthz");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://api.example.com/healthz",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 정상, 503 데이터베이스 사용 불가
{
"status": "ok",
"database": "ok",
"version": "0.1.0"
}GET프로젝트 결제 자산 목록/v1/projects/{project_id}/payment-assets읽기 전용
프로젝트 정책, 체인 지갑 준비 상태, 설치된 스캔·잔액 기능과 함께 기본 자산과 검증 토큰을 나열해요. scanner_ready는 어댑터 빌드 조건이며 실시간 엔드포인트 정족수 결과가 아니에요. 6.0.6부터 스캐너 중단 중에도 생성 시 설정된 수단을 유지해요. 수신 검증에는 설정된 수의 정상·정확한 역할 제공업체가 여전히 필요해요(기본 2개, 선택적으로 1개).
- 토큰이 전역에 나와도 scanner_ready나 payment_supported가 false이면 선택할 수 없어요.
- 운영자 기능 표에도 스캐너의 정확한 엔드포인트 역할이 필요해요. 정상이어도 호환되지 않는 API를 제공하는 엔드포인트는 세지 않아요.
- 토큰은 기본 체인의 프로젝트 지갑을 공유하며 새 시드 구문을 만들지 않아요.
- 포함된 지갑 요약은 준비 상태만 보여주며 잔액은 비어 있어요. 자세한 잔액은 GET /v1/projects/{project_id}/wallets를 사용하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 인증 정보에 지정된 활성 프로젝트. |
PaymentAsset
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id | UUID | 항상 | 프로젝트·스토어 정책 경로에서 사용하는 영구 결제 자산 식별자. |
| asset_key | string | 항상 | 정규 CAIP 방식 기본 코인 또는 컨트랙트 자산 식별자. |
| chain_slug / network | string | 항상 | Wholly Crypto 체인 식별자 및 설정된 네트워크. |
| caip_network_id / caip_asset_id | string / string|null | 항상 | 정규 네트워크·자산 식별자. |
| asset_kind | native | token | 항상 | 정산에 체인 통화를 쓰는지 검증된 컨트랙트·민트를 쓰는지 여부. |
| payment_rail | string | 항상 | 런타임 경로: utxo, evm-native, solana-native, account-native, privacy-native 또는 token-transfer. |
| symbol / name / decimals | string / string / integer | 항상 | 표시 식별자와 정확한 최소 단위 정밀도. |
| contract_address | string | null | 항상 | 토큰의 정규 ERC-20 컨트랙트 또는 SPL 민트. 기본 자산은 null. |
| coingecko_id | string | null | 항상 | 탐색·가격 식별자. 사용자 지정 컨트랙트는 null이며 티커로 시장가를 추론하지 마세요. CoinGecko 메타데이터만으로 토큰이 선택 가능해지지는 않아요. |
| custom_token | boolean | 항상 | 온체인 검증된 사용자 지정 컨트랙트. 프로젝트 범위 고정 USD 또는 선택한 DEX 풀 가격을 사용해요. |
| icon_path | path | null | 항상 | 가능한 경우 로컬 캐시 토큰 아이콘. |
| token_standard | erc20 | spl-token | null | 항상 | 검증된 런타임 토큰 표준. 기본 자산은 null. |
| metadata_verified_at | timestamp | null | 항상 | 등록된 토큰의 온체인 메타데이터 검증 시간. |
| payment_supported / scanner_ready / balance_ready | boolean | 항상 | 빌드 시 레지스트리 조건. scanner_ready는 결제 스캐너 런타임이 설치됐다는 뜻이에요. 결제 확인에는 설정된 수의 정상·정확한 역할 제공업체가 필요해요(기본 2개, 선택적으로 1개). 6.0.6부터 일시적인 스캐너 불가 상태는 청구서 생성을 막지 않아요. balance_ready는 구현된 잔액 어댑터에만 true예요. |
| default_finality_mode | confirmations | finalized | 항상 | 새 프로젝트 정책이 이어받는 기본 최종 확정 모델. |
| default_required_confirmations / default_monitoring_minutes | integer | 항상 | 기본 확인 및 모니터링 정책. |
ProjectPaymentAsset
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| asset | PaymentAsset | 항상 | 영구 기본 코인 또는 검증 토큰 자산. |
| policy | ProjectAssetPolicy | null | 항상 | 프로젝트 활성화·최종 확정 정책이며 미설정 시 null이에요. custom_price_mode(fixed/dex), custom_price_usd(고정 십진수 문자열 또는 null), custom_dex_pair(선택 풀 또는 null), custom_dex(dex_id, quote_symbol, 현재 price_usd 또는 null, liquidity_usd, fetched_at, last_error)를 포함해요. 프로젝트의 스토어들이 사용자 지정 가격을 공유해요. |
| wallet | WalletSummary | null | 항상 | 체인의 비수탁 프로젝트 지갑. 토큰은 기본 체인 지갑을 공유해요. |
| wallet_readiness | readiness enum | 항상 | unsupported, project_disabled, project_asset_disabled, store_disabled, store_asset_disabled, wallet_missing, wallet_pending, wallet_disabled, wallet_error, backup_required, account_activation_required, external_wallet_rpc_required 또는 ready. |
| receive_readiness | ReceiveReadiness | null | 5.5.0+ | 공유 프로젝트 수신 설정 평가. 지갑과 독립 스캐너 제공업체 검사를 포함하며 잔액 최신성·전송 가스와는 별개예요. 프로젝트 정책이 없으면 null이에요. 청구서 통화·환율은 생성 시 확인해요. |
WalletSummary
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id / project_id / native_asset_id | UUID | 항상 | 지갑, 소유 프로젝트, 체인 기본 자산 식별자. |
| chain_slug / network | string | 항상 | 지갑 체인과 네트워크. |
| asset_symbol / asset_name | string | 항상 | 체인 기본 코인 표시 식별자. |
| status | pending | active | disabled | error | 항상 | 지갑 운영 상태. |
| label | string | 항상 | 운영자 라벨. |
| public_key / primary_address | string | null | 항상 | 공개 지갑 식별자. 시드 구문이나 개인 키는 노출하지 않아요. |
| derivation_scheme / address_format | string | null | 항상 | 주소 정책과 형식. |
| backup_confirmed_at | timestamp | null | 항상 | 운영자가 복구 백업을 확인하면 null이 아니에요. |
| activation_required / activation_verified_at | boolean / timestamp|null | 항상 | XRP·Stellar 공유 계정은 운영자가 표시된 주소에 자금을 넣고 설정된 스캐너 제공업체가 정확한 계정을 검증해야 사용할 수 있어요. 영구 증명은 만료되지 않아요. 실시간 스캐너 상태는 청구서 생성이 아닌 결제 검증용으로 별도 확인해요. |
| receive_readiness | ReceiveReadiness | null | 5.5.0+ | 지갑 목록에는 프로젝트 수신 설정과 체인 스캐너 전제 조건이 포함돼요. 잔액, 토큰 가스, 전송 준비 상태와 별개예요. 다른 지갑 응답에서는 null일 수 있어요. |
| monero_wallet_rpc | MoneroWalletRpcBinding | null | 항상 | 비밀 정보를 제거한 Monero 외부 조회 전용 wallet-RPC 연결 상태. 엔드포인트, 인증 모드, account-0 기본 주소, 기술 증명 표시·높이, 운영자 확인 시간을 포함해요. 인증 정보, 지갑 키, 지갑 파일은 직렬화하지 않아요. |
| last_secret_revealed_at / secret_reveal_count | timestamp|null / integer | 항상 | 콘솔의 비밀 정보 공개 감사 메타데이터. |
| next_receive_index | integer | 항상 | 다음 예약 하위 주소 인덱스. |
| last_scanned_height / last_scanned_at / last_error | integer|null / timestamp|null / string|null | 항상 | 지갑 스캐너 상태. |
| balances | WalletAssetBalance[] | 항상 | 30개 기본 체인 경로와 검증된 ERC-20·SPL 자산의 캐시 잔액. Monero에는 설정된 외부 조회 전용 wallet-RPC가 필요해요. |
| total_value_usd | decimal string | null | 항상 | 현재 USD 가격이 있는 잔액의 참고 합계. |
| balance_status | pending | refreshing | fresh | stale | error | unknown | 항상 | 집계된 캐시 최신성. unknown은 방어적 대체값이며 어떤 상태도 청구서 정산을 증명하지 않아요. |
| balance_checked_at | timestamp | null | 항상 | 집계에 포함된 관련 성공 잔액 점검 중 가장 오래된 시간. |
| recent_payments | WalletRecentPayment[] | 항상 | 정확히 이 지갑에 속하는 최신 유효 detected, confirming, final 관찰 기록 최대 3개. |
| created_at / updated_at | RFC 3339 timestamp | 항상 | 지갑 생성 및 마지막 업데이트 시간. |
ReceiveReadiness
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| ready | boolean | 항상 | 수신 설정 검사를 통과했어요. 지출 준비, 가스, 잔액 갱신, 미래 견적 보장을 뜻하지 않아요. |
| invoice_creatable | boolean | 6.0.6+ | 설정상 일시적인 스캐너 경고가 있어도 청구서 수단을 만들 수 있어요. 통화 가격은 생성 시 확인해요. 결제 검증은 아니므로 ready가 false여도 invoice_creatable은 true일 수 있어요. 지갑 없음, 비활성 정책, 미지원 어댑터는 여전히 안전하게 거부해요. |
| checked_at | timestamp | 항상 | 평가 시간. 목록 조회는 네트워크 요청이나 주소 할당을 하지 않아요. |
| issues | PaymentMethodIssue[] | 항상 | 준비되면 비어 있고 아니면 수신 경고나 설정 차단 이유가 있어요. invoice_creatable로 일시적 스캐너 경고와 청구서 설정 실패를 구분하세요. |
PaymentMethodIssue
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | 알려진 경우 | 영향받는 체인과 자산을 식별해요. Lightning은 asset_id를 생략할 수 있어요. |
| reason_code | string | 항상 | scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled 또는 asset_not_accepted. |
| message / action | string | 제공되는 경우 | 판매자용 설명 및 작업 식별자: chain_connections, wallets, rates, payment_methods, project_settings 또는 store_settings. 인증 정보나 비공개 제공업체 URL은 없어요. |
| required_endpoint_role | string | null | 온체인 | 선호 스캐너 API 역할(이전 필드). 전체 호환 목록은 accepted_endpoint_roles를 사용하세요. 기본 노드 상태로 결제 기록 지원을 증명할 수 없어요. |
| accepted_endpoint_roles | string[] | null | 온체인 | 호환 API 형식이며 엔드포인트 기록·용량을 보장하지 않아요. 원시 node-rpc는 BTC/BCH/LTC/DOGE/DASH와 투명 ZEC(완전히 디코딩한 블록, 확인 1~48회), 확정된 기본 TRX, algod의 기본 ALGO, Octez의 XTZ, SCALE 메타데이터의 최종 Asset Hub DOT, 청구서 메모 ID를 포함한 Stellar RPC의 기본 XLM을 지원해요. 잘리거나 불완전한 기록은 안 돼요. 이 원시 어댑터는 토큰 경로를 추가하지 않아요. 인덱스 API도 대안이며 아래 경로 표를 보세요. 혼합 원시·인덱스 소스는 제한된 구간을 독립 검증해요. 기본은 같은 운영자의 별칭이 아닌 독립 제공업체 두 곳이에요. 기본 노드 높이, ORDnet 체인 정보, 비 EVM 경로의 EVM 릴레이는 수신 증거가 아니에요. Monero에는 프로젝트에 연결된 조회 전용 wallet-RPC가 여전히 필요해요. |
| healthy_endpoints | integer | 온체인 | 일치하는 정상 엔드포인트 수이며 독립 제공업체 수가 아니에요. |
| usable_independent_providers / required_independent_providers | integer | 온체인 | 사용 가능한 검증 슬롯은 최대 2개예요. required_independent_providers는 체인 설정으로 기본 2, 관리자가 명시적으로 선택하면 1이에요. 두 제공업체 모드에서는 제공업체 키와 호스트가 모두 달라야 해요. 비활성, 오래된 상태(10분 초과), 대기 중 소스는 슬롯에 포함하지 않아요. Lightning은 자체 연결 규칙을 사용해요. |
| last_checked_at | timestamp | null | 온체인 | 가장 최근의 일치 엔드포인트 상태 검사. 평가 시간과 별개예요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [
{
"asset": {
"id": "10000000-0000-4000-8000-000000000003",
"asset_key": "eip155:1/slip44:60",
"chain_slug": "ethereum",
"network": "mainnet",
"caip_network_id": "eip155:1",
"caip_asset_id": "eip155:1/slip44:60",
"asset_kind": "native",
"payment_rail": "evm-native",
"symbol": "ETH",
"name": "Ethereum",
"decimals": 18,
"contract_address": null,
"coingecko_id": "ethereum",
"icon_path": "/assets/coingecko/ethereum.png",
"token_standard": null,
"metadata_verified_at": null,
"payment_supported": true,
"scanner_ready": true,
"balance_ready": true,
"default_finality_mode": "confirmations",
"default_required_confirmations": 12,
"default_monitoring_minutes": 60
},
"policy": {
"enabled": true,
"finality_mode": "confirmations",
"required_confirmations": 12,
"monitoring_minutes": 60,
"late_monitoring_days": 30
},
"wallet": null,
"wallet_readiness": "wallet_missing",
"receive_readiness": { "ready": false, "invoice_creatable": false, "checked_at": "2026-09-16T09:00:00Z", "issues": [{ "chain_slug": "ethereum", "asset_id": "10000000-0000-4000-8000-000000000003", "asset_ticker": "ETH", "reason_code": "wallet_missing", "message": "ethereum / ETH: Create a project wallet for this chain.", "action": "wallets" }] }
}
]
}PUT프로젝트 자산 정책 업데이트/v1/projects/{project_id}/payment-assets/{asset_id}읽기 + 쓰기
영구 자산 하나의 프로젝트 정책을 만들거나 교체하고 새 프로젝트 자산 목록을 반환해요. 기본 체인을 끄면 새 청구서에서 기본 자산과 토큰을 쓸 수 없지만 나중에 재개하도록 토큰 정책, 지갑, 스토어 선택을 보존해요.
- 본문은 정책 전체를 교체하며 알 수 없는 필드는 거부해요.
- 프로젝트에서 켜는 것만으로 스토어에 자산이 선택되지는 않아요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | 필수 | application/json |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 인증 정보에 지정된 활성 프로젝트. |
| asset_id | path UUID | 프로젝트 자산 목록이나 토큰 등록에서 반환한 자산 id. |
프로젝트 자산 정책 업데이트
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| enabled | boolean | 필수 | 프로젝트 자산을 켜거나 꺼요. 토큰보다 기본 체인을 먼저 켜야 해요. |
| finality_mode | confirmations | finalized | 필수 | 자산 경로가 지원하는 최종 확정 정책. finalized에는 required_confirmations=1이 필요해요. |
| required_confirmations | integer | 필수 | Bitcoin과 EVM 경로는 0을 허용하고 다른 확인 경로는 최소 1, 최종 확정 전용은 정확히 1이어야 해요. EVM은 모든 전송이 거래 재생 구간 안에 있도록 0~48로 제한돼요. |
| monitoring_minutes | integer | 필수 | 청구서 활성 중 조회 기간, 1~10,080분. |
| late_monitoring_days | integer | 필수 | 청구서 만료 후 모니터링, 0~3,650일. |
PaymentAsset
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id | UUID | 항상 | 프로젝트·스토어 정책 경로에서 사용하는 영구 결제 자산 식별자. |
| asset_key | string | 항상 | 정규 CAIP 방식 기본 코인 또는 컨트랙트 자산 식별자. |
| chain_slug / network | string | 항상 | Wholly Crypto 체인 식별자 및 설정된 네트워크. |
| caip_network_id / caip_asset_id | string / string|null | 항상 | 정규 네트워크·자산 식별자. |
| asset_kind | native | token | 항상 | 정산에 체인 통화를 쓰는지 검증된 컨트랙트·민트를 쓰는지 여부. |
| payment_rail | string | 항상 | 런타임 경로: utxo, evm-native, solana-native, account-native, privacy-native 또는 token-transfer. |
| symbol / name / decimals | string / string / integer | 항상 | 표시 식별자와 정확한 최소 단위 정밀도. |
| contract_address | string | null | 항상 | 토큰의 정규 ERC-20 컨트랙트 또는 SPL 민트. 기본 자산은 null. |
| coingecko_id | string | null | 항상 | 탐색·가격 식별자. 사용자 지정 컨트랙트는 null이며 티커로 시장가를 추론하지 마세요. CoinGecko 메타데이터만으로 토큰이 선택 가능해지지는 않아요. |
| custom_token | boolean | 항상 | 온체인 검증된 사용자 지정 컨트랙트. 프로젝트 범위 고정 USD 또는 선택한 DEX 풀 가격을 사용해요. |
| icon_path | path | null | 항상 | 가능한 경우 로컬 캐시 토큰 아이콘. |
| token_standard | erc20 | spl-token | null | 항상 | 검증된 런타임 토큰 표준. 기본 자산은 null. |
| metadata_verified_at | timestamp | null | 항상 | 등록된 토큰의 온체인 메타데이터 검증 시간. |
| payment_supported / scanner_ready / balance_ready | boolean | 항상 | 빌드 시 레지스트리 조건. scanner_ready는 결제 스캐너 런타임이 설치됐다는 뜻이에요. 결제 확인에는 설정된 수의 정상·정확한 역할 제공업체가 필요해요(기본 2개, 선택적으로 1개). 6.0.6부터 일시적인 스캐너 불가 상태는 청구서 생성을 막지 않아요. balance_ready는 구현된 잔액 어댑터에만 true예요. |
| default_finality_mode | confirmations | finalized | 항상 | 새 프로젝트 정책이 이어받는 기본 최종 확정 모델. |
| default_required_confirmations / default_monitoring_minutes | integer | 항상 | 기본 확인 및 모니터링 정책. |
ProjectPaymentAsset
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| asset | PaymentAsset | 항상 | 영구 기본 코인 또는 검증 토큰 자산. |
| policy | ProjectAssetPolicy | null | 항상 | 프로젝트 활성화·최종 확정 정책이며 미설정 시 null이에요. custom_price_mode(fixed/dex), custom_price_usd(고정 십진수 문자열 또는 null), custom_dex_pair(선택 풀 또는 null), custom_dex(dex_id, quote_symbol, 현재 price_usd 또는 null, liquidity_usd, fetched_at, last_error)를 포함해요. 프로젝트의 스토어들이 사용자 지정 가격을 공유해요. |
| wallet | WalletSummary | null | 항상 | 체인의 비수탁 프로젝트 지갑. 토큰은 기본 체인 지갑을 공유해요. |
| wallet_readiness | readiness enum | 항상 | unsupported, project_disabled, project_asset_disabled, store_disabled, store_asset_disabled, wallet_missing, wallet_pending, wallet_disabled, wallet_error, backup_required, account_activation_required, external_wallet_rpc_required 또는 ready. |
| receive_readiness | ReceiveReadiness | null | 5.5.0+ | 공유 프로젝트 수신 설정 평가. 지갑과 독립 스캐너 제공업체 검사를 포함하며 잔액 최신성·전송 가스와는 별개예요. 프로젝트 정책이 없으면 null이에요. 청구서 통화·환율은 생성 시 확인해요. |
ReceiveReadiness
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| ready | boolean | 항상 | 수신 설정 검사를 통과했어요. 지출 준비, 가스, 잔액 갱신, 미래 견적 보장을 뜻하지 않아요. |
| invoice_creatable | boolean | 6.0.6+ | 설정상 일시적인 스캐너 경고가 있어도 청구서 수단을 만들 수 있어요. 통화 가격은 생성 시 확인해요. 결제 검증은 아니므로 ready가 false여도 invoice_creatable은 true일 수 있어요. 지갑 없음, 비활성 정책, 미지원 어댑터는 여전히 안전하게 거부해요. |
| checked_at | timestamp | 항상 | 평가 시간. 목록 조회는 네트워크 요청이나 주소 할당을 하지 않아요. |
| issues | PaymentMethodIssue[] | 항상 | 준비되면 비어 있고 아니면 수신 경고나 설정 차단 이유가 있어요. invoice_creatable로 일시적 스캐너 경고와 청구서 설정 실패를 구분하세요. |
PaymentMethodIssue
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | 알려진 경우 | 영향받는 체인과 자산을 식별해요. Lightning은 asset_id를 생략할 수 있어요. |
| reason_code | string | 항상 | scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled 또는 asset_not_accepted. |
| message / action | string | 제공되는 경우 | 판매자용 설명 및 작업 식별자: chain_connections, wallets, rates, payment_methods, project_settings 또는 store_settings. 인증 정보나 비공개 제공업체 URL은 없어요. |
| required_endpoint_role | string | null | 온체인 | 선호 스캐너 API 역할(이전 필드). 전체 호환 목록은 accepted_endpoint_roles를 사용하세요. 기본 노드 상태로 결제 기록 지원을 증명할 수 없어요. |
| accepted_endpoint_roles | string[] | null | 온체인 | 호환 API 형식이며 엔드포인트 기록·용량을 보장하지 않아요. 원시 node-rpc는 BTC/BCH/LTC/DOGE/DASH와 투명 ZEC(완전히 디코딩한 블록, 확인 1~48회), 확정된 기본 TRX, algod의 기본 ALGO, Octez의 XTZ, SCALE 메타데이터의 최종 Asset Hub DOT, 청구서 메모 ID를 포함한 Stellar RPC의 기본 XLM을 지원해요. 잘리거나 불완전한 기록은 안 돼요. 이 원시 어댑터는 토큰 경로를 추가하지 않아요. 인덱스 API도 대안이며 아래 경로 표를 보세요. 혼합 원시·인덱스 소스는 제한된 구간을 독립 검증해요. 기본은 같은 운영자의 별칭이 아닌 독립 제공업체 두 곳이에요. 기본 노드 높이, ORDnet 체인 정보, 비 EVM 경로의 EVM 릴레이는 수신 증거가 아니에요. Monero에는 프로젝트에 연결된 조회 전용 wallet-RPC가 여전히 필요해요. |
| healthy_endpoints | integer | 온체인 | 일치하는 정상 엔드포인트 수이며 독립 제공업체 수가 아니에요. |
| usable_independent_providers / required_independent_providers | integer | 온체인 | 사용 가능한 검증 슬롯은 최대 2개예요. required_independent_providers는 체인 설정으로 기본 2, 관리자가 명시적으로 선택하면 1이에요. 두 제공업체 모드에서는 제공업체 키와 호스트가 모두 달라야 해요. 비활성, 오래된 상태(10분 초과), 대기 중 소스는 슬롯에 포함하지 않아요. Lightning은 자체 연결 규칙을 사용해요. |
| last_checked_at | timestamp | null | 온체인 | 가장 최근의 일치 엔드포인트 상태 검사. 평가 시간과 별개예요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request PUT \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets/YOUR_ASSET_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{
"enabled": true,
"finality_mode": "confirmations",
"required_confirmations": 2,
"monitoring_minutes": 60,
"late_monitoring_days": 30
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"enabled": true,
"finality_mode": "confirmations",
"required_confirmations": 2,
"monitoring_minutes": 60,
"late_monitoring_days": 30
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets/YOUR_ASSET_ID", {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"enabled": true,
"finality_mode": "confirmations",
"required_confirmations": 2,
"monitoring_minutes": 60,
"late_monitoring_days": 30
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets/YOUR_ASSET_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"enabled": true,
"finality_mode": "confirmations",
"required_confirmations": 2,
"monitoring_minutes": 60,
"late_monitoring_days": 30
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-assets/YOUR_ASSET_ID",
method="PUT", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [
{ "asset": { "id": "ASSET_UUID", "symbol": "USDC", "asset_kind": "token", "scanner_ready": true }, "policy": { "enabled": true, "finality_mode": "confirmations", "required_confirmations": 2, "monitoring_minutes": 60, "late_monitoring_days": 30 }, "wallet_readiness": "ready" }
]
}GET결제 토큰 후보 둘러보기/v1/projects/{project_id}/payment-token-candidates읽기 전용
토큰 청구서 스캐너와 잔액 어댑터가 구현된 체인에서만 로컬 캐시 CoinGecko 컨트랙트 매핑을 검색해요. 결과는 탐색 후보이며 신뢰된 결제 자산이 아니에요.
- 지원 토큰 어댑터: Ethereum, Base, BNB Chain, HyperEVM, Avalanche, Polygon, Arbitrum, Optimism의 ERC-20과 Solana의 SPL.
- 지원하지 않는 목록 체인은 선택 가능하게 보이지 않고 거부돼요.
- CoinGecko 순위, 아이콘, 가격은 참고용 탐색 데이터예요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 인증 정보에 지정된 활성 프로젝트. |
| chain_slug | query string | 필수 지원 EVM 체인 슬러그 또는 solana. |
| q | query string | 선택적 이름, 기호, CoinGecko id, 컨트랙트, 민트 부분 문자열. 최대 80자. |
| limit | query integer | 선택 사항, 1~100. 기본값 50. |
TokenCandidate
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| coingecko_id | string | 항상 | 등록 요청에 사용하는 CoinGecko 탐색 식별자. |
| chain_slug | string | 항상 | 일치하는 Wholly Crypto 체인. |
| symbol / name | string | 항상 | 목록 표시 식별자. |
| contract_address | string | 항상 | 일치하는 컨트랙트 또는 민트. 등록 전에 온체인 검증해요. |
| market_cap_rank | integer | null | 항상 | 탐색 순위이며 신뢰도나 결제 준비 신호가 아니에요. |
| icon_path | path | 항상 | 로컬 캐시 CoinGecko 아이콘 경로. |
| current_price_usd | decimal string | null | 항상 | 참고용 캐시 USD 가격. |
| token_standard | erc20 | spl-token | 항상 | 선택한 체인 어댑터가 지원하는 토큰 표준. |
| scanner_ready | boolean | 항상 | 이 빌드에 구현된 토큰 경로의 후보에만 true. |
| registered_asset_id | UUID | null | 항상 | 이미 등록된 경우 기존 영구 자산. |
| project_enabled | boolean | 항상 | 등록된 자산의 해당 프로젝트 활성 여부. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-candidates?chain_slug=ethereum&q=USDC&limit=50" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-candidates?chain_slug=ethereum&q=USDC&limit=50", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-candidates?chain_slug=ethereum&q=USDC&limit=50");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-candidates?chain_slug=ethereum&q=USDC&limit=50",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [
{
"coingecko_id": "usd-coin",
"chain_slug": "ethereum",
"symbol": "USDC",
"name": "USDC",
"contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"market_cap_rank": 7,
"icon_path": "/assets/coingecko/usd-coin.png",
"current_price_usd": "1.0001",
"token_standard": "erc20",
"scanner_ready": true,
"registered_asset_id": null,
"project_enabled": false
}
]
}POST토큰 검증 및 등록/v1/projects/{project_id}/payment-token-assets읽기 + 쓰기
설정된 노드가 체인 식별, 컨트랙트·민트 식별, 소수 자릿수, 사용 가능한 잔액 조회를 검증한 뒤에만 현재 후보를 영구 결제 레지스트리에 등록해요. CoinGecko 메타데이터만 믿지 않으며 프로젝트당 등록 토큰은 최대 20개예요.
- 토큰을 등록하기 전에 체인의 기본 프로젝트 자산을 켜세요.
- 프로젝트는 토큰 자산을 최대 20개 등록할 수 있어요. 한도를 넘는 새 후보는 token_chain_not_ready(409)를 반환해요. 기존 등록 자산 재사용은 슬롯을 추가로 쓰지 않아요.
- 노드 검증은 목록 조회보다 오래 걸릴 수 있으니 명시적 클라이언트 제한 시간을 설정하세요.
- 등록 후 해당 자산을 제공할 각 스토어에서 선택하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | 필수 | application/json |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 인증 정보에 지정된 활성 프로젝트. |
토큰 등록 본문
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| chain_slug | string | 필수 | ethereum, base, bnb-chain, hyperliquid, avalanche, polygon, arbitrum, optimism 또는 solana. |
| coingecko_id | string | 필수 | 토큰 검색이 반환한 정확한 후보 식별자. _ 또는 -6처럼 앞의 밑줄·하이픈을 유지하세요. 토큰 이름이나 티커로 ID를 추론하지 마세요. |
| enabled | boolean | 선택 사항 | 검증 후 프로젝트 정책 상태. 기본값 true. |
RegisteredTokenAsset
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| asset_id | UUID | 항상 | 영구 결제 자산 식별자. |
| chain_slug / coingecko_id | string | 항상 | 검증된 체인과 유지된 탐색·가격 식별자. |
| contract_address | string | 항상 | 정규 검증 컨트랙트 또는 민트. |
| token_standard | erc20 | spl-token | 항상 | 검증된 런타임 토큰 표준. |
| symbol / name / decimals | string / string / integer | 항상 | 등록된 표시 식별자와 정확한 정밀도. |
| enabled | boolean | 항상 | 초기 프로젝트 정책 상태. |
| metadata_verified_at | RFC 3339 timestamp | 항상 | 온체인 검증 시간. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{
"chain_slug": "ethereum",
"coingecko_id": "usd-coin",
"enabled": true
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"chain_slug": "ethereum",
"coingecko_id": "usd-coin",
"enabled": true
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"chain_slug": "ethereum",
"coingecko_id": "usd-coin",
"enabled": true
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"chain_slug": "ethereum",
"coingecko_id": "usd-coin",
"enabled": true
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 201 application/json
{
"data": {
"asset_id": "44444444-4444-4444-8444-444444444444",
"chain_slug": "ethereum",
"coingecko_id": "usd-coin",
"contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"token_standard": "erc20",
"symbol": "USDC",
"name": "USDC",
"decimals": 6,
"enabled": true,
"metadata_verified_at": "2026-08-31T18:00:00Z"
}
}GET사용자 지정 토큰 DEX 풀 찾기/v1/projects/{project_id}/payment-token-dex-pools읽기 전용
DEX Screener로 정확한 체인과 기본 토큰 컨트랙트의 적격 풀을 최대 12개 찾아 유동성순으로 정렬해요. 토큰을 등록하거나 켜지는 않아요.
- 빈 data 배열은 적격 풀이 없다는 뜻이에요. 요청한 정확한 컨트랙트가 기본 토큰인 풀만 반환하며 견적 측 USD 가격을 가정하지 않아요.
- DEX 등록은 보안 감사가 아니에요. 최소 유동성과 최근 활동은 쓸 수 없는 견적을 줄이지만 시장 조작을 막지는 못해요.
- 기존 체인 스캐너가 토큰을 지원하는 곳에서 Uniswap, PancakeSwap 등의 인덱싱된 DEX를 지원해요. API는 프로젝트 범위와 요청 제한을 유지해요. 제공업체 호출도 직렬화하고 속도를 제한해요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 지정된 프로젝트. |
| chain_slug | query string | 지원 EVM 토큰 체인 또는 solana. |
| contract_address | query string | 정확한 ERC-20 컨트랙트 또는 기존 SPL 민트. |
CustomDexPool
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| pair_address / dex_id / quote_symbol | string | 항상 | 정확한 풀 식별자, 거래소 ID(예: uniswap/pancakeswap), 표시 전용 페어 티커. |
| price_usd / liquidity_usd | decimal string | 항상 | 요청한 기본 토큰의 USD 가격과 풀 전체 유동성. 최소 $10,000 유동성과 최근 1시간 내 거래가 필요해요. |
| fetched_at | RFC 3339 timestamp | 항상 | 온체인 거래 시간이 아닌 서버가 제공업체 관찰값을 조회한 시간. |
| url | HTTPS URL | 항상 | 이 풀의 검증된 DEX Screener 링크. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-dex-pools?chain_slug=ethereum&contract_address=YOUR_TOKEN_CONTRACT" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-dex-pools?chain_slug=ethereum&contract_address=YOUR_TOKEN_CONTRACT", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-dex-pools?chain_slug=ethereum&contract_address=YOUR_TOKEN_CONTRACT");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-dex-pools?chain_slug=ethereum&contract_address=YOUR_TOKEN_CONTRACT",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{"data":[{"pair_address":"0x2222222222222222222222222222222222222222","dex_id":"uniswap","quote_symbol":"WETH","price_usd":"0.25","liquidity_usd":"250000.00","fetched_at":"2026-09-09T12:00:00Z","url":"https://dexscreener.com/ethereum/0x2222222222222222222222222222222222222222"}]}POST사용자 지정 토큰 추가 또는 가격 재설정/v1/projects/{project_id}/payment-token-assets/custom읽기 + 쓰기
설정된 체인 노드로 사용자 지정 컨트랙트를 검증하고 CoinGecko 등록 없이 추가해요. 고정 USD 가격이나 선택한 자동 DEX 풀은 티커나 다른 프로젝트가 아닌 이 프로젝트에 속해요. 같은 식별자로 반복하면 기존 활성·비활성 정책은 유지하고 프로젝트 가격을 업데이트해요.
- 등록 후 스토어 payment-assets 엔드포인트에서 asset_id를 선택하세요. 등록만으로 스토어 수단이 켜지지는 않아요.
- 사용자 지정 토큰과 목록 토큰은 프로젝트당 20개 한도를 공유해요. 체인이 다르면 같은 컨트랙트도 다른 결제 자산이에요.
- 기존 목록 컨트랙트는 409를 반환해요. 자동 시장 환율을 유지하려면 목록 등록을 쓰세요. 사용자 지정 티커는 같은 이름 토큰의 가격을 빌리지 않아요.
- 고정 가격은 운영자의 추정이에요. 자동 DEX 가격은 DEX Screener가 선택한 풀에서 관찰한 현물 가격이며 조작 방지 오라클이 아니에요. 최신 법정화폐 환율과 스토어 스프레드·올림은 계속 적용돼요. 이미 발급한 견적은 바뀌지 않아요.
- DEX 모드는 먼저 풀을 찾고 price_mode: dex와 dex_pair_address를 보내며 price_usd는 생략하세요. 공유 백그라운드 작업이 매분 선택한 풀을 갱신해요. 검사가 실패하거나 가격이 5분을 넘으면 새 견적에서 토큰을 제외하며 고정 가격이나 티커로 조용히 대체하지 않아요.
- 표준 ERC-20과 기존 SPL 토큰만 허용해요. Token-2022·확장과 기본 코인 전용 체인은 거부해요. 기술 검증은 발행자·컨트랙트 보안 감사가 아니에요. 전송 수수료, 리베이스, 차단 목록 토큰은 호환되지 않을 수 있어요.
- 클라이언트 제한 시간은 최소 60초로 설정하세요. 검증은 제한 안에서 대체 노드를 시도할 수 있어요. 잘못된 입력은 400, 체인·컨트랙트 검사 실패는 422, 식별 충돌·한도 초과는 409예요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | 필수 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 쓰기 가능한 인증 정보에 지정된 프로젝트. |
사용자 지정 토큰 등록
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| chain_slug | string | 필수 | ethereum, base, bnb-chain, hyperliquid, avalanche, polygon, arbitrum, optimism 또는 solana. 이 컨트랙트에 고정돼요. |
| contract_address | string | 필수 | ERC-20 컨트랙트(0x와 16진수 40자) 또는 기존 SPL 민트. 노드가 네트워크 식별과 정확한 소수 자릿수를 검증하며 호출자가 보낸 소수 자릿수·RPC URL은 거부해요. |
| name / symbol | string / string | 필수 | 표시 이름(1~80자)과 티커(문자·숫자·점·밑줄·하이픈 1~16자, 첫 글자는 영숫자). 기존 식별자는 이 엔드포인트로 이름을 바꿀 수 없어요. |
| price_mode | fixed | dex | 선택 사항 | 하위 호환을 위해 기본은 fixed예요. DEX는 정확한 체인과 컨트랙트로 찾은 특정 풀을 사용해요. |
| price_usd | decimal string | fixed 모드 | 토큰 1개의 고정 USD 값. 양수, 소수 최대 30자리, 최댓값 1000000000000000000000000이에요. 지수나 부동소수점은 안 돼요. dex 모드에서는 생략하세요. |
| dex_pair_address | string | dex 모드 | payment-token-dex-pools의 풀 주소. dex 모드에 필수이며 fixed에서는 생략해요. 저장마다 서버가 풀 식별, 가격, 유동성, 활동을 다시 확인해요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets/custom" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{
"chain_slug": "ethereum",
"contract_address": "YOUR_VERIFIED_TOKEN_CONTRACT",
"name": "Example token",
"symbol": "EXAMPLE",
"price_usd": "0.25"
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"chain_slug": "ethereum",
"contract_address": "YOUR_VERIFIED_TOKEN_CONTRACT",
"name": "Example token",
"symbol": "EXAMPLE",
"price_usd": "0.25"
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets/custom", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"chain_slug": "ethereum",
"contract_address": "YOUR_VERIFIED_TOKEN_CONTRACT",
"name": "Example token",
"symbol": "EXAMPLE",
"price_usd": "0.25"
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets/custom");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"chain_slug": "ethereum",
"contract_address": "YOUR_VERIFIED_TOKEN_CONTRACT",
"name": "Example token",
"symbol": "EXAMPLE",
"price_usd": "0.25"
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/payment-token-assets/custom",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{"data":{"asset_id":"44444444-4444-4444-8444-444444444444"}}GET스토어 결제 수단 목록/v1/projects/{project_id}/stores/{store_id}/payment-assets읽기 전용
data에는 온체인 자산, lightning에는 별도 Lightning 준비 상태를 나열해요. 온체인 수단은 준비된 체인 지갑이 필요해요. Lightning은 온체인 Bitcoin 지갑과 별도로 스토어에서 선택한 검증된 외부 수신 연결을 사용해요.
- selected는 온체인 설정이고 wallet_readiness는 현재 자격 조건이에요.
- lightning 응답에는 payment_rail: lightning, symbol: BTC, asset_decimals: 11, enabled, ready가 있고 노드 인증 정보는 없어요. 스토어 콘솔에서 설정하세요. assets 배열 업데이트는 Lightning을 바꾸지 않아요.
- confirmation_policy는 온체인 수단에만 적용돼요. Lightning은 블록 확인 없이 정산하며 부분 결제 허용 오차 없이 BOLT11 전액이 필요해요.
- 같은 체인의 기본 코인·토큰 수단은 해당 체인 지갑의 같은 청구서 수신 주소를 사용해요.
- 포함된 지갑 요약은 준비 상태만 보여주고 잔액은 비어 있어요. 현재 값은 전용 프로젝트 지갑 경로를 사용하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 인증 정보에 지정된 프로젝트. 일시 중지 상태일 수 있어요. |
| store_id | path UUID | project_id에 속한 스토어. 일시 중지 상태일 수 있어요. |
PaymentAsset
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id | UUID | 항상 | 프로젝트·스토어 정책 경로에서 사용하는 영구 결제 자산 식별자. |
| asset_key | string | 항상 | 정규 CAIP 방식 기본 코인 또는 컨트랙트 자산 식별자. |
| chain_slug / network | string | 항상 | Wholly Crypto 체인 식별자 및 설정된 네트워크. |
| caip_network_id / caip_asset_id | string / string|null | 항상 | 정규 네트워크·자산 식별자. |
| asset_kind | native | token | 항상 | 정산에 체인 통화를 쓰는지 검증된 컨트랙트·민트를 쓰는지 여부. |
| payment_rail | string | 항상 | 런타임 경로: utxo, evm-native, solana-native, account-native, privacy-native 또는 token-transfer. |
| symbol / name / decimals | string / string / integer | 항상 | 표시 식별자와 정확한 최소 단위 정밀도. |
| contract_address | string | null | 항상 | 토큰의 정규 ERC-20 컨트랙트 또는 SPL 민트. 기본 자산은 null. |
| coingecko_id | string | null | 항상 | 탐색·가격 식별자. 사용자 지정 컨트랙트는 null이며 티커로 시장가를 추론하지 마세요. CoinGecko 메타데이터만으로 토큰이 선택 가능해지지는 않아요. |
| custom_token | boolean | 항상 | 온체인 검증된 사용자 지정 컨트랙트. 프로젝트 범위 고정 USD 또는 선택한 DEX 풀 가격을 사용해요. |
| icon_path | path | null | 항상 | 가능한 경우 로컬 캐시 토큰 아이콘. |
| token_standard | erc20 | spl-token | null | 항상 | 검증된 런타임 토큰 표준. 기본 자산은 null. |
| metadata_verified_at | timestamp | null | 항상 | 등록된 토큰의 온체인 메타데이터 검증 시간. |
| payment_supported / scanner_ready / balance_ready | boolean | 항상 | 빌드 시 레지스트리 조건. scanner_ready는 결제 스캐너 런타임이 설치됐다는 뜻이에요. 결제 확인에는 설정된 수의 정상·정확한 역할 제공업체가 필요해요(기본 2개, 선택적으로 1개). 6.0.6부터 일시적인 스캐너 불가 상태는 청구서 생성을 막지 않아요. balance_ready는 구현된 잔액 어댑터에만 true예요. |
| default_finality_mode | confirmations | finalized | 항상 | 새 프로젝트 정책이 이어받는 기본 최종 확정 모델. |
| default_required_confirmations / default_monitoring_minutes | integer | 항상 | 기본 확인 및 모니터링 정책. |
StorePaymentAsset
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| asset | PaymentAsset | 항상 | 프로젝트에 보이는 기본 코인 또는 검증 토큰 자산. |
| project_policy | ProjectAssetPolicy | null | 항상 | 상위 프로젝트 정책. |
| selected | boolean | 항상 | 수단이 스토어에 저장된 원하는 설정에 포함되는지 여부예요. 프로젝트 정책, 지갑, 설치된 어댑터, 가격이 유효하면 제공해요. 일시적 스캐너 장애로 새 청구서에서 제거하지 않아요. |
| display_order | integer | null | 항상 | 선택된 경우 스토어 결제 화면 순서. |
| confirmation_policy | StoreConfirmationPolicy | null | 항상 | 프로젝트에 설정된 자산의 유효 스토어 정책. 프로젝트 정책이 없으면 null. |
| wallet | WalletSummary | null | 항상 | 기본 코인·토큰이 공유하는 체인 지갑. |
| wallet_readiness | readiness enum | 항상 | 지갑·정책 상태만 제공해요. 스캐너 전제 조건은 receive_readiness를 사용하세요. |
| receive_readiness | ReceiveReadiness | null | 5.5.0+ | 공유 수신 설정과 스토어 수락 상태. 캐시 관찰값이며 예약이나 보장이 아니에요. 생성 시 요건과 실제 청구서 환율을 다시 확인해요. |
StoreConfirmationPolicy
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| finality_mode | confirmations | finalized | 항상 | 정산에 설정 가능한 블록 수를 쓰는지 네트워크 최종 확정을 쓰는지 여부. |
| project_required_confirmations | integer | 항상 | 스토어 재정의가 없을 때 향후 청구서가 사용할 현재 프로젝트 기본값. |
| override_required_confirmations | integer | null | 항상 | 스토어별 횟수. null이면 프로젝트 기본값을 이어받아요. |
| effective_required_confirmations | integer | 항상 | 이 스토어·자산의 새 청구서에 저장할 확인 횟수. |
| editable | boolean | 항상 | 최종 확정 정책을 바꿀 수 없는 finalized 네트워크는 false. |
| minimum_required_confirmations | integer | 항상 | 경계를 포함한 체인별 하한. 감지 시 수락을 지원하는 경로에만 0을 표시해요. |
| maximum_required_confirmations | integer | 항상 | 경계를 포함한 체인별 상한. |
WalletSummary
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id / project_id / native_asset_id | UUID | 항상 | 지갑, 소유 프로젝트, 체인 기본 자산 식별자. |
| chain_slug / network | string | 항상 | 지갑 체인과 네트워크. |
| asset_symbol / asset_name | string | 항상 | 체인 기본 코인 표시 식별자. |
| status | pending | active | disabled | error | 항상 | 지갑 운영 상태. |
| label | string | 항상 | 운영자 라벨. |
| public_key / primary_address | string | null | 항상 | 공개 지갑 식별자. 시드 구문이나 개인 키는 노출하지 않아요. |
| derivation_scheme / address_format | string | null | 항상 | 주소 정책과 형식. |
| backup_confirmed_at | timestamp | null | 항상 | 운영자가 복구 백업을 확인하면 null이 아니에요. |
| activation_required / activation_verified_at | boolean / timestamp|null | 항상 | XRP·Stellar 공유 계정은 운영자가 표시된 주소에 자금을 넣고 설정된 스캐너 제공업체가 정확한 계정을 검증해야 사용할 수 있어요. 영구 증명은 만료되지 않아요. 실시간 스캐너 상태는 청구서 생성이 아닌 결제 검증용으로 별도 확인해요. |
| receive_readiness | ReceiveReadiness | null | 5.5.0+ | 지갑 목록에는 프로젝트 수신 설정과 체인 스캐너 전제 조건이 포함돼요. 잔액, 토큰 가스, 전송 준비 상태와 별개예요. 다른 지갑 응답에서는 null일 수 있어요. |
| monero_wallet_rpc | MoneroWalletRpcBinding | null | 항상 | 비밀 정보를 제거한 Monero 외부 조회 전용 wallet-RPC 연결 상태. 엔드포인트, 인증 모드, account-0 기본 주소, 기술 증명 표시·높이, 운영자 확인 시간을 포함해요. 인증 정보, 지갑 키, 지갑 파일은 직렬화하지 않아요. |
| last_secret_revealed_at / secret_reveal_count | timestamp|null / integer | 항상 | 콘솔의 비밀 정보 공개 감사 메타데이터. |
| next_receive_index | integer | 항상 | 다음 예약 하위 주소 인덱스. |
| last_scanned_height / last_scanned_at / last_error | integer|null / timestamp|null / string|null | 항상 | 지갑 스캐너 상태. |
| balances | WalletAssetBalance[] | 항상 | 30개 기본 체인 경로와 검증된 ERC-20·SPL 자산의 캐시 잔액. Monero에는 설정된 외부 조회 전용 wallet-RPC가 필요해요. |
| total_value_usd | decimal string | null | 항상 | 현재 USD 가격이 있는 잔액의 참고 합계. |
| balance_status | pending | refreshing | fresh | stale | error | unknown | 항상 | 집계된 캐시 최신성. unknown은 방어적 대체값이며 어떤 상태도 청구서 정산을 증명하지 않아요. |
| balance_checked_at | timestamp | null | 항상 | 집계에 포함된 관련 성공 잔액 점검 중 가장 오래된 시간. |
| recent_payments | WalletRecentPayment[] | 항상 | 정확히 이 지갑에 속하는 최신 유효 detected, confirming, final 관찰 기록 최대 3개. |
| created_at / updated_at | RFC 3339 timestamp | 항상 | 지갑 생성 및 마지막 업데이트 시간. |
ReceiveReadiness
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| ready | boolean | 항상 | 수신 설정 검사를 통과했어요. 지출 준비, 가스, 잔액 갱신, 미래 견적 보장을 뜻하지 않아요. |
| invoice_creatable | boolean | 6.0.6+ | 설정상 일시적인 스캐너 경고가 있어도 청구서 수단을 만들 수 있어요. 통화 가격은 생성 시 확인해요. 결제 검증은 아니므로 ready가 false여도 invoice_creatable은 true일 수 있어요. 지갑 없음, 비활성 정책, 미지원 어댑터는 여전히 안전하게 거부해요. |
| checked_at | timestamp | 항상 | 평가 시간. 목록 조회는 네트워크 요청이나 주소 할당을 하지 않아요. |
| issues | PaymentMethodIssue[] | 항상 | 준비되면 비어 있고 아니면 수신 경고나 설정 차단 이유가 있어요. invoice_creatable로 일시적 스캐너 경고와 청구서 설정 실패를 구분하세요. |
PaymentMethodIssue
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | 알려진 경우 | 영향받는 체인과 자산을 식별해요. Lightning은 asset_id를 생략할 수 있어요. |
| reason_code | string | 항상 | scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled 또는 asset_not_accepted. |
| message / action | string | 제공되는 경우 | 판매자용 설명 및 작업 식별자: chain_connections, wallets, rates, payment_methods, project_settings 또는 store_settings. 인증 정보나 비공개 제공업체 URL은 없어요. |
| required_endpoint_role | string | null | 온체인 | 선호 스캐너 API 역할(이전 필드). 전체 호환 목록은 accepted_endpoint_roles를 사용하세요. 기본 노드 상태로 결제 기록 지원을 증명할 수 없어요. |
| accepted_endpoint_roles | string[] | null | 온체인 | 호환 API 형식이며 엔드포인트 기록·용량을 보장하지 않아요. 원시 node-rpc는 BTC/BCH/LTC/DOGE/DASH와 투명 ZEC(완전히 디코딩한 블록, 확인 1~48회), 확정된 기본 TRX, algod의 기본 ALGO, Octez의 XTZ, SCALE 메타데이터의 최종 Asset Hub DOT, 청구서 메모 ID를 포함한 Stellar RPC의 기본 XLM을 지원해요. 잘리거나 불완전한 기록은 안 돼요. 이 원시 어댑터는 토큰 경로를 추가하지 않아요. 인덱스 API도 대안이며 아래 경로 표를 보세요. 혼합 원시·인덱스 소스는 제한된 구간을 독립 검증해요. 기본은 같은 운영자의 별칭이 아닌 독립 제공업체 두 곳이에요. 기본 노드 높이, ORDnet 체인 정보, 비 EVM 경로의 EVM 릴레이는 수신 증거가 아니에요. Monero에는 프로젝트에 연결된 조회 전용 wallet-RPC가 여전히 필요해요. |
| healthy_endpoints | integer | 온체인 | 일치하는 정상 엔드포인트 수이며 독립 제공업체 수가 아니에요. |
| usable_independent_providers / required_independent_providers | integer | 온체인 | 사용 가능한 검증 슬롯은 최대 2개예요. required_independent_providers는 체인 설정으로 기본 2, 관리자가 명시적으로 선택하면 1이에요. 두 제공업체 모드에서는 제공업체 키와 호스트가 모두 달라야 해요. 비활성, 오래된 상태(10분 초과), 대기 중 소스는 슬롯에 포함하지 않아요. Lightning은 자체 연결 규칙을 사용해요. |
| last_checked_at | timestamp | null | 온체인 | 가장 최근의 일치 엔드포인트 상태 검사. 평가 시간과 별개예요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [
{ "asset": { "id": "ASSET_UUID", "chain_slug": "ethereum", "symbol": "USDC", "asset_kind": "token", "token_standard": "erc20", "scanner_ready": true }, "project_policy": { "enabled": true, "required_confirmations": 12 }, "selected": true, "display_order": 0, "confirmation_policy": { "finality_mode": "confirmations", "project_required_confirmations": 12, "override_required_confirmations": 3, "effective_required_confirmations": 3, "editable": true, "minimum_required_confirmations": 0, "maximum_required_confirmations": 48 }, "wallet": { "id": "WALLET_UUID", "status": "active" }, "wallet_readiness": "ready" }
],
"lightning": { "payment_rail": "lightning", "symbol": "BTC", "asset_decimals": 11, "enabled": true, "ready": true }
}PUT스토어 결제 수단 교체/v1/projects/{project_id}/stores/{store_id}/payment-assets읽기 + 쓰기
스토어의 전체 정렬 자산 하위 집합을 원자적으로 교체하고 새 목록을 반환해요. 생략한 자산은 선택 해제돼요.
- 배열은 고유한 자산과 표시 순서를 최대 64개 허용해요.
- 선택지는 저장된 원하는 설정으로, 지갑 백업 전이나 체인 중지 중에도 미리 설정할 수 있어요. 청구서 생성은 프로젝트 정책, 기본 자산 정책, 지갑, 런타임 검사가 준비된 수단만 제공해요.
- 빈 assets 배열을 보내면 결제 수단을 없앨 수 있어요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | 필수 | application/json |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 인증 정보에 지정된 프로젝트. 일시 중지 상태일 수 있어요. |
| store_id | path UUID | project_id에 속한 스토어. 일시 중지 상태일 수 있어요. |
스토어 결제 자산 선택 본문
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| assets | StoreAssetSelection[] | 필수 | 전체 교체 목록, 최대 64개. 각 항목은 고유 asset_id와 0~10,000의 고유 display_order를 포함해요. |
PaymentAsset
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id | UUID | 항상 | 프로젝트·스토어 정책 경로에서 사용하는 영구 결제 자산 식별자. |
| asset_key | string | 항상 | 정규 CAIP 방식 기본 코인 또는 컨트랙트 자산 식별자. |
| chain_slug / network | string | 항상 | Wholly Crypto 체인 식별자 및 설정된 네트워크. |
| caip_network_id / caip_asset_id | string / string|null | 항상 | 정규 네트워크·자산 식별자. |
| asset_kind | native | token | 항상 | 정산에 체인 통화를 쓰는지 검증된 컨트랙트·민트를 쓰는지 여부. |
| payment_rail | string | 항상 | 런타임 경로: utxo, evm-native, solana-native, account-native, privacy-native 또는 token-transfer. |
| symbol / name / decimals | string / string / integer | 항상 | 표시 식별자와 정확한 최소 단위 정밀도. |
| contract_address | string | null | 항상 | 토큰의 정규 ERC-20 컨트랙트 또는 SPL 민트. 기본 자산은 null. |
| coingecko_id | string | null | 항상 | 탐색·가격 식별자. 사용자 지정 컨트랙트는 null이며 티커로 시장가를 추론하지 마세요. CoinGecko 메타데이터만으로 토큰이 선택 가능해지지는 않아요. |
| custom_token | boolean | 항상 | 온체인 검증된 사용자 지정 컨트랙트. 프로젝트 범위 고정 USD 또는 선택한 DEX 풀 가격을 사용해요. |
| icon_path | path | null | 항상 | 가능한 경우 로컬 캐시 토큰 아이콘. |
| token_standard | erc20 | spl-token | null | 항상 | 검증된 런타임 토큰 표준. 기본 자산은 null. |
| metadata_verified_at | timestamp | null | 항상 | 등록된 토큰의 온체인 메타데이터 검증 시간. |
| payment_supported / scanner_ready / balance_ready | boolean | 항상 | 빌드 시 레지스트리 조건. scanner_ready는 결제 스캐너 런타임이 설치됐다는 뜻이에요. 결제 확인에는 설정된 수의 정상·정확한 역할 제공업체가 필요해요(기본 2개, 선택적으로 1개). 6.0.6부터 일시적인 스캐너 불가 상태는 청구서 생성을 막지 않아요. balance_ready는 구현된 잔액 어댑터에만 true예요. |
| default_finality_mode | confirmations | finalized | 항상 | 새 프로젝트 정책이 이어받는 기본 최종 확정 모델. |
| default_required_confirmations / default_monitoring_minutes | integer | 항상 | 기본 확인 및 모니터링 정책. |
StorePaymentAsset
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| asset | PaymentAsset | 항상 | 프로젝트에 보이는 기본 코인 또는 검증 토큰 자산. |
| project_policy | ProjectAssetPolicy | null | 항상 | 상위 프로젝트 정책. |
| selected | boolean | 항상 | 수단이 스토어에 저장된 원하는 설정에 포함되는지 여부예요. 프로젝트 정책, 지갑, 설치된 어댑터, 가격이 유효하면 제공해요. 일시적 스캐너 장애로 새 청구서에서 제거하지 않아요. |
| display_order | integer | null | 항상 | 선택된 경우 스토어 결제 화면 순서. |
| confirmation_policy | StoreConfirmationPolicy | null | 항상 | 프로젝트에 설정된 자산의 유효 스토어 정책. 프로젝트 정책이 없으면 null. |
| wallet | WalletSummary | null | 항상 | 기본 코인·토큰이 공유하는 체인 지갑. |
| wallet_readiness | readiness enum | 항상 | 지갑·정책 상태만 제공해요. 스캐너 전제 조건은 receive_readiness를 사용하세요. |
| receive_readiness | ReceiveReadiness | null | 5.5.0+ | 공유 수신 설정과 스토어 수락 상태. 캐시 관찰값이며 예약이나 보장이 아니에요. 생성 시 요건과 실제 청구서 환율을 다시 확인해요. |
StoreConfirmationPolicy
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| finality_mode | confirmations | finalized | 항상 | 정산에 설정 가능한 블록 수를 쓰는지 네트워크 최종 확정을 쓰는지 여부. |
| project_required_confirmations | integer | 항상 | 스토어 재정의가 없을 때 향후 청구서가 사용할 현재 프로젝트 기본값. |
| override_required_confirmations | integer | null | 항상 | 스토어별 횟수. null이면 프로젝트 기본값을 이어받아요. |
| effective_required_confirmations | integer | 항상 | 이 스토어·자산의 새 청구서에 저장할 확인 횟수. |
| editable | boolean | 항상 | 최종 확정 정책을 바꿀 수 없는 finalized 네트워크는 false. |
| minimum_required_confirmations | integer | 항상 | 경계를 포함한 체인별 하한. 감지 시 수락을 지원하는 경로에만 0을 표시해요. |
| maximum_required_confirmations | integer | 항상 | 경계를 포함한 체인별 상한. |
ReceiveReadiness
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| ready | boolean | 항상 | 수신 설정 검사를 통과했어요. 지출 준비, 가스, 잔액 갱신, 미래 견적 보장을 뜻하지 않아요. |
| invoice_creatable | boolean | 6.0.6+ | 설정상 일시적인 스캐너 경고가 있어도 청구서 수단을 만들 수 있어요. 통화 가격은 생성 시 확인해요. 결제 검증은 아니므로 ready가 false여도 invoice_creatable은 true일 수 있어요. 지갑 없음, 비활성 정책, 미지원 어댑터는 여전히 안전하게 거부해요. |
| checked_at | timestamp | 항상 | 평가 시간. 목록 조회는 네트워크 요청이나 주소 할당을 하지 않아요. |
| issues | PaymentMethodIssue[] | 항상 | 준비되면 비어 있고 아니면 수신 경고나 설정 차단 이유가 있어요. invoice_creatable로 일시적 스캐너 경고와 청구서 설정 실패를 구분하세요. |
PaymentMethodIssue
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | 알려진 경우 | 영향받는 체인과 자산을 식별해요. Lightning은 asset_id를 생략할 수 있어요. |
| reason_code | string | 항상 | scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled 또는 asset_not_accepted. |
| message / action | string | 제공되는 경우 | 판매자용 설명 및 작업 식별자: chain_connections, wallets, rates, payment_methods, project_settings 또는 store_settings. 인증 정보나 비공개 제공업체 URL은 없어요. |
| required_endpoint_role | string | null | 온체인 | 선호 스캐너 API 역할(이전 필드). 전체 호환 목록은 accepted_endpoint_roles를 사용하세요. 기본 노드 상태로 결제 기록 지원을 증명할 수 없어요. |
| accepted_endpoint_roles | string[] | null | 온체인 | 호환 API 형식이며 엔드포인트 기록·용량을 보장하지 않아요. 원시 node-rpc는 BTC/BCH/LTC/DOGE/DASH와 투명 ZEC(완전히 디코딩한 블록, 확인 1~48회), 확정된 기본 TRX, algod의 기본 ALGO, Octez의 XTZ, SCALE 메타데이터의 최종 Asset Hub DOT, 청구서 메모 ID를 포함한 Stellar RPC의 기본 XLM을 지원해요. 잘리거나 불완전한 기록은 안 돼요. 이 원시 어댑터는 토큰 경로를 추가하지 않아요. 인덱스 API도 대안이며 아래 경로 표를 보세요. 혼합 원시·인덱스 소스는 제한된 구간을 독립 검증해요. 기본은 같은 운영자의 별칭이 아닌 독립 제공업체 두 곳이에요. 기본 노드 높이, ORDnet 체인 정보, 비 EVM 경로의 EVM 릴레이는 수신 증거가 아니에요. Monero에는 프로젝트에 연결된 조회 전용 wallet-RPC가 여전히 필요해요. |
| healthy_endpoints | integer | 온체인 | 일치하는 정상 엔드포인트 수이며 독립 제공업체 수가 아니에요. |
| usable_independent_providers / required_independent_providers | integer | 온체인 | 사용 가능한 검증 슬롯은 최대 2개예요. required_independent_providers는 체인 설정으로 기본 2, 관리자가 명시적으로 선택하면 1이에요. 두 제공업체 모드에서는 제공업체 키와 호스트가 모두 달라야 해요. 비활성, 오래된 상태(10분 초과), 대기 중 소스는 슬롯에 포함하지 않아요. Lightning은 자체 연결 규칙을 사용해요. |
| last_checked_at | timestamp | null | 온체인 | 가장 최근의 일치 엔드포인트 상태 검사. 평가 시간과 별개예요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request PUT \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{
"assets": [
{
"asset_id": "YOUR_ASSET_ID",
"display_order": 0
}
]
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"assets": [
{
"asset_id": "YOUR_ASSET_ID",
"display_order": 0
}
]
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets", {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"assets": [
{
"asset_id": "YOUR_ASSET_ID",
"display_order": 0
}
]
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"assets": [
{
"asset_id": "YOUR_ASSET_ID",
"display_order": 0
}
]
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets",
method="PUT", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [
{ "asset": { "id": "44444444-4444-4444-8444-444444444444", "symbol": "USDC" }, "selected": true, "display_order": 0, "confirmation_policy": { "finality_mode": "confirmations", "project_required_confirmations": 12, "override_required_confirmations": null, "effective_required_confirmations": 12, "editable": true, "minimum_required_confirmations": 0, "maximum_required_confirmations": 48 }, "wallet_readiness": "ready" }
]
}PUT스토어 확인 정책 설정/v1/projects/{project_id}/stores/{store_id}/payment-assets/{asset_id}/confirmation-policy읽기 + 쓰기
스토어별 확인 횟수 재정의 하나를 설정·해제하고 새 결제 수단 목록을 반환해요. 스토어에서 이미 선택한 자산이어야 해요. 프로젝트, 스토어, 체인, 지갑 중지 중에도 설정할 수 있어요.
- {"strategy":"inherit"}로 스토어 재정의를 없애고 향후 청구서가 현재 프로젝트 기본값을 따르게 하세요.
- 최종 확정 네트워크는 editable false를 반환하고 네트워크 최종 확정을 사용해요. 사용자 지정 블록 수 재정의는 허용하지 않아요.
- 0은 네트워크 확인과 체인 재구성 보호 없이 감지 시 수락한다는 뜻이에요. minimum_required_confirmations가 0일 때만 허용해요.
- 정책 변경은 새 청구서에만 적용돼요. 기존 청구서는 생성 시 프로젝트·스토어 확인 정책 스냅샷을 유지해요.
- 한 번에 자산 하나를 업데이트해요. 같은 스토어 자산의 동시 편집은 순서대로 처리하고 새 응답을 현재 상태로 사용하세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Content-Type | 필수 | application/json |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 인증 정보에 지정된 프로젝트. 일시 중지 상태일 수 있어요. |
| store_id | path UUID | project_id에 속한 스토어. 일시 중지 상태일 수 있어요. |
| asset_id | path UUID | 현재 선택된 업데이트 대상 스토어 결제 자산. |
스토어 확인 정책 본문
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| strategy | inherit | custom | 필수 | 태그가 있는 전략. inherit는 스토어 재정의를 제거하고 custom은 required_confirmations가 필요해요. |
| required_confirmations | integer | custom 전용 | 이 자산에 반환된 최솟값·최댓값 범위의 정수. 알 수 없거나 추가된 필드는 거부해요. |
PaymentAsset
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id | UUID | 항상 | 프로젝트·스토어 정책 경로에서 사용하는 영구 결제 자산 식별자. |
| asset_key | string | 항상 | 정규 CAIP 방식 기본 코인 또는 컨트랙트 자산 식별자. |
| chain_slug / network | string | 항상 | Wholly Crypto 체인 식별자 및 설정된 네트워크. |
| caip_network_id / caip_asset_id | string / string|null | 항상 | 정규 네트워크·자산 식별자. |
| asset_kind | native | token | 항상 | 정산에 체인 통화를 쓰는지 검증된 컨트랙트·민트를 쓰는지 여부. |
| payment_rail | string | 항상 | 런타임 경로: utxo, evm-native, solana-native, account-native, privacy-native 또는 token-transfer. |
| symbol / name / decimals | string / string / integer | 항상 | 표시 식별자와 정확한 최소 단위 정밀도. |
| contract_address | string | null | 항상 | 토큰의 정규 ERC-20 컨트랙트 또는 SPL 민트. 기본 자산은 null. |
| coingecko_id | string | null | 항상 | 탐색·가격 식별자. 사용자 지정 컨트랙트는 null이며 티커로 시장가를 추론하지 마세요. CoinGecko 메타데이터만으로 토큰이 선택 가능해지지는 않아요. |
| custom_token | boolean | 항상 | 온체인 검증된 사용자 지정 컨트랙트. 프로젝트 범위 고정 USD 또는 선택한 DEX 풀 가격을 사용해요. |
| icon_path | path | null | 항상 | 가능한 경우 로컬 캐시 토큰 아이콘. |
| token_standard | erc20 | spl-token | null | 항상 | 검증된 런타임 토큰 표준. 기본 자산은 null. |
| metadata_verified_at | timestamp | null | 항상 | 등록된 토큰의 온체인 메타데이터 검증 시간. |
| payment_supported / scanner_ready / balance_ready | boolean | 항상 | 빌드 시 레지스트리 조건. scanner_ready는 결제 스캐너 런타임이 설치됐다는 뜻이에요. 결제 확인에는 설정된 수의 정상·정확한 역할 제공업체가 필요해요(기본 2개, 선택적으로 1개). 6.0.6부터 일시적인 스캐너 불가 상태는 청구서 생성을 막지 않아요. balance_ready는 구현된 잔액 어댑터에만 true예요. |
| default_finality_mode | confirmations | finalized | 항상 | 새 프로젝트 정책이 이어받는 기본 최종 확정 모델. |
| default_required_confirmations / default_monitoring_minutes | integer | 항상 | 기본 확인 및 모니터링 정책. |
StorePaymentAsset
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| asset | PaymentAsset | 항상 | 프로젝트에 보이는 기본 코인 또는 검증 토큰 자산. |
| project_policy | ProjectAssetPolicy | null | 항상 | 상위 프로젝트 정책. |
| selected | boolean | 항상 | 수단이 스토어에 저장된 원하는 설정에 포함되는지 여부예요. 프로젝트 정책, 지갑, 설치된 어댑터, 가격이 유효하면 제공해요. 일시적 스캐너 장애로 새 청구서에서 제거하지 않아요. |
| display_order | integer | null | 항상 | 선택된 경우 스토어 결제 화면 순서. |
| confirmation_policy | StoreConfirmationPolicy | null | 항상 | 프로젝트에 설정된 자산의 유효 스토어 정책. 프로젝트 정책이 없으면 null. |
| wallet | WalletSummary | null | 항상 | 기본 코인·토큰이 공유하는 체인 지갑. |
| wallet_readiness | readiness enum | 항상 | 지갑·정책 상태만 제공해요. 스캐너 전제 조건은 receive_readiness를 사용하세요. |
| receive_readiness | ReceiveReadiness | null | 5.5.0+ | 공유 수신 설정과 스토어 수락 상태. 캐시 관찰값이며 예약이나 보장이 아니에요. 생성 시 요건과 실제 청구서 환율을 다시 확인해요. |
StoreConfirmationPolicy
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| finality_mode | confirmations | finalized | 항상 | 정산에 설정 가능한 블록 수를 쓰는지 네트워크 최종 확정을 쓰는지 여부. |
| project_required_confirmations | integer | 항상 | 스토어 재정의가 없을 때 향후 청구서가 사용할 현재 프로젝트 기본값. |
| override_required_confirmations | integer | null | 항상 | 스토어별 횟수. null이면 프로젝트 기본값을 이어받아요. |
| effective_required_confirmations | integer | 항상 | 이 스토어·자산의 새 청구서에 저장할 확인 횟수. |
| editable | boolean | 항상 | 최종 확정 정책을 바꿀 수 없는 finalized 네트워크는 false. |
| minimum_required_confirmations | integer | 항상 | 경계를 포함한 체인별 하한. 감지 시 수락을 지원하는 경로에만 0을 표시해요. |
| maximum_required_confirmations | integer | 항상 | 경계를 포함한 체인별 상한. |
ReceiveReadiness
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| ready | boolean | 항상 | 수신 설정 검사를 통과했어요. 지출 준비, 가스, 잔액 갱신, 미래 견적 보장을 뜻하지 않아요. |
| invoice_creatable | boolean | 6.0.6+ | 설정상 일시적인 스캐너 경고가 있어도 청구서 수단을 만들 수 있어요. 통화 가격은 생성 시 확인해요. 결제 검증은 아니므로 ready가 false여도 invoice_creatable은 true일 수 있어요. 지갑 없음, 비활성 정책, 미지원 어댑터는 여전히 안전하게 거부해요. |
| checked_at | timestamp | 항상 | 평가 시간. 목록 조회는 네트워크 요청이나 주소 할당을 하지 않아요. |
| issues | PaymentMethodIssue[] | 항상 | 준비되면 비어 있고 아니면 수신 경고나 설정 차단 이유가 있어요. invoice_creatable로 일시적 스캐너 경고와 청구서 설정 실패를 구분하세요. |
PaymentMethodIssue
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | 알려진 경우 | 영향받는 체인과 자산을 식별해요. Lightning은 asset_id를 생략할 수 있어요. |
| reason_code | string | 항상 | scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled 또는 asset_not_accepted. |
| message / action | string | 제공되는 경우 | 판매자용 설명 및 작업 식별자: chain_connections, wallets, rates, payment_methods, project_settings 또는 store_settings. 인증 정보나 비공개 제공업체 URL은 없어요. |
| required_endpoint_role | string | null | 온체인 | 선호 스캐너 API 역할(이전 필드). 전체 호환 목록은 accepted_endpoint_roles를 사용하세요. 기본 노드 상태로 결제 기록 지원을 증명할 수 없어요. |
| accepted_endpoint_roles | string[] | null | 온체인 | 호환 API 형식이며 엔드포인트 기록·용량을 보장하지 않아요. 원시 node-rpc는 BTC/BCH/LTC/DOGE/DASH와 투명 ZEC(완전히 디코딩한 블록, 확인 1~48회), 확정된 기본 TRX, algod의 기본 ALGO, Octez의 XTZ, SCALE 메타데이터의 최종 Asset Hub DOT, 청구서 메모 ID를 포함한 Stellar RPC의 기본 XLM을 지원해요. 잘리거나 불완전한 기록은 안 돼요. 이 원시 어댑터는 토큰 경로를 추가하지 않아요. 인덱스 API도 대안이며 아래 경로 표를 보세요. 혼합 원시·인덱스 소스는 제한된 구간을 독립 검증해요. 기본은 같은 운영자의 별칭이 아닌 독립 제공업체 두 곳이에요. 기본 노드 높이, ORDnet 체인 정보, 비 EVM 경로의 EVM 릴레이는 수신 증거가 아니에요. Monero에는 프로젝트에 연결된 조회 전용 wallet-RPC가 여전히 필요해요. |
| healthy_endpoints | integer | 온체인 | 일치하는 정상 엔드포인트 수이며 독립 제공업체 수가 아니에요. |
| usable_independent_providers / required_independent_providers | integer | 온체인 | 사용 가능한 검증 슬롯은 최대 2개예요. required_independent_providers는 체인 설정으로 기본 2, 관리자가 명시적으로 선택하면 1이에요. 두 제공업체 모드에서는 제공업체 키와 호스트가 모두 달라야 해요. 비활성, 오래된 상태(10분 초과), 대기 중 소스는 슬롯에 포함하지 않아요. Lightning은 자체 연결 규칙을 사용해요. |
| last_checked_at | timestamp | null | 온체인 | 가장 최근의 일치 엔드포인트 상태 검사. 평가 시간과 별개예요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request PUT \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets/YOUR_ASSET_ID/confirmation-policy" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Content-Type: application/json' \
--data-raw '{
"strategy": "custom",
"required_confirmations": 0
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const body = `{
"strategy": "custom",
"required_confirmations": 0
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets/YOUR_ASSET_ID/confirmation-policy", {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$body = <<<'JSON'
{
"strategy": "custom",
"required_confirmations": 0
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets/YOUR_ASSET_ID/confirmation-policy");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"strategy": "custom",
"required_confirmations": 0
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/payment-assets/YOUR_ASSET_ID/confirmation-policy",
method="PUT", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [
{
"asset": { "id": "YOUR_ASSET_ID", "chain_slug": "bitcoin", "symbol": "BTC" },
"selected": true,
"display_order": 0,
"confirmation_policy": {
"finality_mode": "confirmations",
"project_required_confirmations": 2,
"override_required_confirmations": 0,
"effective_required_confirmations": 0,
"editable": true,
"minimum_required_confirmations": 0,
"maximum_required_confirmations": 10000
},
"wallet_readiness": "ready"
}
]
}GET프로젝트 지갑 및 잔액 목록/v1/projects/{project_id}/wallets읽기 전용
공개 지갑 메타데이터와 지갑의 정확한 체인·네트워크에 등록된 모든 잔액 조회 가능 자산을 반환해요. 30개 기본 체인 경로와 검증된 ERC-20·SPL 자산을 추적해요. 첫 스캔 전이나 결제 미수락 자산도 바로 보여요. project_enabled는 결제 수락, tracking_active는 독립적인 읽기 전용 갱신 자격이에요. Monero는 프로젝트에 연결된 외부 조회 전용 wallet-RPC가 필요해요. 콘솔 잔액 스캔은 제한된 읽기를 우선 처리하며 자산별 진행·오류를 보여주고 완전한 주기만 최신 합계를 갱신해요. 청구서 정산은 캐시 잔액이 아닌 거래별 모니터링과 확인 정책에 따라요.
- 이 Bearer 경로는 복구 구문, 개인 키, 암호화된 비밀 값, 지출 메서드를 반환하지 않아요.
- 새로 등록한 같은 체인 자산은 첫 스캔 완료 전 null 잔액과 pending 상태를 반환해요. 가짜 0으로 표시하지 않아요.
- 프로젝트, 지갑 결제 수락, 기본 경로, 개별 자산을 꺼도 읽기 전용 잔액 추적은 멈추지 않아요. 기본 주소가 있는 활성·비활성 지갑은 등록된 같은 체인의 지원 자산을 계속 갱신해요. pending·error 지갑은 스캔하지 않아요.
- project_enabled는 프로젝트 자산 수락 정책만 보고하며 tracking_active가 true여도 false일 수 있어요.
- balance와 balance_atomic은 정확한 문자열이에요. price_usd, value_usd, total_value_usd는 참고용이며 null일 수 있어요. 최신 잔액 상태가 최신 시장가를 보장하지 않아요.
- 평가는 2시간 이내 CoinGecko 가격을 우선해요. 기본 코인과 검증된 정규 USDC/USDT는 활성 Kraken·Binance의 5분 이내 USD 견적을 기본 제공업체부터 사용할 수 있어요. 달러 페그를 가정하거나 티커만으로 사용자 지정 토큰 가격을 정하지 않아요. 프로젝트 고정·DEX 가격은 별개이고 청구서 견적은 바뀌지 않아요.
- Pending은 완전한 스냅샷이 없다는 뜻이에요. Refreshing은 마지막 완료 금액과 checked_at을 유지하며 블록체인 전송 대기를 뜻하지 않아요. Stale/error도 이전 금액을 유지할 수 있어요. 사용 불가 캐시를 0이나 누락 결제로 처리하지 마세요. 일반 EVM·Solana 갱신은 점검 사이에 최근 확인한 빈 주소를 최대 30분 재사용하고, 자금 있는 주소·새 주소·바뀐 주소는 다시 확인해요. 콘솔 잔액 스캔을 직접 실행하면 전체 스캔을 요청해요.
- recent_payments는 지갑당 관찰 3개로 제한하며 무효 기록은 제외해요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 인증 정보에 지정된 활성 프로젝트. |
WalletSummary
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id / project_id / native_asset_id | UUID | 항상 | 지갑, 소유 프로젝트, 체인 기본 자산 식별자. |
| chain_slug / network | string | 항상 | 지갑 체인과 네트워크. |
| asset_symbol / asset_name | string | 항상 | 체인 기본 코인 표시 식별자. |
| status | pending | active | disabled | error | 항상 | 지갑 운영 상태. |
| label | string | 항상 | 운영자 라벨. |
| public_key / primary_address | string | null | 항상 | 공개 지갑 식별자. 시드 구문이나 개인 키는 노출하지 않아요. |
| derivation_scheme / address_format | string | null | 항상 | 주소 정책과 형식. |
| backup_confirmed_at | timestamp | null | 항상 | 운영자가 복구 백업을 확인하면 null이 아니에요. |
| activation_required / activation_verified_at | boolean / timestamp|null | 항상 | XRP·Stellar 공유 계정은 운영자가 표시된 주소에 자금을 넣고 설정된 스캐너 제공업체가 정확한 계정을 검증해야 사용할 수 있어요. 영구 증명은 만료되지 않아요. 실시간 스캐너 상태는 청구서 생성이 아닌 결제 검증용으로 별도 확인해요. |
| receive_readiness | ReceiveReadiness | null | 5.5.0+ | 지갑 목록에는 프로젝트 수신 설정과 체인 스캐너 전제 조건이 포함돼요. 잔액, 토큰 가스, 전송 준비 상태와 별개예요. 다른 지갑 응답에서는 null일 수 있어요. |
| monero_wallet_rpc | MoneroWalletRpcBinding | null | 항상 | 비밀 정보를 제거한 Monero 외부 조회 전용 wallet-RPC 연결 상태. 엔드포인트, 인증 모드, account-0 기본 주소, 기술 증명 표시·높이, 운영자 확인 시간을 포함해요. 인증 정보, 지갑 키, 지갑 파일은 직렬화하지 않아요. |
| last_secret_revealed_at / secret_reveal_count | timestamp|null / integer | 항상 | 콘솔의 비밀 정보 공개 감사 메타데이터. |
| next_receive_index | integer | 항상 | 다음 예약 하위 주소 인덱스. |
| last_scanned_height / last_scanned_at / last_error | integer|null / timestamp|null / string|null | 항상 | 지갑 스캐너 상태. |
| balances | WalletAssetBalance[] | 항상 | 30개 기본 체인 경로와 검증된 ERC-20·SPL 자산의 캐시 잔액. Monero에는 설정된 외부 조회 전용 wallet-RPC가 필요해요. |
| total_value_usd | decimal string | null | 항상 | 현재 USD 가격이 있는 잔액의 참고 합계. |
| balance_status | pending | refreshing | fresh | stale | error | unknown | 항상 | 집계된 캐시 최신성. unknown은 방어적 대체값이며 어떤 상태도 청구서 정산을 증명하지 않아요. |
| balance_checked_at | timestamp | null | 항상 | 집계에 포함된 관련 성공 잔액 점검 중 가장 오래된 시간. |
| recent_payments | WalletRecentPayment[] | 항상 | 정확히 이 지갑에 속하는 최신 유효 detected, confirming, final 관찰 기록 최대 3개. |
| created_at / updated_at | RFC 3339 timestamp | 항상 | 지갑 생성 및 마지막 업데이트 시간. |
WalletAssetBalance
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| wallet_id / asset_id | UUID | 항상 | 지갑 및 영구 자산 식별자. |
| project_enabled | boolean | 항상 | 프로젝트 자산 정책이 현재 이 자산을 켰는지 여부. |
| active_store_count | integer | 항상 | 현재 이 자산을 선택한 활성 스토어 수. 수락 상태를 반영하며 읽기 전용 잔액 추적과는 별개예요. |
| active_store_ids | UUID[] | 항상 | 현재 자산을 받는 프로젝트의 활성 스토어. 추가 API 요청 없이 정확한 로컬 스토어 필터링이 가능해요. |
| tracking_active | boolean | 항상 | 읽기 가능한 지갑과 등록된 같은 체인 자산이 백그라운드 잔액 갱신 대상인지 여부예요. 프로젝트·결제 수단 수락 스위치는 읽기 전용 추적을 멈추지 않아요. |
| asset_kind | native | token | 항상 | 기본 통화 또는 검증된 컨트랙트·민트 자산. |
| contract_address | string | null | 항상 | 토큰 컨트랙트 또는 민트. 기본 통화는 null. |
| symbol / name / decimals | string / string / integer | 항상 | 표시 식별자와 최소 단위 정밀도. |
| coingecko_id | string | null | 항상 | 매핑된 경우 가격 식별자. |
| balance / balance_atomic | decimal string|null / integer string|null | 항상 | 지갑 기본 주소와 발급된 청구서 주소 전체의 정확한 표시·최소 단위 잔액. 완전한 값을 구할 수 없으면 null. |
| price_usd | decimal string | null | 항상 | 평가에 사용한 참고 캐시 USD 단가. |
| value_usd | decimal string | null | 항상 | 현재 환율이 있을 때의 참고 법정화폐 평가액. |
| status | pending | refreshing | fresh | stale | error | 항상 | 캐시 스캔 상태. refreshing은 완료 잔액을 유지할 수 있으니 checked_at으로 시간을 확인하세요. Pending은 완료 스냅샷이 없다는 뜻이에요. 어떤 상태도 전송 대기나 청구서 정산을 증명하지 않아요. |
| checked_at | timestamp | null | 항상 | 완료한 잔액 스캔이 나타내는 시간. |
| last_error | string | null | 항상 | 안전한 운영자 진단. |
WalletRecentPayment
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| invoice_public_id | UUID | 항상 | 관찰 기록에 연결된 고객용 청구서 식별자. |
| chain_slug / symbol | string | 항상 | 체인 및 기본 코인·검증 토큰 표시 기호. |
| transaction_id / event_index | string / integer | 항상 | 정규 거래·전송 이벤트 식별자. |
| amount | decimal string | 항상 | 부동소수점 변환 없는 정확한 관찰 자산 금액. |
| status | detected | confirming | final | 항상 | 현재 유효 관찰 상태. reorged, replaced, invalid 기록은 제외해요. |
| confirmations | integer | 항상 | 최근 관찰된 확인 횟수. |
| observed_at | RFC 3339 timestamp | 항상 | Wholly Crypto가 결제를 처음 관찰한 시간. |
ReceiveReadiness
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| ready | boolean | 항상 | 수신 설정 검사를 통과했어요. 지출 준비, 가스, 잔액 갱신, 미래 견적 보장을 뜻하지 않아요. |
| invoice_creatable | boolean | 6.0.6+ | 설정상 일시적인 스캐너 경고가 있어도 청구서 수단을 만들 수 있어요. 통화 가격은 생성 시 확인해요. 결제 검증은 아니므로 ready가 false여도 invoice_creatable은 true일 수 있어요. 지갑 없음, 비활성 정책, 미지원 어댑터는 여전히 안전하게 거부해요. |
| checked_at | timestamp | 항상 | 평가 시간. 목록 조회는 네트워크 요청이나 주소 할당을 하지 않아요. |
| issues | PaymentMethodIssue[] | 항상 | 준비되면 비어 있고 아니면 수신 경고나 설정 차단 이유가 있어요. invoice_creatable로 일시적 스캐너 경고와 청구서 설정 실패를 구분하세요. |
PaymentMethodIssue
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | 알려진 경우 | 영향받는 체인과 자산을 식별해요. Lightning은 asset_id를 생략할 수 있어요. |
| reason_code | string | 항상 | scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled 또는 asset_not_accepted. |
| message / action | string | 제공되는 경우 | 판매자용 설명 및 작업 식별자: chain_connections, wallets, rates, payment_methods, project_settings 또는 store_settings. 인증 정보나 비공개 제공업체 URL은 없어요. |
| required_endpoint_role | string | null | 온체인 | 선호 스캐너 API 역할(이전 필드). 전체 호환 목록은 accepted_endpoint_roles를 사용하세요. 기본 노드 상태로 결제 기록 지원을 증명할 수 없어요. |
| accepted_endpoint_roles | string[] | null | 온체인 | 호환 API 형식이며 엔드포인트 기록·용량을 보장하지 않아요. 원시 node-rpc는 BTC/BCH/LTC/DOGE/DASH와 투명 ZEC(완전히 디코딩한 블록, 확인 1~48회), 확정된 기본 TRX, algod의 기본 ALGO, Octez의 XTZ, SCALE 메타데이터의 최종 Asset Hub DOT, 청구서 메모 ID를 포함한 Stellar RPC의 기본 XLM을 지원해요. 잘리거나 불완전한 기록은 안 돼요. 이 원시 어댑터는 토큰 경로를 추가하지 않아요. 인덱스 API도 대안이며 아래 경로 표를 보세요. 혼합 원시·인덱스 소스는 제한된 구간을 독립 검증해요. 기본은 같은 운영자의 별칭이 아닌 독립 제공업체 두 곳이에요. 기본 노드 높이, ORDnet 체인 정보, 비 EVM 경로의 EVM 릴레이는 수신 증거가 아니에요. Monero에는 프로젝트에 연결된 조회 전용 wallet-RPC가 여전히 필요해요. |
| healthy_endpoints | integer | 온체인 | 일치하는 정상 엔드포인트 수이며 독립 제공업체 수가 아니에요. |
| usable_independent_providers / required_independent_providers | integer | 온체인 | 사용 가능한 검증 슬롯은 최대 2개예요. required_independent_providers는 체인 설정으로 기본 2, 관리자가 명시적으로 선택하면 1이에요. 두 제공업체 모드에서는 제공업체 키와 호스트가 모두 달라야 해요. 비활성, 오래된 상태(10분 초과), 대기 중 소스는 슬롯에 포함하지 않아요. Lightning은 자체 연결 규칙을 사용해요. |
| last_checked_at | timestamp | null | 온체인 | 가장 최근의 일치 엔드포인트 상태 검사. 평가 시간과 별개예요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/wallets" \
--header "Authorization: Bearer $WHOLLY_TOKEN"// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/wallets", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/wallets");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/wallets",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [
{
"id": "55555555-5555-4555-8555-555555555555",
"project_id": "11111111-1111-4111-8111-111111111111",
"native_asset_id": "10000000-0000-4000-8000-000000000003",
"chain_slug": "ethereum",
"network": "mainnet",
"asset_symbol": "ETH",
"asset_name": "Ethereum",
"status": "active",
"label": "Primary Ethereum wallet",
"public_key": "0x…",
"primary_address": "0x…",
"derivation_scheme": "bip44",
"address_format": "eip55",
"backup_confirmed_at": "2026-08-31T17:00:00Z",
"last_secret_revealed_at": null,
"secret_reveal_count": 0,
"next_receive_index": 43,
"last_scanned_height": 23123456,
"last_scanned_at": "2026-08-31T18:05:00Z",
"last_error": null,
"balances": [
{ "wallet_id": "55555555-5555-4555-8555-555555555555", "asset_id": "10000000-0000-4000-8000-000000000003", "project_enabled": true, "tracking_active": true, "asset_kind": "native", "contract_address": null, "symbol": "ETH", "name": "Ethereum", "decimals": 18, "coingecko_id": "ethereum", "balance": "0.125", "balance_atomic": "125000000000000000", "price_usd": "4500", "value_usd": "562.50", "status": "fresh", "checked_at": "2026-08-31T18:05:00Z", "last_error": null },
{ "wallet_id": "55555555-5555-4555-8555-555555555555", "asset_id": "10000000-0000-4000-8000-000000000099", "project_enabled": false, "tracking_active": true, "asset_kind": "token", "contract_address": "0xA0b86991c6218b36c1d19d4a2e9eb0cE3606eB48", "symbol": "USDC", "name": "USDC", "decimals": 6, "coingecko_id": "usd-coin", "balance": null, "balance_atomic": null, "price_usd": "1", "value_usd": null, "status": "pending", "checked_at": null, "last_error": null }
],
"total_value_usd": "562.50",
"balance_status": "fresh",
"balance_checked_at": "2026-08-31T18:05:00Z",
"recent_payments": [
{ "invoice_public_id": "0a6a98db-d93d-48ee-8c3c-fd45f90c4a50", "chain_slug": "ethereum", "symbol": "USDC", "transaction_id": "0x…", "event_index": 0, "amount": "25", "status": "final", "confirmations": 12, "observed_at": "2026-08-31T18:04:00Z" }
],
"created_at": "2026-08-31T16:00:00Z",
"updated_at": "2026-08-31T18:05:00Z"
}
]
}POST청구서 생성/v1/projects/{project_id}/stores/{store_id}/invoices읽기 + 쓰기
지갑 목적지, 최신 정확한 견적, 감사 기록, 알림 발신함 기록과 함께 청구서를 원자적으로 만들어요. 같은 인증 정보, Idempotency-Key, 동일한 원본 본문 바이트로 재시도하면 원래 청구서를 반환해요.
- payment_methods는 이 청구서에서만 스토어의 활성 수단을 필터링해요. 생략·null은 전체 수단을 유지하며 []는 무효예요. 프로젝트 → 스토어 → 결제 수단에서 chain_slug 힌트와 표시 티커를 찾으세요. API payment-assets 목록은 chain_slug, asset.symbol, asset.id를 제공해요. 받는 이더리움 토큰에는 {chain_slug: ethereum, asset_tickers: [USDC, USDT]}를 쓰며 BTC와 PEPE도 선택한 체인에서 같아요. 티커는 대소문자를 구분하지 않고 체인 범위이며 스토어 안에서만 해석해요. 받는 컨트랙트 두 개의 티커가 같으면 하나가 미준비여도 임의 선택하지 않고 400을 반환하니 asset_ids를 쓰세요. 기본 자산, 목록 토큰, 사용자 지정 토큰 모두 같은 규칙이에요. 체인·경로는 각각 한 번, 최종 수단은 최대 64개예요. 판매자 5.4.0+는 알 수 없는·비활성·다른 체인·미수락 선택지를 무시해요. 전체 선택에 활성 수락 항목이 없으면 스토어 기본값을 쓰고, 있으면 일치 항목만 써요. 체인만 지정하면 활성 수락 온체인 자산을 모두 포함해요. 활성 선택 수단에는 유효 지갑, 설치된 스캐너 어댑터, 신뢰할 수 있는 환율이 필요해요. 6.0.6부터 스캐너 불가, 대기 시간, 대기·오래된 상태 검사는 생성이나 설정된 온체인 수단을 막지 않아요. 감지는 자동 재시도하며 정산에는 제공업체 정족수와 확인이 여전히 필요해요. receive_readiness를 확인하고 제공업체를 유지하세요. 스캐너 복구 전까지 청구서가 미검증일 수 있어요. Monero 하위 주소 할당과 Lightning BOLT11 생성에는 외부 지갑·노드가 필요해요. 실패 시 error.message 및 error.details.payment_methods에 chain_slug, asset_ticker, reason_code가 있고 스캐너 진단에는 required_endpoint_role, healthy_endpoints, required_independent_providers도 있어요. TRON은 인덱스 기록이나 지원되는 원시 확정 기본 블록 API를 허용하며 기본 상태만으로 스캐너 호환을 증명하지 못해요. 가격 실패는 자산·통화를 알려줘요. 미수락 자산을 켜거나 스토어 정책을 바꾸지 않아요. 5.4.0 이전은 알 수 없는·비활성 명시적 선택에 실패해요. 스토어 설정이 바뀌어도 기존 청구서 수단은 늘지 않아요. Lightning은 따로 선택해야 해요. 재시도는 원래 수단을 유지하며 같은 Idempotency-Key로 선택을 바꾸면 409예요.
- checkout_appearance는 위의 모든 표시 설정을 지원해요. 생략 필드는 상속, 배열은 교체, 중첩 메시지 필드는 병합하고 빈 메시지 객체는 해당 범위를 지워요. 완성된 디자인·이미지는 스토어 변경 없이 청구서에 저장돼요. 공개 결제 JSON의 appearance로 결과를 확인하세요. 전체 요청은 32 KiB, 완성 설정은 20 KiB까지예요.
- 같은 Idempotency-Key로 checkout_appearance를 바꾸면 409예요. 동일한 원본 바이트로 재시도하세요. 모양은 금액, 환율, 수락 자산, 확인 조건, 실제 상태, 임베딩 권한을 바꾸지 않아요. HTML, CSS, 스크립트, 원격 이미지 조회는 없어요.
- exchange_rate_spread_percent는 이 청구서의 스토어 기본값을 바꿔요. 생략·null은 상속, "0"은 끄기예요. 기존 청구서 견적은 바뀌지 않아요.
- 스프레드 적용 후 올림해요. 수수료는 스프레드를 제외한 원래 법정화폐 청구액 기준이에요.
- 반환된 expected_amount나 expected_amount_atomic을 보내세요. 올림은 자산 정밀도, 금액의 0.1%, 법정화폐 최소 단위 1개로 제한돼요.
- 재시도는 같은 인증 정보, Idempotency-Key, 정확한 본문 바이트를 유지해야 해요. 같은 키로 스프레드를 바꾸면 409 idempotency_conflict예요.
- 새 견적, 콜백 DNS, 주소 준비 전에 정확한 재시도인지 확인해요. 인증 정보 범위와 프로젝트·스토어 권한은 매번 확인해요.
- 유효 ipn_url에는 스토어 IPN 서명 비밀 키가 필요해요. 알 수 없는 본문 필드는 거부해요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Idempotency-Key | 필수 | 공백 없는 표시 가능한 고유 ASCII 1~128자. |
| Content-Type | 권장 | application/json. 현재 원본 본문 처리기는 미디어 유형을 강제하지 않고 JSON을 파싱해요. |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 프로젝트 → 설정 → API ID에서 프로젝트 API ID를 복사하세요. 인증 정보에 지정돼야 하며 읽기 쉬운 프로젝트 식별자는 허용하지 않아요. |
| store_id | path UUID | 프로젝트 → 스토어 → 스토어 선택 → 기본 → API ID에서 스토어 API ID를 복사하세요. 기본 스토어도 필요하며 활성 상태이고 project_id에 속해야 해요. |
청구서 생성 본문
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| amount | string | 필수 | 부호·지수 없는 일반 십진수 문자열이며 정수 최대 48자리, 소수 최대 30자리예요. 기본적으로 양수여야 해요. 스토어 → 청구서에서 금액 0 청구서를 허용할 수 있으며 입금·주소 할당·처리 수수료 없이 정산돼요. |
| currency | string | null | 선택 사항 | 지원되는 3글자 법정화폐 코드를 대문자로 정규화해요. 생략·null은 스토어 청구서 통화를 상속해요. 생성에는 독립적으로 사용 가능한 청구 환산율도 필요해요. |
| payment_methods | InvoicePaymentSelection[] | null | 선택 사항 | 이 청구서의 스토어 활성 수단을 선택해요. 판매자 5.4.0+는 알 수 없는·비활성·미수락 선택을 무시하고 없으면 스토어 기본값을 써요. 생략·null도 기본값이며 []는 무효예요. 수단을 켜거나 설정을 바꾸지 않아요. 아래 선택 스키마를 보세요. |
| order_id | string | null | 선택 사항 | 판매자 주문 참조. 양끝 공백 제거 후 1~128자이며 제어 문자는 거부해요. |
| string | null | 선택 사항 | 판매자 전용 고객 이메일. 실용 ASCII 주소로 정규화하며 최대 254자예요. 생략·null은 이메일을 저장하지 않아요. | |
| description | string | null | 선택 사항 | 고객용 설명 1~500자. 줄바꿈과 탭을 허용해요. |
| expires_in_seconds | integer | null | 선택 사항 | 청구서 견적 수명 300~86,400초. 생략·null은 스토어 정책을 상속해요. |
| exchange_rate_spread_percent | decimal string | null | 선택 사항 | 견적 가산율 0~100, 소수 최대 2자리. 생략·null은 스토어 기본값이고 "0"은 이 청구서에서 꺼요. 올림 전 적용 후 고정돼요. 법정화폐 청구액이나 처리 수수료 기준은 바꾸지 않아요. |
| underpayment_tolerance_percent | decimal string | null | 선택 사항 | 미달 허용 비율 0~99.99, 소수 최대 2자리. 생략·null은 스토어 기본값이에요. |
| ipn_url | string | null | 선택 사항 | 공개 HTTPS 콜백, 최대 2,048바이트이며 인증 정보·프래그먼트가 없어야 해요. 스토어 기본값을 덮고 null·생략은 상속해요. |
| redirect_url | string | null | 선택 사항 | 정산 후 HTTPS 성공 URL. 최대 2,048바이트, 내장 인증 정보 불가. 생략·null은 스토어 기본값을 상속하며 지울 수 없어요. |
| cancel_url | string | null | 선택 사항 | 결제가 성공하지 않고 끝날 때의 HTTPS 반환 URL. 생략·null은 스토어 기본값을 상속하며 지울 수 없어요. |
| redirect_automatically | boolean | null | 선택 사항 | 생략·null은 스토어 정책을 상속해요. true에는 유효한 redirect_url이 필요해요. |
| language | string | null | 선택 사항 | en, de, de-DE 같은 영어·독일어 BCP 47 태그. 생략·null은 스토어 정책을 상속해요. |
| checkout_appearance | CheckoutAppearanceOverride | null | 선택 사항 | 이 청구서의 일부 표시 설정. 생략·null은 스토어 현재 디자인을 따라요. {}를 포함한 객체는 생성 시 완성 디자인·이미지를 고정해요. 아래 재정의 스키마를 보세요. 재무 설정, HTML, CSS, JavaScript, 원격 이미지 URL은 없어요. |
| metadata | object | null | 선택 사항 | 판매자 전용 JSON 객체. 생략·null은 {}가 되며 인코딩 후 최대 4,096바이트, 중첩 5단계예요. firstname, lastname, street, street2, zip, city, country, countryiso2, company, vatid를 검증·정규화해 고객 요약 필드에 반영해요. |
InvoicePaymentSelection · 스토어 체인·자산 선택
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| chain_slug | string | 필수 | 프로젝트 → 스토어 → 결제 수단에서 chain_slug를 복사하거나 GET /v1/projects/{project_id}/stores/{store_id}/payment-assets에서 읽으세요. 예: ethereum, base, bitcoin. 체인·경로 쌍은 한 번만 나올 수 있어요. |
| asset_ids | UUID[] | null | 선택 사항 | 온체인 asset.id UUID이며 컨트랙트 주소나 청구서 결제 수단 ID가 아니에요. 이 필드와 asset_tickers 중 하나만 써요. 둘 다 생략하면 이 체인의 활성 수락 자산 전체를 선택해요. []와 중복·nil ID는 무효예요. 5.4.0+는 해당 스토어·체인에서 비활성·미수락 ID를 무시하고 전체가 불일치하면 스토어 기본값을 써요. |
| asset_tickers | string[] | null | 선택 사항 | 판매자 5.3.0+. BTC, USDC, PEPE 같은 기호이며 chain_slug와 이 스토어 범위예요. 고유 티커 1~64개, 양끝 공백 제거·대소문자 무시, ASCII 문자·숫자·점·밑줄·하이픈 1~40자예요. asset_ids와 둘 중 하나만 쓰세요. 5.4.0+는 알 수 없는·비활성·미수락 티커를 무시해요. 수락 기호가 모호하면 실패하니 asset_ids를 쓰세요. 활성 선택 수단은 유효 지갑·가격이 필요하며 6.0.6부터 일시적 온체인 스캐너 장애는 생성을 막지 않아요. Lightning은 선택적으로 BTC만 받아요. |
| payment_rail | onchain | lightning | 선택 사항 | 기본값은 onchain이에요. Bitcoin Lightning은 asset_ids 없이 {chain_slug: bitcoin, payment_rail: lightning}를 쓰고 asset_tickers는 선택적으로 [BTC]를 쓸 수 있어요. Bitcoin 온체인에는 Lightning이 포함되지 않아요. 스토어 Lightning 연결이 이미 활성·준비 상태여야 해요. |
CheckoutAppearanceOverride · 모든 필드 선택 사항
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| inherit_default_store | boolean | 선택 사항 | true는 프로젝트 기본 스토어 디자인을 기반으로 하고, 아니면 대상 스토어의 유효 디자인을 써요. 그다음 변경을 적용해 독립 저장하며 최종 청구서 플래그는 false예요. |
| title | string | 선택 사항 | 결제 제목, 최대 120자. 비어 있으면 표준 제목을 써요. |
| intro / outro | string | 선택 사항 | 각각 최대 2,000자의 일반 텍스트. 시작 문구는 위, 마무리 문구는 모든 상태 아래에 표시돼요. 줄바꿈은 유지하고 안전한 텍스트 URL은 링크가 돼요. 빈 문자열은 지워요. 이전 customer_message는 intro의 별칭으로 허용하지만 둘 다 보내지는 마세요. |
| intro_font_size / outro_font_size | integer | 선택 사항 | 픽셀: 12, 14, 16, 18, 20, 24. 다르게 상속하지 않으면 기본 16. |
| theme | system | light | dim | dark | 선택 사항 | 고객 기기를 따르거나 고정 테마를 사용해요. |
| accent_color / background_color / card_color / button_color | string | 선택 사항 | #RRGGBB. 배경·카드·버튼은 비우면 자동 색상이에요. 텍스트 대비는 자동이에요. |
| logo_size / logo_alignment | string | 선택 사항 | small, medium, large. left 또는 center. |
| images | object | 선택 사항 | 키는 logo_light, logo_dark, favicon이에요. 생략하면 기본 이미지 유지, null은 제거해요. 객체 {store_id: UUID, kind?: logo_light|logo_dark|favicon}는 같은 프로젝트의 해당 스토어 유효 업로드 이미지를 재사용해요. kind 기본값은 대상 키예요. 먼저 스토어 → 결제 화면에서 업로드하고 기본 → API ID에서 스토어 API ID를 복사하세요. 이미지 없음·다른 프로젝트 ID는 400이며 외부 URL·이미지 데이터는 허용하지 않아요. |
| show_order_id / show_description / details_expanded | boolean | 선택 사항 | 제목 아래 주문 ID 상세와 일반 텍스트 설명을 표시해요. details_expanded는 처음부터 주문 ID 상세를 열어요. 표시만 바꾸며 데이터를 가리지는 않아요. |
| show_project_name / show_store_name | boolean | 선택 사항 | 판매자 5.6.0+: 고객 결제 상단에 각 이름을 표시하거나 숨겨요. 둘 다 기본 true예요. 스토어 → 결제 화면에도 있고 다른 모양 설정처럼 상속·청구서 스냅샷에 저장해요. 표시만 바꾸며 데이터 삭제가 아니에요. |
| featured_chains | string[] | 선택 사항 | 순서 있는 체인 슬러그, 고유 값 최대 60개(소문자·숫자·하이픈, 최대 64자). []는 지워요. 사용 가능한 청구서 수단만 재정렬해요. |
| featured_asset_ids / default_asset_id | UUID[] / UUID|null | 선택 사항 | 고유한 순서 자산 ID 최대 100개. []는 지우고 기본 자산은 null일 수 있어요. ID는 결제 인텐트가 아닌 payment-assets에서 가져와요. 수단을 켜지 않으며 수신 결제와 유효 고객 선택이 우선이에요. |
| messages | object | 선택 사항 | waiting, confirming, paid, underpaid, expired 일반 문자열(각 500자)을 담은 en/de 객체. 제공한 언어·상태만 바꾸고 {}는 전체, {en:{}}는 영어, 빈 상태 문자열은 해당 상태를 지워요. 영어가 대체 언어예요. 실제 상태는 바꾸지 않아요. |
| support_email | string | 선택 사항 | ASCII 이메일, 최대 254자. 비우면 지워요. |
| support_url / terms_url / privacy_url | string | 선택 사항 | 인증 정보 없는 HTTPS URL, 최대 2,048자. 비우면 지우고 링크는 새 창에서 열려요. |
| return_button_text | string | 선택 사항 | 라벨 최대 60자. 청구서 동작은 최상위 redirect_url/cancel_url/redirect_automatically/language를 사용하세요. |
청구서 요약
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id | UUID | 항상 | 내부 청구서 UUID. 판매자 상세나 결제 경로에 사용하지 마세요. |
| invoice_id | UUID | 항상 | 판매자 상세 및 결제 경로에 쓰는 공개 청구서 UUID. |
| project_id | UUID | 항상 | 소속 프로젝트. |
| store_id | UUID | 항상 | 소속 스토어. |
| source | manual | api | 항상 | 청구서 생성 방식. |
| order_id | string | null | 항상 | 판매자 주문 참조. |
| string | null | 항상 | 판매자 전용 고객 이메일. 공개 결제 화면에서 반환하지 않아요. | |
| customer_name | string | null | 항상 | 비공개 firstname, lastname, company 메타데이터에서 만든 표시 이름. |
| customer_address | string | null | 항상 | 비공개 company, street, street2, zip, city, country, countryiso2, vatid 메타데이터에서 만든 한 줄 판매자 주소. |
| description | string | null | 항상 | 고객용 설명. |
| amount | decimal string | 항상 | 정규 청구서 금액. |
| currency | string | 항상 | 정규화된 청구서 통화·자산 코드. |
| exchange_rate_spread_percent | decimal string | 항상 | 고정 견적 스프레드. 생성 시 지정값 또는 생략 시 스토어 기본값이에요. 올림 전에 적용하고 이 청구서에서는 바뀌지 않아요. |
| underpayment_tolerance_percent | decimal string | 항상 | 청구서 생성 시 저장한 변경 불가능한 허용 미달 비율. |
| status | invoice status | 항상 | new, processing, settled, expired, invalid 또는 cancelled. |
| amount_status | amount status | 항상 | none, partial, paid 또는 overpaid. 명시적으로 허용한 금액 0 청구서는 결제 수단 없이 none으로 정산돼요. |
| timing_status | timing status | 항상 | on_time 또는 late. |
| resolution | resolution | 항상 | automatic, manually_settled 또는 manually_invalidated. |
| sequence | integer | 항상 | 1부터 시작하는 단조 증가 청구서 상태 순번. |
| winning_payment_intent_id | UUID | null | 항상 | 선택된 경우 청구서를 완료시킨 결제 수단. |
| expires_at | RFC 3339 timestamp | 항상 | 견적·결제 기한. |
| monitoring_expires_at | RFC 3339 timestamp | 항상 | 결제 수단에 설정된 가장 늦은 지연 모니터링 종료 시간. |
| settled_at | timestamp | null | 항상 | 정산된 경우 정산 시간. |
| cancelled_at | timestamp | null | 항상 | 취소된 경우 취소 시간. |
| archived_at | timestamp | null | 항상 | 보관 처리된 경우 보관 시간. |
| created_at | RFC 3339 timestamp | 항상 | 생성 시간. |
| updated_at | RFC 3339 timestamp | 항상 | 마지막 상태 업데이트 시간. |
청구서 상세 추가 필드
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| ipn_url | string | null | 항상 | 청구서별 유효 IPN 대상. 판매자 응답 전용이며 공개 결제 화면에서는 생략해요. |
| redirect_url | string | null | 항상 | 정산 후 사용하는 유효 성공 URL. |
| cancel_url | string | null | 항상 | 결제가 성공하지 않고 끝날 때 사용하는 유효 반환 URL. |
| redirect_automatically | boolean | 항상 | 성공 후 결제 화면의 자동 이동 여부. |
| checkout_language | string | 항상 | 유효 결제 화면 언어 태그. |
| metadata | object | 항상 | 판매자 메타데이터. 공개 결제 화면에는 반환하지 않아요. |
| payment_intents | PaymentIntent[] | 항상 | 견적된 결제 수단과 모니터링 상태. |
PaymentIntent
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id | UUID | 항상 | 결제 인텐트 식별자이며 결제 QR의 intent_id로도 써요. |
| payment_rail | onchain | lightning | 항상 | 청구서 전송 방식. Bitcoin 온체인과 Lightning은 asset_id를 공유할 수 있으니 기호만 보지 말고 인텐트 id와 이 필드를 함께 쓰세요. 자산 목록 스캐너의 payment_rail과는 달라요. |
| bolt11 | string | null | 항상 | Lightning 결제 요청이며 아니면 null이에요. Lightning 지갑으로 결제하고 결제 해시에 온체인 자금을 보내지 마세요. |
| asset_id | UUID | 항상 | 설정된 결제 자산 식별자. |
| asset_key | string | 항상 | 정규 CAIP 방식 자산 키. |
| chain_slug | string | 항상 | Wholly Crypto 체인 식별자. |
| network | string | 항상 | 설정된 네트워크. 지원 결제 자산은 현재 mainnet. |
| caip_network_id | string | 항상 | 정규 CAIP-2 네트워크 식별자. |
| caip_asset_id | string | null | 항상 | 등록된 경우 정규 CAIP-19 식별자. |
| symbol | string | 항상 | 자산 기호. |
| asset_decimals | integer | 항상 | 최소 단위 정밀도. Lightning BTC는 온체인 Bitcoin의 8이 아닌 11(밀리사토시)이에요. 견적은 사토시 정수이며 수신은 밀리사토시 정밀도를 유지해요. |
| status | intent status | 항상 | pending, partial, paid, overpaid, expired 또는 invalid. |
| finality_mode | confirmations | finalized | 항상 | 최종 확정 정책. |
| required_confirmations | integer | 항상 | 해당하는 경우 필요한 확인 횟수. |
| quote_rate | decimal string | 항상 | 고정 스프레드를 포함한 청구서 통화 1단위당 자산 단위 수. 예: USD당 1.02 USDC. 역환율이 아니에요. |
| quote_details | object | null | 항상 | 고정 견적 출처: 스프레드 전 reference_rate, unrounded_payment_amount, rounding_adjustment, pricing_provider, asset_provider, pricing_fetched_at, asset_fetched_at. 이전 청구서는 null이며 과거 값을 만들지 않아요. |
| expected_amount | decimal string | 항상 | 스프레드·올림 후 정확히 고정된 지불 자산 금액. 4.1.1부터 인식된 검증 법정화폐 스테이블코인(USDC, USDT, DAI, USDS, EURC 등)은 소수 최대 2자리로 올림해요. 1.321은 1.32가 아닌 1.33이에요. 허용 오차가 0이어도 이 금액이 예상 금액이에요. 다른 자산은 적응형 정밀도를 유지하고 기존 청구서는 재평가하지 않아요. |
| expected_amount_atomic | integer string | 항상 | 자산 최소 단위의 정확한 금액. |
| minimum_payment_amount | decimal string | 항상 | 청구서 허용 오차 적용 후 결제됨으로 수락하는 최소 금액. |
| minimum_payment_amount_atomic | integer string | 항상 | 자산 최소 단위의 정확한 수락 기준. |
| received_amount | decimal string | 항상 | 관찰된 금액. |
| received_amount_atomic | integer string | 항상 | 관찰된 최소 단위 금액. |
| confirmed_amount | decimal string | 항상 | 확인·최종 확정 금액. |
| confirmed_amount_atomic | integer string | 항상 | 확인·최종 확정 최소 단위 금액. |
| destination_address | string | 항상 | 온체인 수신 주소 또는 Lightning의 64자 결제 해시. Lightning 결제는 bolt11을 쓰세요. 해시는 Bitcoin 주소가 아니에요. |
| destination_tag | string | null | 항상 | 경로에 필요한 공개 결제 참조: XRP destination tag, Stellar memo ID, TON 청구서 코멘트. 고유 주소 경로는 null. |
| derivation_index | integer | 항상 | 예약된 지갑 하위 인덱스. 판매자 상세 전용. |
| quote_expires_at | RFC 3339 timestamp | 항상 | 견적 만료. |
| monitoring_expires_at | RFC 3339 timestamp | 항상 | 이 수단의 지연 모니터링 종료 시간. |
| next_check_at | timestamp | null | 항상 | 다음 예정 체인 점검. |
| last_checked_at | timestamp | null | 항상 | 마지막 체인 점검. |
| last_chain_height | integer | null | 항상 | 모니터가 관찰한 마지막 신뢰할 수 있는 높이. |
| last_anchor_hash | string | null | 항상 | 마지막 모니터 기준점·블록 해시. |
| last_monitor_error | string | null | 항상 | 운영자용 안전한 모니터링 진단. |
| first_payment_at | timestamp | null | 항상 | 최초 결제 관찰 시간. |
| fully_paid_at | timestamp | null | 항상 | 수락 최소 금액에 처음 도달한 시간. |
| finalized_at | timestamp | null | 항상 | 결제가 최종 확정 정책을 충족한 시간. |
PaymentMethodIssue
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| chain_slug / asset_id / asset_ticker | string / UUID / string | 알려진 경우 | 영향받는 체인과 자산을 식별해요. Lightning은 asset_id를 생략할 수 있어요. |
| reason_code | string | 항상 | scanner_provider_quorum, scanner_not_checked, scanner_unavailable, wallet_missing, wallet_disabled, wallet_backup_required, wallet_key_unavailable, wallet_activation_required, monero_binding_unavailable, rate_unavailable, custom_rate_unavailable, lightning_unavailable, project_disabled, store_disabled, chain_disabled, asset_disabled 또는 asset_not_accepted. |
| message / action | string | 제공되는 경우 | 판매자용 설명 및 작업 식별자: chain_connections, wallets, rates, payment_methods, project_settings 또는 store_settings. 인증 정보나 비공개 제공업체 URL은 없어요. |
| required_endpoint_role | string | null | 온체인 | 선호 스캐너 API 역할(이전 필드). 전체 호환 목록은 accepted_endpoint_roles를 사용하세요. 기본 노드 상태로 결제 기록 지원을 증명할 수 없어요. |
| accepted_endpoint_roles | string[] | null | 온체인 | 호환 API 형식이며 엔드포인트 기록·용량을 보장하지 않아요. 원시 node-rpc는 BTC/BCH/LTC/DOGE/DASH와 투명 ZEC(완전히 디코딩한 블록, 확인 1~48회), 확정된 기본 TRX, algod의 기본 ALGO, Octez의 XTZ, SCALE 메타데이터의 최종 Asset Hub DOT, 청구서 메모 ID를 포함한 Stellar RPC의 기본 XLM을 지원해요. 잘리거나 불완전한 기록은 안 돼요. 이 원시 어댑터는 토큰 경로를 추가하지 않아요. 인덱스 API도 대안이며 아래 경로 표를 보세요. 혼합 원시·인덱스 소스는 제한된 구간을 독립 검증해요. 기본은 같은 운영자의 별칭이 아닌 독립 제공업체 두 곳이에요. 기본 노드 높이, ORDnet 체인 정보, 비 EVM 경로의 EVM 릴레이는 수신 증거가 아니에요. Monero에는 프로젝트에 연결된 조회 전용 wallet-RPC가 여전히 필요해요. |
| healthy_endpoints | integer | 온체인 | 일치하는 정상 엔드포인트 수이며 독립 제공업체 수가 아니에요. |
| usable_independent_providers / required_independent_providers | integer | 온체인 | 사용 가능한 검증 슬롯은 최대 2개예요. required_independent_providers는 체인 설정으로 기본 2, 관리자가 명시적으로 선택하면 1이에요. 두 제공업체 모드에서는 제공업체 키와 호스트가 모두 달라야 해요. 비활성, 오래된 상태(10분 초과), 대기 중 소스는 슬롯에 포함하지 않아요. Lightning은 자체 연결 규칙을 사용해요. |
| last_checked_at | timestamp | null | 온체인 | 가장 최근의 일치 엔드포인트 상태 검사. 평가 시간과 별개예요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
# Keep this key and the exact body for retries; use a new key for each new invoice.
curl --fail-with-body --max-time 30 \
--request POST \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Idempotency-Key: order-1042-attempt-1' \
--header 'Content-Type: application/json' \
--data-raw '{
"payment_methods": [
{
"chain_slug": "bitcoin",
"asset_tickers": [
"BTC"
]
}
],
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"email": "ada@example.com",
"description": "Annual plan",
"underpayment_tolerance_percent": "1",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"language": "en",
"metadata": {
"cart_id": "cart-681",
"firstname": "Ada",
"lastname": "Lovelace",
"street": "12 Example Street",
"street2": "Suite 2",
"zip": "10115",
"city": "Berlin",
"country": "Germany",
"countryiso2": "DE",
"company": "Example GmbH",
"vatid": "DE123456789"
},
"exchange_rate_spread_percent": "0.5",
"checkout_appearance": {
"title": "Complete your order",
"intro": "Thanks for choosing our annual plan.",
"outro": "Questions? https://merchant.example/help",
"intro_font_size": 18,
"outro_font_size": 14,
"theme": "light",
"accent_color": "#1768CE",
"messages": {
"en": {
"paid": "Your order is ready."
}
}
}
}'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
// Keep this key and the exact body for retries; use a new key for each new invoice.
const body = `{
"payment_methods": [
{
"chain_slug": "bitcoin",
"asset_tickers": [
"BTC"
]
}
],
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"email": "ada@example.com",
"description": "Annual plan",
"underpayment_tolerance_percent": "1",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"language": "en",
"metadata": {
"cart_id": "cart-681",
"firstname": "Ada",
"lastname": "Lovelace",
"street": "12 Example Street",
"street2": "Suite 2",
"zip": "10115",
"city": "Berlin",
"country": "Germany",
"countryiso2": "DE",
"company": "Example GmbH",
"vatid": "DE123456789"
},
"exchange_rate_spread_percent": "0.5",
"checkout_appearance": {
"title": "Complete your order",
"intro": "Thanks for choosing our annual plan.",
"outro": "Questions? https://merchant.example/help",
"intro_font_size": 18,
"outro_font_size": 14,
"theme": "light",
"accent_color": "#1768CE",
"messages": {
"en": {
"paid": "Your order is ready."
}
}
}
}`;
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Idempotency-Key": "order-1042-attempt-1",
"Content-Type": "application/json"
},
body,
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
// Keep this key and the exact body for retries; use a new key for each new invoice.
$body = <<<'JSON'
{
"payment_methods": [
{
"chain_slug": "bitcoin",
"asset_tickers": [
"BTC"
]
}
],
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"email": "ada@example.com",
"description": "Annual plan",
"underpayment_tolerance_percent": "1",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"language": "en",
"metadata": {
"cart_id": "cart-681",
"firstname": "Ada",
"lastname": "Lovelace",
"street": "12 Example Street",
"street2": "Suite 2",
"zip": "10115",
"city": "Berlin",
"country": "Germany",
"countryiso2": "DE",
"company": "Example GmbH",
"vatid": "DE123456789"
},
"exchange_rate_spread_percent": "0.5",
"checkout_appearance": {
"title": "Complete your order",
"intro": "Thanks for choosing our annual plan.",
"outro": "Questions? https://merchant.example/help",
"intro_font_size": 18,
"outro_font_size": 14,
"theme": "light",
"accent_color": "#1768CE",
"messages": {
"en": {
"paid": "Your order is ready."
}
}
}
}
JSON;
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Idempotency-Key: order-1042-attempt-1", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
# Keep this key and the exact body for retries; use a new key for each new invoice.
headers = {
"Idempotency-Key": "order-1042-attempt-1",
"Content-Type": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
body = """{
"payment_methods": [
{
"chain_slug": "bitcoin",
"asset_tickers": [
"BTC"
]
}
],
"amount": "49.90",
"currency": "USD",
"order_id": "order-1042",
"email": "ada@example.com",
"description": "Annual plan",
"underpayment_tolerance_percent": "1",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"language": "en",
"metadata": {
"cart_id": "cart-681",
"firstname": "Ada",
"lastname": "Lovelace",
"street": "12 Example Street",
"street2": "Suite 2",
"zip": "10115",
"city": "Berlin",
"country": "Germany",
"countryiso2": "DE",
"company": "Example GmbH",
"vatid": "DE123456789"
},
"exchange_rate_spread_percent": "0.5",
"checkout_appearance": {
"title": "Complete your order",
"intro": "Thanks for choosing our annual plan.",
"outro": "Questions? https://merchant.example/help",
"intro_font_size": 18,
"outro_font_size": 14,
"theme": "light",
"accent_color": "#1768CE",
"messages": {
"en": {
"paid": "Your order is ready."
}
}
}
}""".encode("utf-8")
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/invoices",
method="POST", headers=headers, data=body)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 201 새 청구서, 200 정확한 멱등 재시도
{
"data": {
"id": "2f798f9f-01f2-42f0-9d10-5581d6116b4c",
"invoice_id": "0a6a98db-d93d-48ee-8c3c-fd45f90c4a50",
"project_id": "11111111-1111-4111-8111-111111111111",
"store_id": "22222222-2222-4222-8222-222222222222",
"source": "api",
"order_id": "order-1042",
"email": "ada@example.com",
"customer_name": "Ada Lovelace · Example GmbH",
"customer_address": "Example GmbH · 12 Example Street · Suite 2 · 10115 Berlin · Germany (DE) · VAT DE123456789",
"description": "Annual plan",
"amount": "49.9",
"currency": "USD",
"exchange_rate_spread_percent": "0.5",
"underpayment_tolerance_percent": "1",
"status": "new",
"amount_status": "none",
"timing_status": "on_time",
"resolution": "automatic",
"sequence": 1,
"winning_payment_intent_id": null,
"expires_at": "2026-08-31T18:15:00Z",
"monitoring_expires_at": "2026-09-07T18:15:00Z",
"settled_at": null,
"cancelled_at": null,
"archived_at": null,
"created_at": "2026-08-31T18:00:00Z",
"updated_at": "2026-08-31T18:00:00Z",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"checkout_language": "en",
"metadata": { "cart_id": "cart-681", "firstname": "Ada", "lastname": "Lovelace", "street": "12 Example Street", "street2": "Suite 2", "zip": "10115", "city": "Berlin", "country": "Germany", "countryiso2": "DE", "company": "Example GmbH", "vatid": "DE123456789" },
"payment_intents": [
{
"id": "33333333-3333-4333-8333-333333333333",
"asset_id": "10000000-0000-4000-8000-000000000001",
"asset_key": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"chain_slug": "bitcoin",
"network": "mainnet",
"caip_network_id": "bip122:000000000019d6689c085ae165831e93",
"caip_asset_id": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"symbol": "BTC",
"asset_decimals": 8,
"status": "pending",
"finality_mode": "confirmations",
"required_confirmations": 1,
"quote_rate": "0.000009218",
"quote_details": null,
"expected_amount": "0.00046",
"expected_amount_atomic": "46000",
"minimum_payment_amount": "0.0004554",
"minimum_payment_amount_atomic": "45540",
"received_amount": "0",
"received_amount_atomic": "0",
"confirmed_amount": "0",
"confirmed_amount_atomic": "0",
"destination_address": "bc1q…example",
"destination_tag": null,
"derivation_index": 42,
"quote_expires_at": "2026-08-31T18:15:00Z",
"monitoring_expires_at": "2026-09-07T18:15:00Z",
"next_check_at": "2026-08-31T18:00:00Z",
"last_checked_at": null,
"last_chain_height": null,
"last_anchor_hash": null,
"last_monitor_error": null,
"first_payment_at": null,
"fully_paid_at": null,
"finalized_at": null
}
]
},
"links": {
"checkout": "https://pay.example.com/invoice/0a6a98db-d93d-48ee-8c3c-fd45f90c4a50"
}
}GET청구서 목록/v1/projects/{project_id}/invoices읽기 전용
범위 내 청구서 요약을 간결한 최신순 페이지로 반환해요. 판매자 전용 이메일과 인식된 메타데이터에서 만든 고객 필드를 포함해요. 검색·상태·스토어 필터는 서버에서 처리하며 예측 가능한 페이지 구분을 위해 total과 has_more를 반환해요.
- created_at 내림차순, 다음으로 내부 id 내림차순.
- 목록 항목은 InvoiceSummary 객체이며 email, customer_name, customer_address는 판매자 전용이에요. 원본 메타데이터·결제 인텐트는 상세를 호출하세요.
- has_more가 true일 때만 다음 페이지 offset을 pagination.offset + pagination.limit으로 설정하세요.
- 개수와 페이지는 하나의 반복 읽기 데이터베이스 스냅샷에서 읽어요. 동시 쓰기는 이후 요청에 나타나요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 인증 정보에 지정된 활성 프로젝트. |
| store_id | query UUID | 선택적인 정확한 스토어 필터. |
| status | query enum | 선택 사항: new, processing, settled, expired, invalid 또는 cancelled. |
| search | query string | 선택 사항: 대소문자를 구분하지 않는 청구서 ID, 주문 ID 또는 이메일 접두사, 정확한 청구서 UUID, 설명과 인식된 고객 필드의 부분 문자열. 모든 메타데이터 키와 텍스트·숫자·불리언 값(중첩 객체/배열 포함)도 인덱스 기반 단어 접두사 검색을 지원해요. 모든 검색어가 일치해야 하고 문장부호는 구분자로 처리돼요. 앞뒤 공백을 제거한 뒤 최대 100자이며 제어 문자는 허용되지 않아요. 메타데이터가 검색되어도 목록 응답에 원본 메타데이터를 추가하지 않아요. 원본은 청구서 상세에서 읽으세요. |
| limit | query integer | 선택 사항, 1~100. 기본값 50. |
| offset | query integer | 선택 사항, 0–1,000,000. 기본값은 0이에요. |
청구서 요약
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id | UUID | 항상 | 내부 청구서 UUID. 판매자 상세나 결제 경로에 사용하지 마세요. |
| invoice_id | UUID | 항상 | 판매자 상세 및 결제 경로에 쓰는 공개 청구서 UUID. |
| project_id | UUID | 항상 | 소속 프로젝트. |
| store_id | UUID | 항상 | 소속 스토어. |
| source | manual | api | 항상 | 청구서 생성 방식. |
| order_id | string | null | 항상 | 판매자 주문 참조. |
| string | null | 항상 | 판매자 전용 고객 이메일. 공개 결제 화면에서 반환하지 않아요. | |
| customer_name | string | null | 항상 | 비공개 firstname, lastname, company 메타데이터에서 만든 표시 이름. |
| customer_address | string | null | 항상 | 비공개 company, street, street2, zip, city, country, countryiso2, vatid 메타데이터에서 만든 한 줄 판매자 주소. |
| description | string | null | 항상 | 고객용 설명. |
| amount | decimal string | 항상 | 정규 청구서 금액. |
| currency | string | 항상 | 정규화된 청구서 통화·자산 코드. |
| exchange_rate_spread_percent | decimal string | 항상 | 고정 견적 스프레드. 생성 시 지정값 또는 생략 시 스토어 기본값이에요. 올림 전에 적용하고 이 청구서에서는 바뀌지 않아요. |
| underpayment_tolerance_percent | decimal string | 항상 | 청구서 생성 시 저장한 변경 불가능한 허용 미달 비율. |
| status | invoice status | 항상 | new, processing, settled, expired, invalid 또는 cancelled. |
| amount_status | amount status | 항상 | none, partial, paid 또는 overpaid. 명시적으로 허용한 금액 0 청구서는 결제 수단 없이 none으로 정산돼요. |
| timing_status | timing status | 항상 | on_time 또는 late. |
| resolution | resolution | 항상 | automatic, manually_settled 또는 manually_invalidated. |
| sequence | integer | 항상 | 1부터 시작하는 단조 증가 청구서 상태 순번. |
| winning_payment_intent_id | UUID | null | 항상 | 선택된 경우 청구서를 완료시킨 결제 수단. |
| expires_at | RFC 3339 timestamp | 항상 | 견적·결제 기한. |
| monitoring_expires_at | RFC 3339 timestamp | 항상 | 결제 수단에 설정된 가장 늦은 지연 모니터링 종료 시간. |
| settled_at | timestamp | null | 항상 | 정산된 경우 정산 시간. |
| cancelled_at | timestamp | null | 항상 | 취소된 경우 취소 시간. |
| archived_at | timestamp | null | 항상 | 보관 처리된 경우 보관 시간. |
| created_at | RFC 3339 timestamp | 항상 | 생성 시간. |
| updated_at | RFC 3339 timestamp | 항상 | 마지막 상태 업데이트 시간. |
청구서 페이지 나누기
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| limit | integer | 항상 | 실제 페이지 크기, 1–100. |
| offset | integer | 항상 | 실제 행 오프셋, 0부터 시작하며 범위는 0–1,000,000이에요. |
| total | integer | 항상 | 페이지 스냅샷에서 프로젝트, 스토어, 상태, 검색 필터에 맞는 총 행 수. |
| has_more | boolean | 항상 | 오프셋과 반환된 행 수의 합이 총수보다 작으면 true예요. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices?search=order-1042&status=processing&limit=50&offset=0" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices?search=order-1042&status=processing&limit=50&offset=0", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices?search=order-1042&status=processing&limit=50&offset=0");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices?search=order-1042&status=processing&limit=50&offset=0",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": [
{
"id": "2f798f9f-01f2-42f0-9d10-5581d6116b4c",
"invoice_id": "0a6a98db-d93d-48ee-8c3c-fd45f90c4a50",
"project_id": "11111111-1111-4111-8111-111111111111",
"store_id": "22222222-2222-4222-8222-222222222222",
"source": "api",
"order_id": "order-1042",
"email": "ada@example.com",
"customer_name": "Ada Lovelace · Example GmbH",
"customer_address": "Example GmbH · 12 Example Street · Suite 2 · 10115 Berlin · Germany (DE) · VAT DE123456789",
"description": "Annual plan",
"amount": "49.9",
"currency": "USD",
"exchange_rate_spread_percent": "0.5",
"underpayment_tolerance_percent": "1",
"status": "processing",
"amount_status": "paid",
"timing_status": "on_time",
"resolution": "automatic",
"sequence": 3,
"winning_payment_intent_id": "33333333-3333-4333-8333-333333333333",
"expires_at": "2026-08-31T18:15:00Z",
"monitoring_expires_at": "2026-09-07T18:15:00Z",
"settled_at": null,
"cancelled_at": null,
"archived_at": null,
"created_at": "2026-08-31T18:00:00Z",
"updated_at": "2026-08-31T18:04:10Z"
}
],
"pagination": {
"limit": 50,
"offset": 0,
"total": 143,
"has_more": true
}
}GET청구서 조회/v1/projects/{project_id}/invoices/{invoice_id}읽기 전용
전체 판매자 청구서 상세와 현재 활성 결제 URL을 반환해요. 폴링과 대사에는 이 경로를 사용하세요.
- 범위가 제한된 조회에서는 공개 ID가 승인된 프로젝트에 속하지 않으면 의도적으로 invoice_not_found를 반환해요.
- links.checkout은 스토어 → 기본 → 스토어 도메인 설정을 사용해요. 이 스토어의 활성 pay 호스트명, 기본 스토어의 선택, 시스템 기본값 순으로 적용해요. 폐기·초안 상태이거나 서비스가 다른 호스트는 무시해요. 생성 응답과 MCP 응답에도 적용되며, 멱등 재실행을 포함해 응답 시점에 링크를 결정해요. 서명된 콜백 링크는 이벤트 생성 시 고정되고 재시도해도 바뀌지 않아요. 이 설정은 링크만 생성하며 트래픽을 리디렉션하거나 IP 제한을 바꾸지 않아요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 인증 정보에 지정된 활성 프로젝트. |
| invoice_id | path UUID | 생성/목록 조회에서 반환된 invoice_id이며 내부 id가 아니에요. |
청구서 요약
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id | UUID | 항상 | 내부 청구서 UUID. 판매자 상세나 결제 경로에 사용하지 마세요. |
| invoice_id | UUID | 항상 | 판매자 상세 및 결제 경로에 쓰는 공개 청구서 UUID. |
| project_id | UUID | 항상 | 소속 프로젝트. |
| store_id | UUID | 항상 | 소속 스토어. |
| source | manual | api | 항상 | 청구서 생성 방식. |
| order_id | string | null | 항상 | 판매자 주문 참조. |
| string | null | 항상 | 판매자 전용 고객 이메일. 공개 결제 화면에서 반환하지 않아요. | |
| customer_name | string | null | 항상 | 비공개 firstname, lastname, company 메타데이터에서 만든 표시 이름. |
| customer_address | string | null | 항상 | 비공개 company, street, street2, zip, city, country, countryiso2, vatid 메타데이터에서 만든 한 줄 판매자 주소. |
| description | string | null | 항상 | 고객용 설명. |
| amount | decimal string | 항상 | 정규 청구서 금액. |
| currency | string | 항상 | 정규화된 청구서 통화·자산 코드. |
| exchange_rate_spread_percent | decimal string | 항상 | 고정 견적 스프레드. 생성 시 지정값 또는 생략 시 스토어 기본값이에요. 올림 전에 적용하고 이 청구서에서는 바뀌지 않아요. |
| underpayment_tolerance_percent | decimal string | 항상 | 청구서 생성 시 저장한 변경 불가능한 허용 미달 비율. |
| status | invoice status | 항상 | new, processing, settled, expired, invalid 또는 cancelled. |
| amount_status | amount status | 항상 | none, partial, paid 또는 overpaid. 명시적으로 허용한 금액 0 청구서는 결제 수단 없이 none으로 정산돼요. |
| timing_status | timing status | 항상 | on_time 또는 late. |
| resolution | resolution | 항상 | automatic, manually_settled 또는 manually_invalidated. |
| sequence | integer | 항상 | 1부터 시작하는 단조 증가 청구서 상태 순번. |
| winning_payment_intent_id | UUID | null | 항상 | 선택된 경우 청구서를 완료시킨 결제 수단. |
| expires_at | RFC 3339 timestamp | 항상 | 견적·결제 기한. |
| monitoring_expires_at | RFC 3339 timestamp | 항상 | 결제 수단에 설정된 가장 늦은 지연 모니터링 종료 시간. |
| settled_at | timestamp | null | 항상 | 정산된 경우 정산 시간. |
| cancelled_at | timestamp | null | 항상 | 취소된 경우 취소 시간. |
| archived_at | timestamp | null | 항상 | 보관 처리된 경우 보관 시간. |
| created_at | RFC 3339 timestamp | 항상 | 생성 시간. |
| updated_at | RFC 3339 timestamp | 항상 | 마지막 상태 업데이트 시간. |
청구서 상세 추가 필드
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| ipn_url | string | null | 항상 | 청구서별 유효 IPN 대상. 판매자 응답 전용이며 공개 결제 화면에서는 생략해요. |
| redirect_url | string | null | 항상 | 정산 후 사용하는 유효 성공 URL. |
| cancel_url | string | null | 항상 | 결제가 성공하지 않고 끝날 때 사용하는 유효 반환 URL. |
| redirect_automatically | boolean | 항상 | 성공 후 결제 화면의 자동 이동 여부. |
| checkout_language | string | 항상 | 유효 결제 화면 언어 태그. |
| metadata | object | 항상 | 판매자 메타데이터. 공개 결제 화면에는 반환하지 않아요. |
| payment_intents | PaymentIntent[] | 항상 | 견적된 결제 수단과 모니터링 상태. |
PaymentIntent
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| id | UUID | 항상 | 결제 인텐트 식별자이며 결제 QR의 intent_id로도 써요. |
| payment_rail | onchain | lightning | 항상 | 청구서 전송 방식. Bitcoin 온체인과 Lightning은 asset_id를 공유할 수 있으니 기호만 보지 말고 인텐트 id와 이 필드를 함께 쓰세요. 자산 목록 스캐너의 payment_rail과는 달라요. |
| bolt11 | string | null | 항상 | Lightning 결제 요청이며 아니면 null이에요. Lightning 지갑으로 결제하고 결제 해시에 온체인 자금을 보내지 마세요. |
| asset_id | UUID | 항상 | 설정된 결제 자산 식별자. |
| asset_key | string | 항상 | 정규 CAIP 방식 자산 키. |
| chain_slug | string | 항상 | Wholly Crypto 체인 식별자. |
| network | string | 항상 | 설정된 네트워크. 지원 결제 자산은 현재 mainnet. |
| caip_network_id | string | 항상 | 정규 CAIP-2 네트워크 식별자. |
| caip_asset_id | string | null | 항상 | 등록된 경우 정규 CAIP-19 식별자. |
| symbol | string | 항상 | 자산 기호. |
| asset_decimals | integer | 항상 | 최소 단위 정밀도. Lightning BTC는 온체인 Bitcoin의 8이 아닌 11(밀리사토시)이에요. 견적은 사토시 정수이며 수신은 밀리사토시 정밀도를 유지해요. |
| status | intent status | 항상 | pending, partial, paid, overpaid, expired 또는 invalid. |
| finality_mode | confirmations | finalized | 항상 | 최종 확정 정책. |
| required_confirmations | integer | 항상 | 해당하는 경우 필요한 확인 횟수. |
| quote_rate | decimal string | 항상 | 고정 스프레드를 포함한 청구서 통화 1단위당 자산 단위 수. 예: USD당 1.02 USDC. 역환율이 아니에요. |
| quote_details | object | null | 항상 | 고정 견적 출처: 스프레드 전 reference_rate, unrounded_payment_amount, rounding_adjustment, pricing_provider, asset_provider, pricing_fetched_at, asset_fetched_at. 이전 청구서는 null이며 과거 값을 만들지 않아요. |
| expected_amount | decimal string | 항상 | 스프레드·올림 후 정확히 고정된 지불 자산 금액. 4.1.1부터 인식된 검증 법정화폐 스테이블코인(USDC, USDT, DAI, USDS, EURC 등)은 소수 최대 2자리로 올림해요. 1.321은 1.32가 아닌 1.33이에요. 허용 오차가 0이어도 이 금액이 예상 금액이에요. 다른 자산은 적응형 정밀도를 유지하고 기존 청구서는 재평가하지 않아요. |
| expected_amount_atomic | integer string | 항상 | 자산 최소 단위의 정확한 금액. |
| minimum_payment_amount | decimal string | 항상 | 청구서 허용 오차 적용 후 결제됨으로 수락하는 최소 금액. |
| minimum_payment_amount_atomic | integer string | 항상 | 자산 최소 단위의 정확한 수락 기준. |
| received_amount | decimal string | 항상 | 관찰된 금액. |
| received_amount_atomic | integer string | 항상 | 관찰된 최소 단위 금액. |
| confirmed_amount | decimal string | 항상 | 확인·최종 확정 금액. |
| confirmed_amount_atomic | integer string | 항상 | 확인·최종 확정 최소 단위 금액. |
| destination_address | string | 항상 | 온체인 수신 주소 또는 Lightning의 64자 결제 해시. Lightning 결제는 bolt11을 쓰세요. 해시는 Bitcoin 주소가 아니에요. |
| destination_tag | string | null | 항상 | 경로에 필요한 공개 결제 참조: XRP destination tag, Stellar memo ID, TON 청구서 코멘트. 고유 주소 경로는 null. |
| derivation_index | integer | 항상 | 예약된 지갑 하위 인덱스. 판매자 상세 전용. |
| quote_expires_at | RFC 3339 timestamp | 항상 | 견적 만료. |
| monitoring_expires_at | RFC 3339 timestamp | 항상 | 이 수단의 지연 모니터링 종료 시간. |
| next_check_at | timestamp | null | 항상 | 다음 예정 체인 점검. |
| last_checked_at | timestamp | null | 항상 | 마지막 체인 점검. |
| last_chain_height | integer | null | 항상 | 모니터가 관찰한 마지막 신뢰할 수 있는 높이. |
| last_anchor_hash | string | null | 항상 | 마지막 모니터 기준점·블록 해시. |
| last_monitor_error | string | null | 항상 | 운영자용 안전한 모니터링 진단. |
| first_payment_at | timestamp | null | 항상 | 최초 결제 관찰 시간. |
| fully_paid_at | timestamp | null | 항상 | 수락 최소 금액에 처음 도달한 시간. |
| finalized_at | timestamp | null | 항상 | 결제가 최종 확정 정책을 충족한 시간. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": {
"id": "2f798f9f-01f2-42f0-9d10-5581d6116b4c",
"invoice_id": "0a6a98db-d93d-48ee-8c3c-fd45f90c4a50",
"project_id": "11111111-1111-4111-8111-111111111111",
"store_id": "22222222-2222-4222-8222-222222222222",
"source": "api",
"order_id": "order-1042",
"email": "ada@example.com",
"customer_name": "Ada Lovelace · Example GmbH",
"customer_address": "Example GmbH · 12 Example Street · Suite 2 · 10115 Berlin · Germany (DE) · VAT DE123456789",
"description": "Annual plan",
"amount": "49.9",
"currency": "USD",
"exchange_rate_spread_percent": "0.5",
"underpayment_tolerance_percent": "1",
"status": "settled",
"amount_status": "paid",
"timing_status": "on_time",
"resolution": "automatic",
"sequence": 4,
"winning_payment_intent_id": "33333333-3333-4333-8333-333333333333",
"expires_at": "2026-08-31T18:15:00Z",
"monitoring_expires_at": "2026-09-07T18:15:00Z",
"settled_at": "2026-08-31T18:05:00Z",
"cancelled_at": null,
"archived_at": null,
"created_at": "2026-08-31T18:00:00Z",
"updated_at": "2026-08-31T18:05:00Z",
"ipn_url": "https://merchant.example/wholly/ipn",
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"checkout_language": "en",
"metadata": { "cart_id": "cart-681", "firstname": "Ada", "lastname": "Lovelace", "street": "12 Example Street", "street2": "Suite 2", "zip": "10115", "city": "Berlin", "country": "Germany", "countryiso2": "DE", "company": "Example GmbH", "vatid": "DE123456789" },
"payment_intents": [
{
"id": "33333333-3333-4333-8333-333333333333",
"asset_id": "10000000-0000-4000-8000-000000000001",
"asset_key": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"chain_slug": "bitcoin",
"network": "mainnet",
"caip_network_id": "bip122:000000000019d6689c085ae165831e93",
"caip_asset_id": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"symbol": "BTC",
"asset_decimals": 8,
"status": "paid",
"finality_mode": "confirmations",
"required_confirmations": 1,
"quote_rate": "0.000009218",
"quote_details": null,
"expected_amount": "0.00046",
"expected_amount_atomic": "46000",
"minimum_payment_amount": "0.0004554",
"minimum_payment_amount_atomic": "45540",
"received_amount": "0.0004554",
"received_amount_atomic": "45540",
"confirmed_amount": "0.0004554",
"confirmed_amount_atomic": "45540",
"destination_address": "bc1q…example",
"destination_tag": null,
"derivation_index": 42,
"quote_expires_at": "2026-08-31T18:15:00Z",
"monitoring_expires_at": "2026-09-07T18:15:00Z",
"next_check_at": null,
"last_checked_at": "2026-08-31T18:05:00Z",
"last_chain_height": 912345,
"last_anchor_hash": "000000000000000000example",
"last_monitor_error": null,
"first_payment_at": "2026-08-31T18:03:00Z",
"fully_paid_at": "2026-08-31T18:03:00Z",
"finalized_at": "2026-08-31T18:05:00Z"
}
]
},
"links": {
"checkout": "https://pay.example.com/invoice/0a6a98db-d93d-48ee-8c3c-fd45f90c4a50"
}
}GET청구서 결제 목록/v1/projects/{project_id}/invoices/{invoice_id}/payments읽기 전용
무효화된 관측을 포함한 현재 전체 이체 이력. 콜백에 payments_truncated가 표시되면 사용하세요. 과거 이벤트의 재구성이 아니라 현재 상태예요.
- 관측 하나는 토큰 로그, UTXO 출력 또는 다른 결제 방식의 이체이며, 반드시 고유한 트랜잭션 해시 하나를 뜻하지는 않아요. payment_id로 중복을 제거하세요. transaction_id와 event_index가 체인 이체를 식별해요.
- status는 detected, confirming, final, reorged, replaced 또는 invalid예요. counts_towards_received인 관측만 수신 금액에 반영돼요. 서로 다른 자산의 금액을 절대 합산하지 마세요.
- Lightning 기록은 payment_hash를 사용하고 transaction_id, 확인 수, 탐색기 링크는 null이에요. BTC 정밀도는 11(밀리사토시)이에요. 프리이미지, BOLT11, 지갑 비밀 정보는 노출하지 않아요.
- observed_at 내림차순, 그다음 payment_id 내림차순으로 정렬해요. 개수와 페이지는 하나의 반복 읽기 스냅샷을 사용해요. 결제가 들어오면 이후 페이지는 바뀔 수 있어요. 실시간 청구서를 페이지별로 읽을 때는 payment_id로 중복을 제거하세요.
- 기존 읽기 전용 프로젝트 범위, IP 제한, 자격 증명별 요청 한도가 적용돼요. 콜백이 제공한 링크의 출처가 설정된 API 호스트와 일치하지 않으면 절대 토큰을 담아 접근하지 마세요.
| 헤더 | 필수 여부 | 규칙 |
|---|---|---|
| Authorization | 필수 | Bearer YOUR_MERCHANT_API_TOKEN |
| Accept | 권장 | application/json |
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 이 인증 정보에 지정된 프로젝트. |
| invoice_id | path UUID | 생성 시 반환된 공개 invoice_id. |
| payment_method_id | optional query UUID | 청구서 결제 수단 하나로 제한해요. |
| limit | query integer | 1–100, 기본값은 25. |
| offset | query integer | 0–1,000,000, 기본값은 0. |
요청
: "${WHOLLY_TOKEN:?Set WHOLLY_TOKEN to your server-side API token}"
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID/payments?limit=25&offset=0" \
--header "Authorization: Bearer $WHOLLY_TOKEN" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const token = process.env.WHOLLY_TOKEN;
if (!token) throw new Error("Set WHOLLY_TOKEN");
const response = await fetch("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID/payments?limit=25&offset=0", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`,
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$token = getenv('WHOLLY_TOKEN');
if (!$token) { throw new RuntimeException('Set WHOLLY_TOKEN'); }
$ch = curl_init("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID/payments?limit=25&offset=0");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, "Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
import os
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
headers["Authorization"] = "Bearer " + os.environ["WHOLLY_TOKEN"]
request = Request("https://api.example.com/v1/projects/YOUR_PROJECT_ID/invoices/YOUR_PUBLIC_INVOICE_ID/payments?limit=25&offset=0",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"invoice_id": "11111111-2222-4333-8444-555555555555",
"data": [{
"payment_id": "44444444-4444-4444-8444-444444444444",
"payment_method_id": "33333333-3333-4333-8333-333333333333",
"transaction_id": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"payment_hash": null,
"event_index": 12,
"payment_rail": "onchain",
"chain_slug": "ethereum",
"network": "mainnet",
"asset_id": "55555555-5555-4555-8555-555555555555",
"asset_key": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"caip_asset_id": "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"symbol": "USDC",
"asset_decimals": 6,
"amount": "58.17342",
"amount_atomic": "58173420",
"status": "final",
"counts_towards_received": true,
"confirmations": 2,
"block_height": 25975377,
"observed_at": "2026-09-14T12:03:00Z",
"chain_time": "2026-09-14T12:02:48Z",
"finalized_at": "2026-09-14T12:04:00Z",
"explorer_name": "Etherscan",
"explorer_url": "https://etherscan.io/tx/0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}],
"pagination": {"limit": 25, "offset": 0, "total": 1, "has_more": false}
}GET결제 화면 셸/공개
청구서를 선택하지 않고 결제 앱을 제공하는 관리형 결제 호스트의 루트. 고객 통합은 보통 links.checkout을 사용해야 해요.
- Bearer 토큰은 필요 없어요.
- 관리형 결제 엣지는 GET/HEAD를 허용하고 다른 메서드는 거부해요.
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/" \
--output 'checkout.html'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("checkout.html", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("checkout.html", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("checkout.html").write_bytes(response.read())응답 예시 · 200 text/html
<!doctype html>
<!-- Hosted Wholly Crypto checkout shell -->GET호스팅 결제 페이지/invoice/{invoice_id}공개
고객용 HTML 결제 페이지. 같은 결제 호스트에서 결제 화면용으로 안전한 JSON을 가져와요. 스토어가 임베딩을 켜고 상위 HTTPS 출처를 명시적으로 허용하지 않으면 삽입이 거부돼요.
- Bearer 토큰은 받지 않으며 필요하지도 않아요.
- 청구서가 없어도 HTML 셸 자체는 200을 반환해요. 이후 결제 JSON 요청에는 invoice_not_found가 반환돼요.
- 응답은 no-store, noindex이며 청구서별 frame-ancestors CSP를 포함해요.
- 비활성 프로젝트/스토어이거나 알 수 없는 청구서이면 결제 데이터를 노출하지 않아요.
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| invoice_id | path UUID | 판매자 API가 반환한 공개 청구서 UUID. |
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/invoice/YOUR_PUBLIC_INVOICE_ID" \
--output 'checkout.html'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/invoice/YOUR_PUBLIC_INVOICE_ID", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("checkout.html", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/invoice/YOUR_PUBLIC_INVOICE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("checkout.html", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/invoice/YOUR_PUBLIC_INVOICE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("checkout.html").write_bytes(response.read())응답 예시 · 200 text/html
<!doctype html>
<!-- Hosted Wholly Crypto checkout application -->GET결제 화면용 안전한 청구서 데이터/checkout-api/invoices/{invoice_id}공개
결제 화면 렌더링에 필요한 필드만 반환해요. 내부 ID, 고객 이메일과 파생 주소 필드, IPN URL, 판매자 메타데이터, 지갑 ID, 파생 경로, 모니터 진단은 의도적으로 제외해요.
- Bearer 토큰은 필요 없어요.
- Cache-Control은 no-store이며 검색 색인도 비활성화돼요.
- invoice_id는 고객의 접근 권한을 담은 데이터로 취급하고 불필요하게 공개하지 마세요.
- asset_icon_url은 같은 출처의 로컬 리소스예요. 고객 결제 화면은 아이콘을 표시하기 위해 CoinGecko에 접속할 필요가 없어요.
- destination_tag가 null이 아니면 주소 옆에 표시하고 복사할 수 있게 하세요. 필수 XRP 대상 태그, Stellar memo ID 또는 TON 청구서 메모이므로 정확히 그대로 전송해야 해요.
- 검증된 토큰의 asset_kind는 token이고, contract_address는 정확한 ERC-20 계약 또는 SPL mint를, token_standard는 결제 방식을 식별해요. payment_uri에 해당 토큰 식별자가 포함돼요.
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| invoice_id | path UUID | 공개 청구서 UUID. |
공개 결제 청구서
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| invoice_id | UUID | 항상 | 공개 청구서 UUID. |
| order_id | string | null | 항상 | 판매자 주문 참조. |
| description | string | null | 항상 | 고객용 설명. |
| amount | decimal string | 항상 | 청구서 금액. |
| currency | string | 항상 | 청구서 통화. |
| exchange_rate_spread_percent | decimal string | 항상 | 청구서별 재정의 값을 포함해 생성 시 고정된 실제 견적 스프레드. |
| underpayment_tolerance_percent | decimal string | 항상 | 이 청구서에서 허용하는 부족액 비율. |
| status | invoice status | 항상 | 현재 청구서 상태. |
| amount_status | amount status | 항상 | none, partial, paid 또는 overpaid. 명시적으로 허용한 금액 0 청구서는 결제 수단 없이 none으로 정산돼요. |
| timing_status | timing status | 항상 | on_time 또는 late. |
| sequence | integer | 항상 | 현재 상태 순번. |
| active_payment_method_id | UUID | null | 항상 | 목록 중 자금을 받은 결제 수단. 부족한 금액을 호환되지 않는 자산으로 이어서 결제하지 않도록 결제 화면은 이 수단을 유지해요. |
| payment_method_locked | boolean | 항상 | 유효한 결제로 active_payment_method_id가 선택되면 true예요. |
| server_time | RFC 3339 timestamp | 항상 | 이 응답을 위해 기록한 서버 시각. 고객 기기의 시계 오차를 피하려면 expires_at과 함께 사용하세요. |
| expires_at | RFC 3339 timestamp | 항상 | 청구서 마감 시각. |
| expires_in_seconds | integer | 항상 | server_time 시점의 남은 초. 올림한 정수이며 최소값은 0이에요. |
| payment_open | boolean | 항상 | new 또는 processing 청구서가 마감 전이고, 남은 금액이 있는 결제 가능한 수단이 하나 이상일 때만 true예요. |
| redirect_url | string | null | 항상 | 정산 성공 후 고객이 돌아갈 대상. |
| cancel_url | string | null | 항상 | 정산에 성공하지 않고 떠날 때 고객이 돌아갈 대상. |
| redirect_automatically | boolean | 항상 | 자동 리디렉션 정책. |
| checkout_language | string | 항상 | 결제 화면 언어. |
| project | object | 항상 | name, checkout_title, checkout_description, theme, accent_color, logo_url. |
| store | object | 항상 | 공개 스토어 이름. |
| appearance | CheckoutAppearance | 항상 | 실제 표시 방식: 청구서별 재정의가 있으면 고정된 값을, 없으면 현재 스토어 디자인을 사용해요. 금융 필드나 안전 경고는 절대 바꾸지 않아요. |
| payment_methods | CheckoutPaymentMethod[] | 항상 | 결제 화면에 안전하게 제공할 수 있는 결제 수단. |
CheckoutAppearance
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| inherit_default_store | boolean | 항상 | 프로젝트의 기본 스토어가 외관을 제공하면 true예요. 독립 스토어와 고정된 청구서 재정의는 false예요. |
| invoice_override | boolean | 항상 | 청구서 생성 시 checkout_appearance를 제공하면 true예요. 생략하거나 null이면 false를 유지해요. |
| title / intro / outro | string | 항상 | 판매자 제목, 상단 메시지, 하단 메시지의 일반 텍스트. intro가 customer_message를 대체하며 기존 저장 문구는 보존돼요. 마크업으로 해석하지 마세요. |
| intro_font_size / outro_font_size | integer | 항상 | 픽셀 단위 글자 크기: 12, 14, 16, 18, 20 또는 24. |
| customer_message | string | 항상 | intro의 사용 중단된 호환 별칭. 새 통합에서는 intro를 사용하세요. |
| theme | system | light | dim | dark | 항상 | 고객 기기 설정 또는 고정 테마. |
| accent_color / background_color / card_color / button_color | string | 항상 | 엄격한 #RRGGBB 색상 형식. 선택 색상이 비어 있으면 자동값을 사용하며 전경 대비를 계산해요. |
| logo_size / logo_alignment | string | 항상 | small, medium 또는 large. left 또는 center. 이미지는 잘리지 않고 영역 안에 들어가요. |
| images | object | 항상 | 선택 사항인 logo_light, logo_dark, favicon URL: 범위가 지정된 동일 출처의 정규화된 PNG 이미지. |
| show_order_id / show_description / details_expanded | boolean | 항상 | 주문 ID 표시 여부, 제목 아래 설명, 주문 ID의 초기 펼침 상태. 금액은 계속 표시돼요. 표시 설정일 뿐 데이터를 삭제하거나 가리는 기능은 아니에요. |
| show_project_name / show_store_name | boolean | 항상 | Merchant 5.6.0+: 헤더 이름 표시 여부. 둘 다 기본값은 true예요. 프로젝트/스토어 식별 정보는 JSON에서 계속 제공돼요. |
| featured_chains / featured_asset_ids | array | 항상 | 정렬된 선호 설정으로, 청구서에 이미 있는 수단에만 적용돼요. 없거나 비활성화된 수단은 무시해요. |
| default_asset_id | UUID | null | 항상 | 권장 초기 결제 수단. 유효하게 저장된 고객 선호나 이미 자금을 받은 수단이 우선해요. |
| messages | object | 항상 | waiting, confirming, paid, underpaid, expired를 키로 하는 en/de 일반 텍스트. 영어로 폴백해요. 보충용이며 실제 상태를 대체하지 않아요. |
| support_email / support_url / terms_url / privacy_url | string | 항상 | 선택 연락처와 HTTPS 링크. URL에 자격 증명을 넣을 수 없어요. 외부 링크는 새 창에서 열려요. |
| return_button_text | string | 항상 | 선택 라벨일 뿐이에요. 성공/취소 대상과 리디렉션 정책은 여전히 청구서에 속해요. |
CheckoutPaymentMethod
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| payment_rail | onchain | lightning | 항상 | Lightning은 Bitcoin 결제 수단이지만 온체인 BTC와 별개예요. asset_id만이 아니라 인텐트 id와 결제 방식으로 선택 항목을 식별하세요. |
| bolt11 | string | null | 항상 | 서명된 Lightning 요청. 온체인 수단은 null이에요. payable이 false가 되면 절대 결제하지 마세요. |
| payment_hash | string | null | 항상 | 대사용 Lightning 결제 해시이며 수신 주소가 아니에요. 온체인 수단은 null이에요. |
| id | UUID | 항상 | 결제 인텐트 식별자. |
| asset_id | UUID | 항상 | 외관 선호 설정에 쓰이는 자산 UUID. 이 청구서의 결제 인텐트 id와 달라요. |
| asset_key | string | 항상 | 표준 자산 키. |
| chain_slug / chain_name | string | 항상 | 체인의 기계용 이름과 표시 이름. |
| network | string | 항상 | 결제 네트워크. |
| caip_network_id | string | 항상 | 선택한 체인을 명확히 구분하는 표준 네트워크 식별자. |
| caip_asset_id | string | null | 항상 | 정확한 표준 자산 식별자. 해당하는 경우 검증된 토큰 계약 또는 mint를 포함해요. |
| asset_name / symbol | string | 항상 | 결제 자산 표시 값. |
| asset_icon_url | string | null | 항상 | 동일 출처의 로컬 캐시 자산 아이콘. 검증된 CoinGecko 매핑이 없으면 null이에요. |
| asset_kind | native | token | 항상 | 네이티브 코인 결제와 계약/mint 결제를 구분해요. |
| contract_address | string | null | 항상 | 토큰의 표준 ERC-20 계약 또는 SPL mint. 네이티브 코인은 null이에요. |
| token_standard | erc20 | spl-token | null | 항상 | 검증된 토큰 런타임. 네이티브 코인은 null이에요. |
| asset_decimals | integer | 항상 | 최소 단위 정밀도: Lightning BTC 밀리사토시는 11, 온체인 BTC 사토시는 8. |
| status | intent status | 항상 | 현재 결제 수단 상태. |
| payable | boolean | 항상 | 이 정확한 수단이 현재 결제를 받을 수 있을 때만 true예요. 다른 자산이 자금을 받은 뒤 비활성 수단은 false예요. |
| finality_mode / required_confirmations | string / integer | 항상 | 최종 확정 정책. |
| expected_amount / expected_amount_atomic | decimal / integer string | 항상 | 표시 단위와 실제 온체인 단위의 전체 고정 견적. 인식된 법정화폐 스테이블코인은 견적 소수점이 최대 두 자리이며 스프레드 적용 후 항상 올림해요. 다른 자산은 적응형 정밀도를 사용해요. 실제 토큰 소수 자릿수, 받은 자금, 부분 결제 잔액은 정확하게 유지돼요. 반환된 금액을 바꾸지 말고 사용하세요. |
| minimum_payment_amount / minimum_payment_amount_atomic | decimal / integer string | 항상 | 미달 결제 허용 오차를 적용한 정산 인정 기준액. |
| received_amount / received_amount_atomic | decimal / integer string | 항상 | 관찰된 금액. |
| remaining_amount | decimal string | 항상 | 인정 기준액까지 더 필요한 정확한 표시 금액. 최소값은 0이에요. |
| remaining_amount_atomic | integer string | 항상 | 최소 단위로 나타낸 인정 기준액까지의 부족액. 요청 결제 금액이 아니에요. 허용 오차는 인정 여부에만 영향을 줘요. |
| confirmed_amount / confirmed_amount_atomic | decimal / integer string | 항상 | 확인·최종 확정 금액. |
| destination_address / destination_tag | string / string|null | 항상 | 온체인 수신 대상과 선택 참조값. Lightning에서는 태그 없는 결제 해시예요. 대신 bolt11/payment_uri로 결제하세요. |
| quote_expires_at | RFC 3339 timestamp | 항상 | 견적 만료. |
| payment_uri | string | null | 항상 | 체인에 맞는 요청: ERC-681, Solana Pay, 네이티브 URI 또는 lightning:<bolt11>. 금액이 담긴 요청은 전체 예상 금액에서 받은 자금을 뺀 값을 사용하며, 허용 오차 기준액을 사용하지 않아요. 허용 범위의 부족액이 인정된 뒤를 포함해 payable이 false면 null이에요. Lightning QR에는 결제 해시가 아니라 전체 Lightning 요청이 들어가요. |
| qr_url | path | null | 항상 | 순번과 정확한 잔액의 리비전이 포함된 동일 출처 SVG QR 경로. payable이 false면 null이에요. SVG는 no-store예요. |
| address_explorer_name / address_explorer_url | string|null | 항상 | 지원되는 경우 검증된 메인넷 탐색기 대체 링크. |
| transaction_count | integer | 항상 | 이 수단에서 관측된 서로 다른 공개 유효 트랜잭션의 총수. |
| transactions_truncated | boolean | 항상 | transaction_count가 반환된 최근 트랜잭션 목록의 길이를 넘으면 true예요. |
| transactions | CheckoutTransaction[] | 항상 | 가장 최근의 공개 유효 트랜잭션 최대 10개. 정확한 총 수신액은 이 표시 한도와 별개예요. |
CheckoutTransaction
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| transaction_id | string | 항상 | 관측된 트랜잭션 식별자. |
| status | detected | confirming | final | 항상 | 공개 관측 상태. |
| confirmations | integer | 항상 | 관측된 확인 수. |
| block_height | integer | null | 항상 | 관측된 블록/원장 높이. |
| explorer_name | string | 반환되는 경우 | 검증된 고정 탐색기 이름. |
| explorer_url | string | 반환되는 경우 | 검증된 고정 메인넷 탐색기 URL. |
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const response = await fetch("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID", {
method: "GET",
headers: {
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ["Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
request = Request("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": {
"invoice_id": "0a6a98db-d93d-48ee-8c3c-fd45f90c4a50",
"order_id": "order-1042",
"description": "Annual plan",
"amount": "49.9",
"currency": "USD",
"exchange_rate_spread_percent": "0.5",
"underpayment_tolerance_percent": "1",
"status": "processing",
"amount_status": "partial",
"timing_status": "on_time",
"sequence": 3,
"active_payment_method_id": "33333333-3333-4333-8333-333333333333",
"payment_method_locked": true,
"server_time": "2026-08-31T18:10:00Z",
"expires_at": "2026-08-31T18:15:00Z",
"expires_in_seconds": 300,
"payment_open": true,
"redirect_url": "https://merchant.example/orders/1042",
"cancel_url": "https://merchant.example/cart",
"redirect_automatically": true,
"checkout_language": "en",
"project": {
"name": "Example project",
"checkout_title": "Complete your payment",
"checkout_description": "Send the exact amount shown.",
"theme": "system",
"accent_color": "#42e39b",
"logo_url": "/checkout-api/invoices/…/logo/…/image.png"
},
"store": { "name": "Online shop" },
"payment_methods": [
{
"id": "33333333-3333-4333-8333-333333333333",
"asset_key": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"chain_slug": "bitcoin",
"chain_name": "Bitcoin",
"network": "mainnet",
"caip_network_id": "bip122:000000000019d6689c085ae165831e93",
"caip_asset_id": "bip122:000000000019d6689c085ae165831e93/slip44:0",
"asset_name": "Bitcoin",
"symbol": "BTC",
"asset_icon_url": "/assets/coingecko/bitcoin.png",
"asset_kind": "native",
"contract_address": null,
"token_standard": null,
"asset_decimals": 8,
"status": "partial",
"payable": true,
"finality_mode": "confirmations",
"required_confirmations": 1,
"expected_amount": "0.00046",
"expected_amount_atomic": "46000",
"minimum_payment_amount": "0.0004554",
"minimum_payment_amount_atomic": "45540",
"received_amount": "0.0002",
"received_amount_atomic": "20000",
"remaining_amount": "0.0002554",
"remaining_amount_atomic": "25540",
"confirmed_amount": "0",
"confirmed_amount_atomic": "0",
"destination_address": "bc1q…example",
"destination_tag": null,
"quote_expires_at": "2026-08-31T18:15:00Z",
"payment_uri": "bitcoin:bc1q…example?amount=0.00026",
"qr_url": "/checkout-api/invoices/…/payment-methods/…/qr.svg?sequence=3&amount_atomic=26000",
"address_explorer_name": "mempool.space",
"address_explorer_url": "https://mempool.space/address/…",
"transaction_count": 0,
"transactions_truncated": false,
"transactions": []
}
]
}
}GET스토어 결제 미리보기/invoice/preview/{project_id}공개
예시 금액과 실제 허용 자산 메타데이터로 저장된 스토어 외관을 표시해요. 결제를 만들지 않고 waiting, confirming, paid, underpaid, expired 예시를 전환할 수 있어요.
- 미리보기는 브랜딩 전용이며 고객에게 결제 요청으로 보내면 안 돼요.
- 수신 주소, 결제 가능한 QR, 지갑 작업, 리디렉션, 결제 폴링이 없어요. 예시는 실제 청구서 상태를 바꾸지 않아요.
- 응답은 no-store, noindex이며 삽입할 수 없어요.
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 인증된 콘솔이 미리보기 링크에 넣은 프로젝트 UUID. |
| store_id | query UUID, optional | 이 프로젝트에 속한 스토어. 생략하면 첫 번째/기본 스토어를 사용해요. |
| state | query string, optional | waiting, confirming, paid, underpaid 또는 expired. 브라우저 표시용 예시예요. |
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/invoice/preview/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID&state=confirming" \
--output 'checkout-preview.html'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/invoice/preview/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID&state=confirming", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("checkout-preview.html", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/invoice/preview/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID&state=confirming");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("checkout-preview.html", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/invoice/preview/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID&state=confirming",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("checkout-preview.html").write_bytes(response.read())응답 예시 · 200 text/html
<!doctype html>
<!-- Hosted branding preview; no invoice is created -->GET결제 미리보기 데이터/checkout-api/previews/{project_id}공개
실제 스토어 외관과 안전한 허용 자산 메타데이터를 반환해요. payment_methods는 비어 있고 preview_methods에는 결제 주소, 견적, 비공개 지갑 데이터가 없어요.
- Bearer 토큰은 받지 않으며 필요하지도 않아요.
- 청구서, 수신 대상, 지갑, 트랜잭션, IPN, 웹훅, 판매자 메타데이터는 반환하지 않아요.
- 인증된 콘솔에서 올바른 pay 도메인 미리보기 링크를 받으세요.
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 콘솔 미리보기 링크의 프로젝트 UUID. |
| store_id | query UUID, optional | 이 프로젝트에 속해야 해요. ID가 맞지 않으면 404를 반환하며 알 수 없는 쿼리 필드는 거부해요. |
CheckoutAppearance
| 필드 | 유형 | 필수 여부 | 설명 |
|---|---|---|---|
| inherit_default_store | boolean | 항상 | 프로젝트의 기본 스토어가 외관을 제공하면 true예요. 독립 스토어와 고정된 청구서 재정의는 false예요. |
| invoice_override | boolean | 항상 | 청구서 생성 시 checkout_appearance를 제공하면 true예요. 생략하거나 null이면 false를 유지해요. |
| title / intro / outro | string | 항상 | 판매자 제목, 상단 메시지, 하단 메시지의 일반 텍스트. intro가 customer_message를 대체하며 기존 저장 문구는 보존돼요. 마크업으로 해석하지 마세요. |
| intro_font_size / outro_font_size | integer | 항상 | 픽셀 단위 글자 크기: 12, 14, 16, 18, 20 또는 24. |
| customer_message | string | 항상 | intro의 사용 중단된 호환 별칭. 새 통합에서는 intro를 사용하세요. |
| theme | system | light | dim | dark | 항상 | 고객 기기 설정 또는 고정 테마. |
| accent_color / background_color / card_color / button_color | string | 항상 | 엄격한 #RRGGBB 색상 형식. 선택 색상이 비어 있으면 자동값을 사용하며 전경 대비를 계산해요. |
| logo_size / logo_alignment | string | 항상 | small, medium 또는 large. left 또는 center. 이미지는 잘리지 않고 영역 안에 들어가요. |
| images | object | 항상 | 선택 사항인 logo_light, logo_dark, favicon URL: 범위가 지정된 동일 출처의 정규화된 PNG 이미지. |
| show_order_id / show_description / details_expanded | boolean | 항상 | 주문 ID 표시 여부, 제목 아래 설명, 주문 ID의 초기 펼침 상태. 금액은 계속 표시돼요. 표시 설정일 뿐 데이터를 삭제하거나 가리는 기능은 아니에요. |
| show_project_name / show_store_name | boolean | 항상 | Merchant 5.6.0+: 헤더 이름 표시 여부. 둘 다 기본값은 true예요. 프로젝트/스토어 식별 정보는 JSON에서 계속 제공돼요. |
| featured_chains / featured_asset_ids | array | 항상 | 정렬된 선호 설정으로, 청구서에 이미 있는 수단에만 적용돼요. 없거나 비활성화된 수단은 무시해요. |
| default_asset_id | UUID | null | 항상 | 권장 초기 결제 수단. 유효하게 저장된 고객 선호나 이미 자금을 받은 수단이 우선해요. |
| messages | object | 항상 | waiting, confirming, paid, underpaid, expired를 키로 하는 en/de 일반 텍스트. 영어로 폴백해요. 보충용이며 실제 상태를 대체하지 않아요. |
| support_email / support_url / terms_url / privacy_url | string | 항상 | 선택 연락처와 HTTPS 링크. URL에 자격 증명을 넣을 수 없어요. 외부 링크는 새 창에서 열려요. |
| return_button_text | string | 항상 | 선택 라벨일 뿐이에요. 성공/취소 대상과 리디렉션 정책은 여전히 청구서에 속해요. |
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID" \
--header 'Accept: application/json'// Node.js 18+ · run on your server, never in browser code.
const response = await fetch("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID", {
method: "GET",
headers: {
"Accept": "application/json"
},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ["Accept: application/json"],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
print_r(json_decode($response, true, 512, JSON_THROW_ON_ERROR));# Python 3 · standard library; run on your server.
import json
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {
"Accept": "application/json"
}
request = Request("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID?store_id=YOUR_STORE_ID",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
print(json.load(response))응답 예제 · 200 application/json
{
"data": {
"preview": true,
"invoice_id": "YOUR_PROJECT_ID",
"amount": "100.00",
"currency": "USD",
"project": {
"name": "Example project",
"checkout_title": "Complete your payment",
"checkout_description": "Choose a network and send the exact amount shown.",
"theme": "system",
"accent_color": "#42e39b",
"logo_url": "/checkout-api/previews/…/logo/…/image.png"
},
"appearance": {"inherit_default_store": true, "theme": "system", "accent_color": "#42E39B", "images": {}},
"preview_methods": [],
"payment_methods": []
}
}GET스토어 결제 이미지/checkout-api/invoices/{invoice_id}/appearance-images/{kind}/{revision}/image.png공개
이 청구서에 속한 정규화된 스토어 로고 또는 파비콘을 반환해요. 결제 데이터의 appearance.images URL을 사용하세요.
- 결제 JSON의 appearance.images를 사용하세요. 원본 스토어가 업로드를 교체하거나 삭제해도 고정된 청구서 이미지는 계속 작동해요. 명시적으로 삭제되었거나 청구서·종류가 다르거나 알 수 없는 리비전은 404를 반환해요. 스냅샷이 현재 스토어 이미지로 폴백하는 일은 없어요.
- 청구서 재정의가 없으면 현재 유효한 스토어 이미지를 사용하며 교체/삭제된 리비전은 404를 반환해요. PNG 전용이며 nosniff와 비공개 캐시를 사용해요.
- 인증된 콘솔의 스토어 이미지 업로드는 크기가 제한된 PNG, JPEG, WebP만 받아요. SVG, HTML, 원격 이미지 URL은 받지 않아요.
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| invoice_id | path UUID | 공개 청구서 UUID. |
| kind | path enum | logo_light, logo_dark 또는 favicon. |
| revision | path UUID | 현재 이미지 리비전. |
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png" \
--output 'store-logo.png'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("store-logo.png", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("store-logo.png", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("store-logo.png").write_bytes(response.read())응답 예시 · 200 image/png
(binary PNG response)GET스토어 미리보기 이미지/checkout-api/previews/{project_id}/stores/{store_id}/appearance-images/{kind}/{revision}/image.png공개
프로젝트, 스토어, 종류, 현재 리비전이 모두 맞을 때만 정규화된 미리보기 이미지를 반환해요.
- 미리보기 데이터의 appearance.images를 사용하세요. 알 수 없거나 맞지 않는 ID는 404를 반환해요. 지갑이나 결제 정보는 노출하지 않아요.
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 프로젝트 UUID. |
| store_id | path UUID | 프로젝트에 속한 스토어. |
| kind | path enum | logo_light, logo_dark 또는 favicon. |
| revision | path UUID | 현재 이미지 리비전. |
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png" \
--output 'store-preview-logo.png'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("store-preview-logo.png", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("store-preview-logo.png", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/stores/YOUR_STORE_ID/appearance-images/logo_light/YOUR_IMAGE_REVISION/image.png",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("store-preview-logo.png").write_bytes(response.read())응답 예시 · 200 image/png
(binary PNG response)GET리비전별 미리보기 로고/checkout-api/previews/{project_id}/logo/{revision}/image.png공개
프로젝트와 캐시에 안전한 로고 리비전이 맞을 때만 정규화된 프로젝트 로고를 반환해요. URL을 직접 만들지 말고 미리보기 데이터의 project.logo_url을 사용하세요.
- 알 수 없는 프로젝트와 오래된 로고 리비전은 어느 요소가 없는지 밝히지 않고 invoice_not_found를 반환해요.
- 성공적으로 반환된 리비전 이미지는 불변이며 캐시할 수 있어요.
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| project_id | path UUID | 프로젝트 UUID. |
| revision | path UUID | project.logo_url에 반환된 현재 결제 로고 리비전. |
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/logo/YOUR_LOGO_REVISION/image.png" \
--output 'checkout-preview-logo.png'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/logo/YOUR_LOGO_REVISION/image.png", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("checkout-preview-logo.png", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/logo/YOUR_LOGO_REVISION/image.png");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("checkout-preview-logo.png", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/checkout-api/previews/YOUR_PROJECT_ID/logo/YOUR_LOGO_REVISION/image.png",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("checkout-preview-logo.png").write_bytes(response.read())응답 예시 · 200 image/png
(binary PNG response)GET결제 QR 이미지/checkout-api/invoices/{invoice_id}/payment-methods/{intent_id}/qr.svg공개
청구서 결제 수단의 정확한 체인별 결제 페이로드로 512×512 SVG QR을 생성해요.
- Bearer 토큰은 필요 없어요.
- 결제 JSON이 반환한 순번·잔액 리비전이 포함된 qr_url을 사용하세요. SVG는 비공개이며 no-store예요.
- 부분 결제 후에는 정확한 남은 금액을 요청하며 해당 자산으로 고정돼요.
- 만료·완료 후 또는 다른 수단이 활성화되어 있으면 409를 반환해요. 요청이 너무 커 인코딩할 수 없으면 payment_qr_unavailable(422)을 반환해요.
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| invoice_id | path UUID | 공개 청구서 UUID. |
| intent_id | path UUID | 결제 JSON의 결제 수단 id. |
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/payment-methods/YOUR_INTENT_ID/qr.svg" \
--output 'payment-qr.svg'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/payment-methods/YOUR_INTENT_ID/qr.svg", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("payment-qr.svg", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/payment-methods/YOUR_INTENT_ID/qr.svg");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("payment-qr.svg", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/payment-methods/YOUR_INTENT_ID/qr.svg",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("payment-qr.svg").write_bytes(response.read())응답 예시 · 200 image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">…</svg>GET리비전별 결제 로고/checkout-api/invoices/{invoice_id}/logo/{revision}/image.png공개
청구서와 현재 로고 리비전이 맞을 때만 정규화된 프로젝트 결제 로고를 반환해요. 경로를 직접 만들기보다 결제 JSON이 반환한 project.logo_url을 사용하세요.
- Bearer 토큰은 필요 없어요.
- 리비전이 콘텐츠 주소 방식의 상태이므로 공개 캐시 수명은 1년이고 immutable이 설정돼요.
- 알 수 없거나 맞지 않는 리비전은 invoice_not_found를 반환해요.
| 매개변수 | 유형 / 위치 | 규칙 |
|---|---|---|
| invoice_id | path UUID | 공개 청구서 UUID. |
| revision | path UUID | project.logo_url에 포함된 현재 결제 로고 리비전. |
요청
curl --fail-with-body --max-time 30 \
--request GET \
--url "https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/logo/YOUR_LOGO_REVISION/image.png" \
--output 'checkout-logo.png'// Node.js 18+ · run on your server, never in browser code.
import { writeFile } from "node:fs/promises";
const response = await fetch("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/logo/YOUR_LOGO_REVISION/image.png", {
method: "GET",
headers: {},
redirect: "error",
signal: AbortSignal.timeout(30000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
await writeFile("checkout-logo.png", Buffer.from(await response.arrayBuffer()));<?php
// PHP 8+ with the cURL extension; run on your server.
$ch = curl_init("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/logo/YOUR_LOGO_REVISION/image.png");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [],
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) { throw new RuntimeException(curl_error($ch)); }
curl_close($ch);
if ($status < 200 || $status >= 300) { throw new RuntimeException("HTTP $status: $response"); }
file_put_contents("checkout-logo.png", $response);# Python 3 · standard library; run on your server.
import json
from pathlib import Path
from urllib.request import Request, build_opener, HTTPRedirectHandler
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
headers = {}
request = Request("https://pay.example.com/checkout-api/invoices/YOUR_PUBLIC_INVOICE_ID/logo/YOUR_LOGO_REVISION/image.png",
method="GET", headers=headers)
# Non-2xx responses raise HTTPError. Do not retry writes with a new key.
with build_opener(NoRedirect()).open(request, timeout=30) as response:
Path("checkout-logo.png").write_bytes(response.read())응답 예시 · 200 image/png
(binary PNG response)Wholly Crypto 7.5.5용 참고 문서예요. 설치된 버전의 문서는 콘솔에서 설정 → API 액세스 → 문서를 여세요. 릴리스 보기.