접속·공통 인증
운영 Base URL: https://<운영 도메인>/api/v1. 예제의 pay.example.com은 자리표시자입니다. 현재 로컬 주소는 http://127.0.0.1:3000/api/v1이며 외부 가맹점용 운영 주소는 아직 없습니다.
첫 호출: 접속과 인증 확인
가맹점 서버의 Bash 터미널에서 실행합니다. 아래 URL과 API 키를 발급받은 값으로 교체하면 운영자금 조회 결과를 받습니다. 응답 전체는 운영자금 조회에서 확인할 수 있습니다.
curl --request GET 'https://pay.example.com/api/v1/balance' \
--header 'x-api-key: <가맹점 API 키>'| 필드 | 타입 | 필수 | 설명 | 예시 |
|---|---|---|---|---|
x-api-key | string | 필수 | 가맹점 서버에서 전송합니다. 관리자 키·포털 쿠키·Bearer 토큰은 사용하지 않습니다. | <가맹점 API 키> |
Content-Type | string | 선택 | JSON 본문이 있을 때 사용합니다. 웹훅 설정 PUT에서는 필수입니다. | application/json |
- 지원 자산은 TRON Mainnet USDT(TRC-20)입니다. 다른 네트워크나 자산은 지원하지 않습니다.
- 요청 금액은 소수 문자열을 권장합니다. 최대 6자리이며 응답 금액도 문자열입니다. 날짜는 UTC ISO 8601, 값이 없을 때는 null입니다.
- 가맹점 서버가 회원 잔액·지급 권한을 관리합니다. COINPG는 가맹점 전체 운영자금으로 지급합니다.
- 아래 성공 JSON은 실제 응답 필드를 모두 포함한 형식 예시입니다. 키·주소·ID·시간은 예시 값으로 바꿔 표시했습니다.
받은 값을 다음 작업에 연결하기
| 작업 | 호출·확인 | 저장하거나 처리할 값 |
|---|---|---|
| 회원 입금주소 발급 | PUT /users/{externalUserId} | externalUserId는 가맹점 회원 ID입니다. 응답의 deposit_address를 해당 회원과 연결해 저장하고 회원에게 안내합니다. |
| 회원 입금 반영 | GET /transactions | 회원이 입금한 뒤 type=deposit, status=confirmed 거래의 external_user_id로 회원을 찾고 id로 중복을 막아 amount를 장부에 한 번 반영합니다. |
| 회원에게 지급 요청 | POST /payouts | 주문·요청 본문·Idempotency-Key를 먼저 저장하고 호출합니다. 받은 지급 id를 주문에 저장합니다. HTTP 202는 접수이며 지급 완료가 아닙니다. |
| 지급 결과 확정 | GET /payouts/{id} | 저장한 지급 id로 조회합니다. status=confirmed면 완료, failed면 실패로 처리합니다. 결과가 불명확한 요청은 같은 본문·멱등 키로 재확인합니다. |
결과 알림을 받으려면 웹훅을 등록합니다. 수신한 입금을 회원 장부에 연결하는 과정은 입금 1건 처리 예제를 참고하세요.
오류 응답 형식
| 필드 | 타입 | 설명 |
|---|---|---|
error | string | 사람이 읽을 오류 설명. 문구 대신 HTTP 상태와 code로 분기합니다. |
code | string | 오류 식별자. 각 API의 오류 표를 참고합니다. |
{
"error": "B2B 업체 인증이 필요합니다.",
"code": "USER_REQUEST_FAILED"
}Node.js 공통 클라이언트 코드
아래 코드를 merchant-api-client.mjs로 저장합니다. Node.js 22.18 이상에서 추가 패키지 없이 사용합니다.
// Node.js 22.18+. Importing this file makes no requests and starts no payouts.
export class CoinpgApiError extends Error {
constructor(status, code, message) {
super(message);
this.name = "CoinpgApiError";
this.status = status;
this.code = code;
}
}
export class CoinpgClient {
constructor({ baseUrl, apiKey, timeoutMs = 15_000 }) {
if (!baseUrl || !apiKey) throw new Error("COINPG Base URL and merchant API key are required.");
const url = new URL(baseUrl);
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
if ((url.protocol !== "https:" && !(local && url.protocol === "http:")) ||
url.username || url.password || url.search || url.hash ||
url.pathname.replace(/\/$/u, "") !== "/api/v1") {
throw new Error("Use an HTTPS Base URL ending in /api/v1 (HTTP is allowed on loopback).");
}
this.baseUrl = url.href.replace(/\/$/u, "");
this.apiKey = apiKey;
this.timeoutMs = timeoutMs;
}
async request(path, { method = "GET", body, idempotencyKey } = {}) {
if (!path.startsWith("/") || path.startsWith("//")) throw new Error("Use a relative API path.");
const response = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
"x-api-key": this.apiKey,
...(body !== undefined ? { "content-type": "application/json" } : {}),
...(idempotencyKey ? { "idempotency-key": idempotencyKey } : {}),
},
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
redirect: "error",
signal: AbortSignal.timeout(this.timeoutMs),
});
let payload;
try { payload = await response.json(); }
catch {
throw new CoinpgApiError(response.status, "INVALID_RESPONSE", "API response is not JSON; reconcile the original request.");
}
if (!response.ok) {
throw new CoinpgApiError(response.status, payload?.code ?? "HTTP_ERROR", payload?.error ?? "API request failed.");
}
return payload;
}
getBalance() { return this.request("/balance"); }
getUser(externalUserId) { return this.request(`/users/${encodeURIComponent(externalUserId)}`); }
createOrGetUser(externalUserId, displayName) {
return this.request(`/users/${encodeURIComponent(externalUserId)}`, {
method: "PUT", body: displayName === undefined ? {} : { display_name: displayName },
});
}
listTransactions({ userId, limit, cursor } = {}) {
const query = new URLSearchParams();
if (userId !== undefined) query.set("user_id", userId);
if (limit !== undefined) query.set("limit", String(limit));
if (cursor !== undefined && cursor !== null) query.set("cursor", cursor);
return this.request(`/transactions${query.size ? `?${query}` : ""}`);
}
requestPayout(body, idempotencyKey) {
// Reuse the persisted body and key after any ambiguous response.
if (!idempotencyKey) throw new Error("Persist the payout idempotency key before calling requestPayout.");
return this.request("/payouts", { method: "POST", body, idempotencyKey });
}
getPayout(payoutId) { return this.request(`/payouts/${encodeURIComponent(payoutId)}`); }
}
export COINPG_API_BASE_URL='https://pay.example.com/api/v1'
export COINPG_API_KEY='<가맹점 API 키>'각 항목의 Node.js 호출 예제는 아래 coinpg 인스턴스를 사용합니다.
import { CoinpgClient } from './merchant-api-client.mjs';
const coinpg = new CoinpgClient({
baseUrl: process.env.COINPG_API_BASE_URL,
apiKey: process.env.COINPG_API_KEY,
});