본문 바로가기
빌링플러스개발자센터
홈 로그인 도입 상담

시작하기

빠른 시작

API 키 발급부터 서명한 첫 요청(ping)까지 10분 안에 연동을 확인하는 순서입니다.

결제를 일으키지 않는 점검 경로 /api/v1/ping 으로 키·서명·시각·멱등키 설정이 맞는지 먼저 확인합니다. 여기까지 되면 나머지 API 는 같은 방식으로 부르면 됩니다.

1. API 키 발급

  1. 가맹점 마스터 계정으로 빌링플러스에 로그인합니다. 키 발급·폐기는 마스터만 할 수 있습니다.
  2. 연동관리 > API 연동에서 용도(예: "홈페이지 서버")를 적고 키를 발급합니다. 연동 서버의 IP 를 알면 허용 IP 에 적어 두세요(선택).
  3. 발급 화면에 두 값이 나옵니다.
값모양쓰임
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
# 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 으로 시계 차이를 확인합니다.

응답 200
{
  "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 예제입니다.

Python 3.10 이상 · requests
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 으로 두 가지를 한 번에 확인합니다.

Python
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_REQUIREDPOST 에 Idempotency-Key 헤더가 없습니다.
429 RATE_LIMITED분당 호출 한도를 넘었습니다. Retry-After 초만큼 기다렸다가 보내세요.
404 NOT_FOUND주소에 오타가 있거나 /api/v1/ 이 빠졌습니다.

인증에 실패한 요청(401)은 어느 가맹점의 것인지 알 수 없어 화면의 API 호출 로그에 남지 않습니다. 인증을 통과한 요청은 모두 남으니, 에러 응답의 request_id 로 찾아보세요.

다음 단계