Skip to content
Ask the docs

Find answers across the QairoPay docs.

Type a question and we'll synthesize an answer from the docs with citations back to the source pages.

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

Terminal window
pip install qairopay

Initialize

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.

Terminal window
export QAIROPAY_API_KEY="qp_sk_sandbox_..."
from qairopay import QairoPay
qp = QairoPay() # reads QAIROPAY_API_KEY from the environment

Pass 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 httpx
import 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

ResourceMethods
passescreate, list, retrieve
pass_templatescreate, list, retrieve
webhook_endpointscreate, list, retrieve, update, delete, rotate_secret
cardholderscreate, list, retrieve
cardscreate, list, retrieve
disputeslist, retrieve
settlementslist, retrieve
meretrieve

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 once

Rotate 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 now

Issuing 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 cardholder
pass_ = 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 Wallet
print(download.get("google_url")) # open on Android to add to Google Wallet

Pagination

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 pages
for 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:

ClassHTTPNotes
AuthenticationError401Invalid or missing API key
PermissionDeniedError403Key lacks permission
InsufficientScopeError403Missing RBAC scope; .required_scope
NotFoundError404Resource does not exist
ConflictError409Entity conflict
IdempotencyConflictError409Key reused with different body
UnprocessableEntityError422Field validation failed; .field_errors
CardDeclinedError402Card declined; .decline_reason
RateLimitError429Too many requests; .retry_after_ms
InternalServerError500Server error
ServiceUnavailableError503Service temporarily down
GatewayTimeoutError504Upstream timeout
WebhookSignatureErrorLocal; 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 entirely
qp_no_retry = QairoPay(api_key="qp_sk_sandbox_example", max_retries=0)
# Fine-grained timeout: 60 s total, 10 s to establish the connection
qp_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.

Terminal window
mypy --strict your_integration.py

Recommended pyproject.toml config:

[tool.mypy]
strict = true

For 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 asyncio
import 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 [email protected], not the public tracker.