시작하기
빠른 시작
API 키 발급부터 서명한 첫 요청(ping)까지 10분 안에 연동을 확인하는 순서입니다.
결제를 일으키지 않는 점검 경로 /api/v1/ping 으로 키·서명·시각·멱등키 설정이 맞는지 먼저 확인합니다. 여기까지 되면 나머지 API 는 같은 방식으로 부르면 됩니다.
1. API 키 발급
- 가맹점 마스터 계정으로 빌링플러스에 로그인합니다. 키 발급·폐기는 마스터만 할 수 있습니다.
- 연동관리 > API 연동에서 용도(예: "홈페이지 서버")를 적고 키를 발급합니다. 연동 서버의 IP 를 알면 허용 IP 에 적어 두세요(선택).
- 발급 화면에 두 값이 나옵니다.
| 값 | 모양 | 쓰임 |
|---|---|---|
| API 키 | bpk_live_ 로 시작 | 요청 헤더 X-BillingPlus-Key. 공개되어도 괜찮은 식별자입니다. |
| 시크릿 키 | bos_ 로 시작 | 서명 계산에만 씁니다. 발급 화면에서 한 번만 보입니다. |
시크릿 키는 비밀번호와 같습니다. 서버의 환경변수나 시크릿 저장소에만 두고, 소스코드·브라우저 스크립트·앱·메신저에 넣지 마세요. 잃어버리면 다시 볼 수 없으니 새 키를 발급하고 옛 키를 폐기합니다. 활성 키는 4개까지 둘 수 있어 새 키 발급 → 연동 전환 → 옛 키 폐기 순서로 끊김 없이 바꿀 수 있습니다.
2. 서버 시계 맞추기
요청 시각이 빌링플러스 서버 시각과 5분 넘게 차이 나면 거부됩니다. 연동 서버의 시간 동기화(NTP)가 켜져 있는지 확인하세요. GET /api/v1/ping 응답의 server_time 이 기준 시각입니다(UTC 로 표기되어 한국 시각에서 9시간을 뺀 값으로 보입니다).
3. 첫 요청 — GET /api/v1/ping
터미널에서 바로 확인하려면 아래 셸 예제를 쓰세요. 키와 시크릿은 환경변수 BILLINGPLUS_API_KEY · BILLINGPLUS_API_SECRET 로 넘깁니다.
# bash + openssl + curl. 키·시크릿은 환경변수로 넘깁니다(명령 기록에 남지 않게).
URL="https://api.billingplus.co.kr/api/v1/ping"
PATH_Q="/${URL#*://*/}" # 서명할 경로 — 쿼리가 있으면 쿼리까지
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY=""
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.*= //')
SIG=$(printf '%s\n%s\n%s\n%s\n%s' "GET" "$PATH_Q" "$TS" "$NONCE" "$BODY_HASH" \
| openssl dgst -sha256 -hmac "$BILLINGPLUS_API_SECRET" -hex | sed 's/^.*= //')
curl -s "$URL" \
-H "X-BillingPlus-Key: $BILLINGPLUS_API_KEY" \
-H "X-BillingPlus-Timestamp: $TS" \
-H "X-BillingPlus-Nonce: $NONCE" \
-H "X-BillingPlus-Signature: $SIG"성공하면 이런 응답이 옵니다. tenant 로 어느 가맹점의 키로 붙었는지, server_time 으로 시계 차이를 확인합니다.
{
"ok": true,
"tenant": "예시상점",
"api_key": "bpk_live_3…9f2e",
"server_time": "2026-10-28T05:00:03.512031+00:00"
}서비스 코드에 넣을 때는 인증 · 요청 서명의 언어별 예제(Python · Node.js · PHP · Java)를 쓰세요. 아래는 Python 예제입니다.
import hashlib
import hmac
import json
import os
import secrets
import time
from urllib.parse import urlsplit
import requests
API_BASE = "https://api.billingplus.co.kr/api/v1"
API_KEY = os.environ["BILLINGPLUS_API_KEY"] # bpk_live_ 로 시작
API_SECRET = os.environ["BILLINGPLUS_API_SECRET"] # bos_ 로 시작 — 서버에만 둡니다
def sign(method: str, path_with_query: str, timestamp: str, nonce: str,
body: bytes, secret: str) -> str:
body_hash = hashlib.sha256(body).hexdigest() # 본문이 없으면 b"" 의 해시
message = "\n".join([method.upper(), path_with_query, timestamp, nonce, body_hash])
return hmac.new(secret.encode("utf-8"), message.encode("utf-8"),
hashlib.sha256).hexdigest()
def call(method: str, path: str, payload=None, idempotency_key: str | None = None):
"""path 예: "/ping", "/customers?limit=5" — 기준 주소 뒤에 붙는 부분"""
method = method.upper()
url = API_BASE + path
parts = urlsplit(url)
path_with_query = parts.path + (f"?{parts.query}" if parts.query else "")
body = b"" if payload is None else json.dumps(
payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
timestamp = str(int(time.time())) # 밀리초가 아니라 초
nonce = secrets.token_hex(16) # 요청마다 새 32자
headers = {
"X-BillingPlus-Key": API_KEY,
"X-BillingPlus-Timestamp": timestamp,
"X-BillingPlus-Nonce": nonce,
"X-BillingPlus-Signature": sign(method, path_with_query, timestamp, nonce,
body, API_SECRET),
}
if body:
headers["Content-Type"] = "application/json; charset=utf-8"
if method == "POST":
if not idempotency_key:
raise ValueError("POST 에는 Idempotency-Key 가 필요합니다. "
"재시도 때 같은 값을 쓰도록 미리 만들어 저장하세요.")
headers["Idempotency-Key"] = idempotency_key
# 서명한 바이트를 그대로 보냅니다. json= 으로 넘기면 다시 직렬화되어 서명이 어긋날 수 있습니다.
return requests.request(method, url, data=body, headers=headers, timeout=(5, 60))
if __name__ == "__main__":
print(call("GET", "/ping").json())4. POST 확인 — 멱등키
모든 POST 에는 Idempotency-Key 헤더가 필요하고, 서명에 본문 해시가 들어갑니다. POST /api/v1/ping 으로 두 가지를 한 번에 확인합니다.
res = call("POST", "/ping", {"hello": "빌링플러스"}, idempotency_key="ping-check-0001")
print(res.status_code, res.json())
# 200 {'ok': True, 'tenant': '예시상점', 'idempotency_key': 'ping-check-0001', 'received_bytes': 27}
res = call("POST", "/ping", {"hello": "빌링플러스"}, idempotency_key="ping-check-0001")
print(res.headers.get("Idempotent-Replay")) # "true" — 다시 실행하지 않고 처음 응답을 돌려줬다같은 키로 다시 보냈을 때 응답 헤더에 Idempotent-Replay: true 가 붙으면 멱등키가 제대로 동작하는 것입니다. 결제 승인에서도 같은 원리로 재시도가 두 번째 승인이 되지 않습니다.
막혔을 때
| 응답 | 먼저 볼 것 |
|---|---|
401 UNAUTHORIZED | 키·서명·시각·난수 중 하나가 틀렸습니다. 이유는 일부러 알려 주지 않습니다. 서명할 경로에 /api/v1 과 쿼리스트링이 들어갔는지, 시각이 초 단위인지, 서명이 소문자 16진수인지, 난수를 매번 새로 만드는지 확인하세요. |
403 FORBIDDEN_IP | 서명은 맞았지만 허용 IP 밖에서 호출했습니다. 응답의 detail.ip 를 API 연동 화면의 허용 IP 에 넣으세요. |
400 IDEMPOTENCY_KEY_REQUIRED | POST 에 Idempotency-Key 헤더가 없습니다. |
429 RATE_LIMITED | 분당 호출 한도를 넘었습니다. Retry-After 초만큼 기다렸다가 보내세요. |
404 NOT_FOUND | 주소에 오타가 있거나 /api/v1/ 이 빠졌습니다. |
인증에 실패한 요청(401)은 어느 가맹점의 것인지 알 수 없어 화면의 API 호출 로그에 남지 않습니다. 인증을 통과한 요청은 모두 남으니, 에러 응답의 request_id 로 찾아보세요.
다음 단계
- 등록창으로 카드 등록 — 회원 카드를 받는 첫 번째 방법
- 웹훅 받기 — 결제 결과를 서버로 받기
- 멱등키 — 결제 재시도를 안전하게
개발자센터