개발자/시작하기
인증
모든 API 요청에는 Merchant ID와 API 키가 들어가야 합니다.
주문 요청(POST/orders)에는 서명도 함께 보냅니다. 서명이 있으면 누구도 주문 내용을 바꾸거나 같은 요청을 다시 보낼 수 없습니다.
#인증 정보
| 항목 | 예시 | 비밀 정보인가요? |
|---|---|---|
| Merchant ID | M00000001 | 아니요. 귀사를 식별하는 값입니다. |
| API 키 | cvb2b_... | 예. 비밀번호이자 서명 키입니다. |
- Merchant ID는 계정이 승인되면 CardV가 발급합니다.
- API 키는 Owner가 포털에서 만듭니다(API 키 참고).
- Live와 Sandbox는 키가 서로 다릅니다. 한쪽 키는 다른 쪽에서 쓸 수 없습니다.
- API는 CardV가 사업자 심사를 승인한 뒤부터 사용할 수 있습니다.
승인되면
GET/account응답에"api_access_enabled": true가 표시됩니다.
#요청 헤더
모든 요청에 다음 두 헤더를 보냅니다.
X-Merchant-Id: M00000001
X-Api-Key: cvb2b_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPOST/orders에는 다음 세 헤더를 추가로 보냅니다.
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfedX-Timestamp는 현재 Unix 시간(초)입니다. CardV 서버 시각과 5분(300초) 이상 차이 나면 안 됩니다.X-Nonce는 한 번만 쓰는 임의의 값입니다. 16진수 문자 32개로 된 난수를 사용하세요.X-Signature는 주문 서명에서 설명합니다. 소문자 16진수여야 합니다.
GET 요청에는 서명이 필요 없습니다.
#주문 서명
주문 내용을 JSON 문자열로 한 번만 만듭니다. 서명할 때와 전송할 때 이 바이트를 그대로 씁니다.
본문 해시를 구합니다.
BODY_HASH= 본문의 SHA-256 값(소문자 16진수)아래 다섯 줄을 줄바꿈(
\n)으로 이어 붙입니다. 마지막 줄 뒤에는 줄바꿈을 넣지 않습니다.POST /api/v1/orders <X-Timestamp> <X-Nonce> <BODY_HASH>서명합니다.
X-Signature= 위 문자열의 HMAC-SHA256 값이며, 키로는 API 키 전체를 사용합니다. 결과는 소문자 16진수로 표기합니다.
가장 흔한 실수는 서명한 JSON과 실제로 보낸 JSON이 다른 경우입니다.
예를 들어 {"a":1}과 {"a": 1}은 바이트가 다릅니다.
서명한 뒤 HTTP 라이브러리가 본문을 바꾸지 않는지 꼭 확인하세요.
테스트 값. 아래 값으로 코드를 검증하세요. 키는 가짜입니다.
API key cvb2b_TEST_ONLY_not_a_real_key_0123456789abcdef
Method POST
Path /api/v1/orders
X-Timestamp 1790000000
X-Nonce 0123456789abcdef0123456789abcdef본문(108바이트, 한 줄, 끝에 줄바꿈 없음):
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}올바른 결과:
BODY_HASH b0545ae25d54b219f27d8bd90e4dcbf26cf0d491f53982da67b8eef0a1a59960
X-Signature fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed#예제 코드
아래 예제는 모두 위 테스트 값과 같은 결과를 냅니다. API 키는 코드에 적지 말고 시크릿 저장소에서 불러오세요.
cURL (bash + OpenSSL)
BASE_URL="https://sandbox.cardv.net"
REQ_PATH="/api/v1/orders" # do not call this variable PATH
BODY='{"external_order_id":"SHOP-10001","items":'
BODY+='[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}'
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.*= //')
SIG=$(printf 'POST\n%s\n%s\n%s\n%s' "$REQ_PATH" "$TS" "$NONCE" "$BODY_HASH" \
| openssl dgst -sha256 -hmac "$CARDV_API_KEY" -hex | sed 's/^.*= //')
curl -sS -X POST "$BASE_URL$REQ_PATH" \
-H "Content-Type: application/json" \
-H "X-Merchant-Id: $CARDV_MERCHANT_ID" \
-H "X-Api-Key: $CARDV_API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIG" \
--data-raw "$BODY"-d @file 대신 --data-raw를 쓰세요. -d는 줄바꿈을 지우기 때문에 실제로 전송되는 바이트가 서명과 달라집니다.
Python (requests)
import hashlib, hmac, json, os, secrets, time
import requests
BASE_URL = "https://sandbox.cardv.net"
MERCHANT_ID = os.environ["CARDV_MERCHANT_ID"]
API_KEY = os.environ["CARDV_API_KEY"]
AUTH = {"X-Merchant-Id": MERCHANT_ID, "X-Api-Key": API_KEY}
def sign(path: str, body: bytes, ts: str, nonce: str) -> str:
body_hash = hashlib.sha256(body).hexdigest()
text = "\n".join(["POST", path, ts, nonce, body_hash])
return hmac.new(API_KEY.encode(), text.encode(), hashlib.sha256).hexdigest()
def cardv_get(path: str, params=None) -> requests.Response:
return requests.get(BASE_URL + path, params=params, headers=AUTH, timeout=30)
def cardv_post(path: str, payload: dict) -> requests.Response:
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode()
ts, nonce = str(int(time.time())), secrets.token_hex(16)
headers = {
**AUTH,
"Content-Type": "application/json",
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": sign(path, body, ts, nonce),
}
return requests.post(BASE_URL + path, data=body, headers=headers, timeout=30)
resp = cardv_post("/api/v1/orders", {
"external_order_id": "SHOP-10001",
"items": [{"sku_id": "S000001", "quantity": 1, "expected_unit_price": "9.2500"}],
})
print(resp.status_code, resp.json())
order_id = resp.json()["order"]["order_id"]
print(cardv_get(f"/api/v1/orders/{order_id}").json()["status"])Node.js 18+ (내장 fetch)
import crypto from "node:crypto";
const BASE_URL = "https://sandbox.cardv.net";
const { CARDV_MERCHANT_ID, CARDV_API_KEY } = process.env;
const AUTH = { "X-Merchant-Id": CARDV_MERCHANT_ID, "X-Api-Key": CARDV_API_KEY };
function sign(path, body, timestamp, nonce) {
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const text = ["POST", path, timestamp, nonce, bodyHash].join("\n");
return crypto.createHmac("sha256", CARDV_API_KEY).update(text, "utf8").digest("hex");
}
async function cardvPost(path, payload) {
const body = Buffer.from(JSON.stringify(payload), "utf8"); // serialize once
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString("hex");
const headers = {
...AUTH,
"Content-Type": "application/json",
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": sign(path, body, timestamp, nonce),
};
const res = await fetch(BASE_URL + path, { method: "POST", headers, body });
return { status: res.status, body: await res.json() };
}
console.log(await cardvPost("/api/v1/orders", {
external_order_id: "SHOP-10001",
items: [{ sku_id: "S000001", quantity: 1, expected_unit_price: "9.2500" }],
}));PHP (서명 부분만)
<?php
function cardv_sign(
string $apiKey, string $path, string $body, string $ts, string $nonce
): string {
$text = implode("\n", ['POST', $path, $ts, $nonce, hash('sha256', $body)]);
return hash_hmac('sha256', $text, $apiKey); // lowercase hex
}
// Send exactly $body.
$body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ts = (string) time();
$nonce = bin2hex(random_bytes(16));
$signature = cardv_sign($apiKey, '/api/v1/orders', $body, $ts, $nonce);#오류
아래 오류는 모두 HTTP 403(401이 아님)을 반환합니다. 요청 한도 초과만 429를 반환합니다. 응답 본문은 다음과 같습니다.
{"detail": "Invalid HMAC signature."}| 문제 | 해결 방법 |
|---|---|
| Merchant ID 또는 API 키가 없거나 틀림 | 두 헤더와 환경(Live/Sandbox)을 확인하세요. |
| API 사용이 아직 허용되지 않음 | CardV의 사업자 심사 승인을 기다리세요. |
| 서버 IP가 허용되지 않음 | 포털의 IP 허용 목록에 추가하세요. |
| 포털 전용 엔드포인트 | 포털에서 처리하세요. API 키로는 7개 엔드포인트만 쓸 수 있습니다. |
| 서명이 없거나 틀림 | 서명 코드를 고치고 테스트 값으로 검증하세요. |
| 타임스탬프가 너무 오래됐거나 미래 시각임 | 서버 시계를 동기화하세요(NTP). |
| 이미 사용한 nonce | 주문 요청마다 새 nonce를 만드세요. |
| 요청이 너무 많음(429) | Retry-After에 적힌 초만큼 기다린 뒤 다시 보내세요. |
꼭 알아 두세요.
- 주문을 다시 보낼 때는 타임스탬프, nonce, 서명을 새로 만드세요. 본문은 그대로 둡니다. 안전한 재시도를 참고하세요.
- 서명이 틀린 요청이라도 한 번 보낸 nonce는 다시 쓸 수 없습니다.
- CardV는 실패한 요청을 기록하며, 실패가 많으면 담당 팀에 알림이 갑니다.
#IP 허용 목록
IP 허용 목록을 설정하면 귀사 서버에서만 API 키를 쓸 수 있습니다. 포털에서 Owner가 관리합니다.
- 선택 사항입니다. 규칙이 없으면 모든 IP의 요청을 받습니다.
- 규칙이 하나라도 있으면, 목록에 없는 IP의 요청은 HTTP 403을 받습니다.
- 규칙은
203.0.113.10/32(IPv4)나2001:db8::/48(IPv6) 형식으로 씁니다. - API 키 요청에만 적용되며, 포털 로그인에는 적용되지 않습니다.
- 켜기 전에 NAT 게이트웨이와 백업 리전을 포함해 모든 서버 IP를 먼저 등록하세요.
#API 키
API 키로는 API 한눈에 보기에 나온 7개 엔드포인트만 호출할 수 있습니다. 여기에는 주문과 코드 조회도 포함되므로, Owner 비밀번호처럼 철저히 관리하세요.
키는 Owner만 관리할 수 있으며, 포털의 Integrations → API keys에서 관리합니다. Live와 Sandbox에서 각각 따로 설정합니다.
- 만들기: 키를 만들고 이름을 붙입니다. 이 단계에서는 키가 아직 표시되지 않습니다.
- 확인(Reveal): CardV가 6자리 인증 코드를 이메일로 보냅니다. 코드는 10분간 유효하며 5번까지 입력할 수 있습니다. 코드를 입력하면 키 전체가 표시됩니다. 바로 저장하세요. 키를 확인할 때마다 기록이 남습니다.
- 비활성화: 더 이상 쓰지 않는 키는 비활성화합니다. 즉시 적용되며 되돌릴 수 없습니다.
서비스 중단 없이 키를 교체하는 방법:
- 새 키를 만들고 확인합니다.
- 모든 서버에 새 키를 배포합니다.
- 포털에서 이전 키가 더 이상 쓰이지 않는지 확인합니다.
- 이전 키를 비활성화합니다.
키는 최소 1년에 한 번, 그리고 키에 접근할 수 있던 사람이 퇴사할 때마다 교체하세요. 키가 유출됐을 수 있다면 먼저 비활성화한 다음 원인을 조사하세요.
연동 관련 문의는 Merchant ID와 주문 ID 또는 요청 ID를 포함해 [email protected]로 보내 주세요.