API 규약
멱등키(Idempotency-Key)
결제가 두 번 승인되지 않도록 모든 POST 요청에 멱등키를 붙이는 방법과 재시도 규칙.
네트워크가 끊겨 응답을 못 받으면 요청이 처리됐는지 알 수 없습니다. 이때 다시 보내면 같은 결제가 두 번 승인될 수 있습니다. 멱등키(Idempotency-Key)는 "이 요청은 앞의 그 요청과 같은 일이다" 를 알려 주는 표식입니다. 같은 키로 다시 보내면 빌링플러스는 처음 결과를 그대로 돌려주고 다시 실행하지 않습니다.
규칙
- 모든 POST 요청에
Idempotency-Key헤더가 필요합니다. 없으면400 IDEMPOTENCY_KEY_REQUIRED입니다. GET·PATCH·DELETE 에는 필요 없습니다. - 키는 200자 이하 문자열입니다. 가맹점 안에서 한 업무에 한 키를 씁니다.
- 결과는 가맹점별로 24시간 보관됩니다.
| 상황 | 응답 |
|---|---|
| 처음 보는 키 | 요청을 실행하고 결과를 저장합니다. |
| 같은 키 + 같은 요청, 앞 요청이 끝남 | 저장된 응답을 그대로 돌려줍니다(상태 코드·본문 같음). 응답 헤더에 Idempotent-Replay: true 가 붙습니다. |
| 같은 키, 앞 요청이 아직 처리 중 | 409 REQUEST_IN_PROGRESS + Retry-After(초). 기다렸다가 같은 키로 다시 보내세요. |
| 같은 키인데 본문·경로·메서드가 다름 | 422 IDEMPOTENCY_KEY_REUSED. 실수로 키를 재사용한 것입니다. 새 요청에는 새 키를 씁니다. |
| 앞 요청이 서버 오류(5xx)로 끝남 | 결과를 저장하지 않습니다. 같은 키로 다시 보내도 안전합니다. |
| 24시간이 지난 키 | 새 요청으로 처리합니다. |
4xx 도 결과입니다. 예를 들어 422(처리할 수 없음)를 받은 요청을 같은 키로 다시 보내면 같은 422 가 돌아옵니다. 값을 고쳐 다시 요청할 때는 새 키를 쓰세요. 같은 키로 고친 본문을 보내면 422 IDEMPOTENCY_KEY_REUSED 입니다.
예외 — 등록창 생성(POST /api/v1/register-windows). 창 주소(window_url)에는 창을 여는 열쇠가 들어 있어 빌링플러스가 저장하지 않습니다. 그래서 같은 키로 다시 보내면 저장된 응답이 아니라 창의 지금 상태가 돌아오고, 창은 새로 생기지 않습니다.
- 아직 아무도 열지 않은 창이면 주소를 새로 발급합니다(
window_url_reissued: true). 앞서 받은 주소는 더 이상 열리지 않습니다. - 이미 열렸거나 만료·사용된 창이면
window_url이null입니다. 새 창이 필요하면 새 키로 다시 만드세요.
키 만드는 법
- 업무 한 건마다 한 번 만들고, 요청을 보내기 전에 저장합니다. 서버가 재시작돼도 같은 키로 재시도할 수 있어야 합니다.
- UUID 를 쓰거나, 주문번호처럼 업무를 가리키는 값에 난수를 붙입니다(예:
order-20261028-0042-9b1f…). - 재시도할 때 바뀌는 것은 서명·timestamp·nonce 뿐입니다. 멱등키와 본문은 그대로 둡니다.
- 서로 다른 업무에 같은 키를 쓰지 않습니다. 예를 들어 결제 1번 취소와 2번 취소는 각각 다른 키입니다(본문이 같아도 경로가 다르면 422 로 막힙니다).
재시도 예제
import time
import uuid
import requests
def charge_with_retry(order, call):
"""
call(method, path, payload, idempotency_key) 는 '인증 · 요청 서명' 문서의 함수입니다.
멱등키는 업무 한 건(이 주문의 결제)마다 한 번 만들고, 보내기 전에 저장합니다.
"""
if not order.idempotency_key:
order.idempotency_key = f"order-{order.id}-{uuid.uuid4()}"
order.save() # 재시작해도 같은 키를 쓰도록
for attempt in range(4):
try:
res = call("POST", "/payments", {"invoice_id": order.invoice_id},
idempotency_key=order.idempotency_key)
except requests.RequestException: # 응답을 못 받았다 — 같은 키로 다시
time.sleep(2 ** attempt)
continue
if res.status_code == 409: # 앞 요청이 아직 처리 중
time.sleep(int(res.headers.get("Retry-After", "2")))
continue
if res.status_code in (500, 502, 503, 504): # 서버 오류는 저장되지 않는다 — 같은 키로 다시
time.sleep(int(res.headers.get("Retry-After", str(2 ** attempt))))
continue
return res # 200·4xx 는 결과다. 같은 키로 다시 보내도 같은 응답
raise RuntimeError("결과를 확인하지 못했습니다. 결제 조회로 상태를 확인하세요.")결제 결과가 미확정(UNKNOWN)일 때
결제 승인 응답이 200 인데 status 가 UNKNOWN 이면, 빌링플러스도 결제사로부터 결과를 받지 못한 상태입니다. 실패가 아닙니다. 실제로는 승인됐을 수 있습니다.
- 새 멱등키로 다시 승인하지 마세요. 이중 결제가 될 수 있습니다.
- 빌링플러스가 결제사에 다시 물어 결과를 확정합니다. 확정되면
payment.approved또는payment.failed웹훅이 가고,GET /api/v1/payments/{id}로도 확인할 수 있습니다. - 그 사이 같은 청구서로 새 승인을 요청하면 처리하지 않습니다(결과를 기다리는 결제가 있다는 422).
어디에 연결되나
결제 승인(POST /api/v1/payments)과 수시결제 요청(POST /api/v1/charge-consents/{id}/charges)은 보낸 멱등키를 결제 기록에 이어 둡니다. 그래서 같은 키의 재시도는 결제사로 두 번 나가지 않습니다. 결과가 궁금하면 다시 승인하지 말고 조회 API 로 확인하세요.
개발자센터