Python SDK
payri는 Python 3.10 이상을 지원하고 런타임 의존성은 httpx
동기(Payri)·비동기(AsyncPayri) 클라이언트를 같은 표면으로 제공해요.
pip install payri소스는 sdk-python 저장소
시작하기
서버에서 Payri 클라이언트에 secret 키를 넣어 사용해요.
클라이언트는 context manager라서 with 블록을 벗어나면 연결이 정리돼요.
import os
from payri import Payri
with Payri(api_key=os.environ["PAYRI_API_KEY"]) as client:
page = client.products.list(page=1, limit=20)
result = client.licenses.verify(key="XXXX-YYYY-ZZZZ")
# 체크아웃 세션 — 반환된 url로 구매자를 리다이렉트해요.
session = client.checkout.create(link_id="link_abc")
redirect_url = session["url"]API 키는 서버 전용 비밀키예요.
클라이언트 배포물·저장소에 포함하지 말고 환경변수나 시크릿 매니저로 관리하세요.
구매자 쪽에서 필요한 건 키를 받지 않는 PayriPublic뿐이에요.
비동기 환경에서는 AsyncPayri를 써요.
from payri import AsyncPayri
async with AsyncPayri(api_key=os.environ["PAYRI_API_KEY"]) as client:
page = await client.products.list(limit=10)with 없이 길게 쓰는 경우 close()(비동기는 aclose())로 직접 정리해요.
단, http_client 옵션으로 직접 주입한 httpx 클라이언트는 SDK가 닫지 않아요.
생성자 옵션
| 옵션 | 기본값 | 설명 |
|---|---|---|
api_key | (필수) | 서버 전용 secret 키 (key_live_* / key_test_*) |
base_url | https://api.payri.kr | API base URL. 스테이징 등 다른 환경을 겨눌 때만 변경해요 |
timeout | httpx 기본값 | 요청 타임아웃(초) |
http_client | 자동 생성 | 재사용할 httpx.Client/httpx.AsyncClient 직접 주입 |
리소스
메서드 인자는 keyword-only예요 (client.checkout.create(link_id=...)).
| 리소스 | 메서드 | REST 문서 |
|---|---|---|
client.products | list() · get(product_id) | Products |
client.orders | list() · get(order_id) · refund(order_id, ...) | Orders · Refunds |
client.subscriptions | list() · get(subscription_id) · cancel(subscription_id, reason=...) | Subscriptions |
client.checkout | create(link_id=..., ...) · get(session_id) | Checkout |
client.licenses | verify(key=...) · activate(key=...) · deactivate(key=..., activation_id=...) | Licenses |
키 없는 공개 클라이언트: PayriPublic
PayriPublic은 API 키를 받지 않는 공개 클라이언트예요.
구매자가 이름·이메일 본인확인으로 자기 주문의 라이선스를 조회하는 용도예요.
from payri import PayriPublic
license = PayriPublic().lookup_license(
"ord_123", name="홍길동", email="hong@example.com"
)
if license is None:
... # 주문이 없거나 이름·이메일 불일치 — 존재 여부는 알려주지 않아요비동기 버전 AsyncPayriPublic도 같은 표면으로 제공해요.
웹훅 검증
Webhooks.construct_event가 Standard Webhooks webhook-id·webhook-timestamp·webhook-signature 헤더)을 검증하고 { "type": ..., "data": ... } 이벤트를 반환해요.
검증에는 원본(raw) 본문이 필요해요 — JSON으로 파싱한 dict를 다시 직렬화해 넘기면 서명이 깨져요.
검증 실패 시 PayriWebhookError를 던지고, reason으로 사유를 확인할 수 있어요.
import os
from payri import Webhooks, PayriWebhookError
try:
event = Webhooks.construct_event(
payload=raw_body, # 원본 바디 (bytes 또는 str) 그대로
headers=headers, # 요청 헤더 매핑 그대로
secret=os.environ["PAYRI_WEBHOOK_SECRET"],
)
except PayriWebhookError as err:
... # err.reason 확인 후 400 응답
if event["type"] == "order.paid":
...같은 이벤트가 중복 도착할 수 있으니 webhook-id 헤더를 멱등 키로 쓰는 걸 권장해요. (재전송과 멱등성)
FastAPI
import os
from fastapi import FastAPI, Request, Response
from payri import Webhooks, PayriWebhookError
app = FastAPI()
@app.post("/webhooks/payri")
async def payri_webhook(request: Request):
payload = await request.body() # 원본 바디 — request.json() 금지
try:
event = Webhooks.construct_event(
payload=payload,
headers=request.headers,
secret=os.environ["PAYRI_WEBHOOK_SECRET"],
)
except PayriWebhookError:
return Response(status_code=400)
if event["type"] == "order.paid":
... # webhook-id로 멱등 확인 후 처리
return {"received": True}Flask
import os
from flask import Flask, request
from payri import Webhooks, PayriWebhookError
app = Flask(__name__)
@app.post("/webhooks/payri")
def payri_webhook():
try:
event = Webhooks.construct_event(
payload=request.get_data(), # 원본 바디 — request.json 금지
headers=request.headers,
secret=os.environ["PAYRI_WEBHOOK_SECRET"],
)
except PayriWebhookError:
return "", 400
if event["type"] == "order.paid":
... # webhook-id로 멱등 확인 후 처리
return "", 200Django
외부(페이리)에서 오는 요청이라 CSRF 토큰이 없으므로 해당 뷰만 csrf_exempt로 제외해요.
import os
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from payri import Webhooks, PayriWebhookError
@csrf_exempt
@require_POST
def payri_webhook(request):
try:
event = Webhooks.construct_event(
payload=request.body, # 원본 바디
headers=request.headers,
secret=os.environ["PAYRI_WEBHOOK_SECRET"],
)
except PayriWebhookError:
return HttpResponse(status=400)
if event["type"] == "order.paid":
... # webhook-id로 멱등 확인 후 처리
return JsonResponse({"received": True})이벤트 종류와 페이로드 형식은 이벤트 종류에서 확인해요.
에러 처리
모든 에러는 PayriError를 상속해요. (에러 계층)
JS SDK와 달리 API 에러 클래스 표기가 PayriAPIError(대문자 API)인 점에 주의하세요.
import os
from payri import Payri, PayriAPIError, PayriAuthError, PayriConnectionError
try:
with Payri(api_key=os.environ["PAYRI_API_KEY"]) as client:
order = client.orders.get("ord_123")
except PayriAuthError:
... # 401/403 — 키 누락·무효, 스코프 부족, IP 제한
except PayriAPIError as err:
... # 그 외 4xx/5xx — err.status, err.code, err.body 확인
except PayriConnectionError:
... # 네트워크 실패·타임아웃 — 응답을 받지 못했어요PayriAuthError는 PayriAPIError를 상속하므로 먼저 잡아야 해요.
에러 코드 목록은 에러 코드에서 확인해요.