Python SDK
The Python SDK is the official client for the QairoPay API. Zero mandatory runtime dependencies beyond httpx, fully typed (PEP 561), runs sync or async on Python 3.9+. The same package covers sandbox and live environments — the key prefix picks the right base URL automatically.
Install
pip install qairopayuv add qairopaypipenv install qairopaypoetry add qairopayInitialize
The SDK reads QAIROPAY_API_KEY from the environment automatically. Keys starting with qp_sk_sandbox_ target the sandbox base URL; qp_sk_live_ targets production.
export QAIROPAY_API_KEY="qp_sk_sandbox_..."from qairopay import QairoPay
qp = QairoPay() # reads QAIROPAY_API_KEY from the environmentPass the key explicitly if you use a different variable name or read it from a secrets manager:
import os
from qairopay import QairoPay
qp = QairoPay(api_key=os.environ["QAIROPAY_KEY"])All configuration options:
import httpximport os
from qairopay import QairoPay
qp = QairoPay( api_key=os.environ["QAIROPAY_API_KEY"], base_url="https://api.sandbox.qairopay.com", # override if needed timeout=httpx.Timeout(30.0, connect=5.0), # default 30 s total max_retries=3, # 0 disables retries default_headers={"X-App-Version": "1.0.0"},)Method reference
| Resource | Methods |
|---|---|
passes | create, list, retrieve |
pass_templates | create, list, retrieve |
webhook_endpoints | create, list, retrieve, update, delete, rotate_secret |
cardholders | create, list, retrieve |
cards | create, list, retrieve |
disputes | list, retrieve |
settlements | list, retrieve |
me | retrieve |
Webhook endpoints
Create an endpoint
import os
from qairopay import QairoPay
qp = QairoPay(api_key=os.environ["QAIROPAY_API_KEY"])
endpoint = qp.webhook_endpoints.create( url="https://hooks.example.com/qairopay", enabled_events=["pass.installed", "pass.revoked", "payment.authorized"],)print(endpoint["id"], endpoint["signing_secret"]) # save the secret — shown onceRotate a signing secret
The old secret remains valid for 10 minutes after rotation.
result = qp.webhook_endpoints.rotate_secret(endpoint_id="whe_01HABC")print(result["signing_secret"]) # update QAIROPAY_WEBHOOK_SECRET nowIssuing a pass
A template defines the visual and behavioral mold; a pass is an instance of that template issued to a specific cardholder.
import os
from qairopay import QairoPay
qp = QairoPay(api_key=os.environ["QAIROPAY_API_KEY"])
# 1. Create a pass template (SDK auto-generates X-Idempotency-Key on every write)template = qp.pass_templates.create( name="Loyalty Gold", kind="loyalty", brand={ "background_color": "#1F7A5A", "foreground_color": "#FCFAF6", }, fields=[{"key": "tier", "label": "Tier", "value": "Gold"}],)template_id: str = template["id"] # "tpl_..."
# 2. Issue a pass to an existing cardholderpass_ = qp.passes.create( template_id=template_id, holder_id="holder_01HZXABC", # UUID of an existing Cardholder resource)download = pass_.get("download") or {}print(download.get("apple_url")) # open on iOS to add to Apple Walletprint(download.get("google_url")) # open on Android to add to Google WalletPagination
qp.passes.list() returns a SyncPage. Use auto_paging_iter() to walk all pages automatically, or step through pages manually.
# auto_paging_iter() yields every item across all pagesfor pass_ in qp.passes.list(status="installed").auto_paging_iter(): print(pass_["id"])Manual page-by-page control — useful for checkpointing or backpressure:
page = qp.passes.list(status="installed")while True: for item in page.items: print(item["id"]) if not page.has_more: break page = page.next_page()Error handling
Every HTTP error maps to a typed exception subclassing QairoPayError. Catch the narrowest exception that applies; QairoPayError catches any API error you don’t handle specifically.
import time
from qairopay import ( QairoPay, AuthenticationError, NotFoundError, RateLimitError, UnprocessableEntityError, QairoPayError,)
qp = QairoPay(api_key="qp_sk_sandbox_example")
try: pass_ = qp.passes.create( template_id="tpl_01HZXABC", holder_id="holder_01HZXABC", )except AuthenticationError: # 401 — invalid or expired API key raise SystemExit("Check your QAIROPAY_API_KEY")except NotFoundError as err: # 404 — template or holder does not exist print(f"Resource not found: {err.message}")except RateLimitError as err: # 429 — back off then retry wait_ms = err.retry_after_ms or 1000 time.sleep(wait_ms / 1000)except UnprocessableEntityError as err: # 422 — field-level validation failure for field, messages in err.field_errors.items(): print(f"{field}: {', '.join(messages)}")except QairoPayError as err: # Catch-all — also handles new error types added in future API releases print(f"[{err.status_code}] {err.message} (request_id={err.request_id})")Exception hierarchy at a glance:
| Class | HTTP | Notes |
|---|---|---|
AuthenticationError | 401 | Invalid or missing API key |
PermissionDeniedError | 403 | Key lacks permission |
InsufficientScopeError | 403 | Missing RBAC scope; .required_scope |
NotFoundError | 404 | Resource does not exist |
ConflictError | 409 | Entity conflict |
IdempotencyConflictError | 409 | Key reused with different body |
UnprocessableEntityError | 422 | Field validation failed; .field_errors |
CardDeclinedError | 402 | Card declined; .decline_reason |
RateLimitError | 429 | Too many requests; .retry_after_ms |
InternalServerError | 500 | Server error |
ServiceUnavailableError | 503 | Service temporarily down |
GatewayTimeoutError | 504 | Upstream timeout |
WebhookSignatureError | — | Local; signature mismatch; .reason |
Webhook verification
Always verify the QairoPay-Signature header before trusting webhook payload contents.
import os
from qairopay import webhooks, WebhookSignatureError
def handle_webhook(raw_body: bytes, sig_header: str) -> tuple[int, str]: try: event = webhooks.construct_event( raw_body, sig_header, os.environ["QAIROPAY_WEBHOOK_SECRET"], tolerance=300, # reject events older than 5 minutes ) except WebhookSignatureError as err: # err.reason: "invalid_signature" | "timestamp_out_of_tolerance" | "malformed_header" return 400, f"Signature verification failed: {err.reason}"
if event.type == "pass.installed": pass_data = event.data print(f"Pass installed: {pass_data.get('id')}") elif event.type == "pass.revoked": pass_data = event.data print(f"Pass revoked: {pass_data.get('id')}")
return 200, "ok"construct_event uses hmac.compare_digest internally — constant-time comparison, timing-attack safe by default. No need to reimplement it.
Secret rotation — pass a list and the SDK tries each secret in order:
import os
from qairopay import webhooks
event = webhooks.construct_event( raw_body, sig_header, [os.environ["QAIROPAY_WEBHOOK_SECRET_OLD"], os.environ["QAIROPAY_WEBHOOK_SECRET"]],)Retries and timeouts
The SDK retries on 5xx, 429, and connection errors using exponential backoff with full jitter (base 500 ms, max 8 s) up to max_retries attempts (default 3). The same X-Idempotency-Key is reused on every retry so writes cannot duplicate.
import httpx
from qairopay import QairoPay
# Disable retries entirelyqp_no_retry = QairoPay(api_key="qp_sk_sandbox_example", max_retries=0)
# Fine-grained timeout: 60 s total, 10 s to establish the connectionqp_slow = QairoPay( api_key="qp_sk_sandbox_example", timeout=httpx.Timeout(60.0, connect=10.0),)Type checking
The SDK ships a py.typed marker (PEP 561). Both mypy strict and pyright are verified in CI.
mypy --strict your_integration.pyRecommended pyproject.toml config:
[tool.mypy]strict = trueFor pyright, add to pyrightconfig.json:
{ "typeCheckingMode": "basic" }Async usage
AsyncQairoPay mirrors the sync client exactly, with await on every method. async_auto_paging_iter() replaces auto_paging_iter() for list endpoints.
import asyncioimport os
from qairopay import AsyncQairoPay
async def main() -> None: qp = AsyncQairoPay(api_key=os.environ["QAIROPAY_API_KEY"])
me = await qp.me.retrieve() print(me["tenant"]["name"])
template = await qp.pass_templates.create( name="Async Pass", kind="loyalty", ) print(template["id"])
# passes.list() is a coroutine — await it, then iterate the page page = await qp.passes.list(status="installed") async for pass_ in page.async_auto_paging_iter(): print(pass_["id"])
asyncio.run(main())AsyncQairoPay uses httpx.AsyncClient internally and is compatible with FastAPI, Starlette, Tornado, and any asyncio-based framework.
Source and issues
The SDK is open source on GitHub at qairopay/qairopay-python. Bug reports and pull requests are welcome. Security issues should go to security@qairopay.com, not the public tracker.