This release is a pre-release and may not be stable for production use.
FastAPI Myanmar Payments
Optional FastAPI integration for python-myanmar-payments: a lifespan that owns one payments facade, dependencies for its gateways, raw callback requests, verification that answers bad signatures with a 400, and the gateway's acknowledgement as a response.
You don't need this package to use FastAPI. The core SDK already has async clients (
AsyncKbzPay,AsyncWavePay, ...) that work in any FastAPI endpoint. This adapter only removes the framework glue. Payments, signing, callback verification, amounts and status mapping all stay inpython-myanmar-payments; see its documentation for each gateway.
Installation
Requires Python 3.10+, FastAPI 0.115+ and python-myanmar-payments 4.0.0a16 or later (below 5).
pip install fastapi-myanmar-payments
uv add fastapi-myanmar-payments
The version range names a pre-release, so pip and uv install the core's pre-releases without extra flags.
Pre-release core.
python-myanmar-payments4.0.0 is still in alpha, and its pre-releases may contain breaking changes (4.0.0a16, for example, renamed Wave Money to Wave Pay). This adapter is tested against the oldest supported core (4.0.0a16) and the newest published one. Pin both packages in your application's lock file and upgrade them together.
Quick start
from fastapi import FastAPI, Response
from python_myanmar_payments import AsyncMyanmarPayments
from fastapi_myanmar_payments import (
CallbackRequestDep,
KbzPayDep,
acknowledgement_response,
payments_lifespan,
verify_callback,
)
app = FastAPI(lifespan=payments_lifespan(AsyncMyanmarPayments.from_env))
@app.post("/payments/kbz/callback")
async def kbz_callback(kbz: KbzPayDep, request: CallbackRequestDep) -> Response:
callback = verify_callback(kbz, request) # 400 on a bad signature
... # validate and persist it: see "Secure webhooks"
return acknowledgement_response(callback.acknowledgement)
Configuration
The adapter adds no settings. Configure the facade exactly as the core SDK documents (Configuration):
AsyncMyanmarPayments.from_envreadsKBZ_PAY_*,WAVE_PAY_*,AYA_PAY_*,YOMA_MMQR_*,CYBER_SOURCE_*andMYANMAR_PAYMENTS_HTTP_TIMEOUTwhen each gateway is first used.- Or pass a factory that builds it from your own settings:
from python_myanmar_payments import AsyncMyanmarPayments
def build_payments() -> AsyncMyanmarPayments:
return AsyncMyanmarPayments(
kbz_pay={"app_id": settings.kbz_app_id, ...},
wave_pay={"merchant_id": settings.wave_merchant_id, ...},
)
app = FastAPI(lifespan=payments_lifespan(build_payments))
Only the gateways you use need to be configured. Using one that is not raises the core ConfigurationError (gateway, key), which FastAPI answers with a 500.
Lifespan
payments_lifespan(...) stores one AsyncMyanmarPayments on app.state for the life of the application, so every request reuses the same gateways, HTTP connections and Yoma MMQR token cache. Nothing is created per request.
Who closes what:
| You pass | At shutdown |
|---|---|
A factory, e.g. AsyncMyanmarPayments.from_env |
The lifespan calls aclose(), which closes only the HTTP clients the gateways created. |
An instance, e.g. payments_lifespan(my_payments) |
Nothing is closed; it is yours. |
An http_client or token_cache inside the facade |
Never closed by the adapter or the core SDK; close it yourself. |
Combine it with your own lifespan:
from contextlib import asynccontextmanager
payments = payments_lifespan(AsyncMyanmarPayments.from_env)
@asynccontextmanager
async def lifespan(app: FastAPI):
async with payments(app):
# your own startup
yield
# your own shutdown
app = FastAPI(lifespan=lifespan)
Share your own httpx.AsyncClient (proxies, tracing, limits):
import httpx
@asynccontextmanager
async def lifespan(app: FastAPI):
async with httpx.AsyncClient(timeout=30) as http_client: # you close it
factory = lambda: AsyncMyanmarPayments.from_env(http_client=http_client)
async with payments_lifespan(factory)(app):
yield
Dependency injection
| Dependency | Annotated alias | Gives |
|---|---|---|
get_payments |
PaymentsDep |
AsyncMyanmarPayments |
get_kbz_pay |
KbzPayDep |
AsyncKbzPay |
get_wave_pay |
WavePayDep |
AsyncWavePay |
get_aya_pay |
AyaPayDep |
AsyncAyaPay |
get_yoma_mmqr |
YomaMmqrDep |
AsyncYomaMmqr |
get_cyber_source |
CyberSourceDep |
CyberSource |
callback_request |
CallbackRequestDep |
the core CallbackRequest (raw body, headers and query string) |
Every gateway dependency reads the facade through get_payments, so overriding get_payments replaces them all. Without a lifespan (and without an override), get_payments raises a RuntimeError that says so.
Async payment initiation
from fastapi import HTTPException
from fastapi.responses import RedirectResponse
from python_myanmar_payments import Amount, ApiError, InvalidPaymentDataError, KbzPayPaymentData
from fastapi_myanmar_payments import KbzPayDep
@app.post("/orders/{order_id}/pay")
async def pay(order_id: str, kbz: KbzPayDep) -> RedirectResponse:
order = await orders.get(order_id) # your storage
try:
payment = await kbz.pwa(
KbzPayPaymentData(
order_id=order.id,
amount=Amount.parse(order.amount),
callback_url="https://shop.example/payments/kbz/callback",
)
)
except InvalidPaymentDataError as error:
raise HTTPException(422, detail=dict(error.errors)) from error
except ApiError as error:
raise HTTPException(502, detail="The payment gateway is unavailable.") from error
return RedirectResponse(payment.url, status_code=303)
Don't retry a failed initiation automatically: the gateway may have created the payment before the error reached you. Check kbz.status(order_id) (KBZ Pay, AYA Pay; Yoma MMQR by QR reference) or let the customer try again with a new attempt where the gateway requires one (Wave Pay's merchant_reference_id).
AyaPay.initiate() and CyberSource.initiate() make no network call and return a FormPayment; return HTMLResponse(payment.to_html()).
Secure webhooks
Use this order for every gateway callback: verify → validate against the stored order → persist → acknowledge → process once.
from fastapi import Response
from python_myanmar_payments import Amount
from fastapi_myanmar_payments import (
CallbackRequestDep,
WavePayDep,
acknowledgement_response,
verify_callback,
)
@app.post("/payments/wave/callback")
async def wave_callback(wave: WavePayDep, request: CallbackRequestDep) -> Response:
# 1. Verify. A bad signature or a malformed signed callback becomes a 400;
# nothing from an unverified payload is trusted, stored or echoed.
callback = verify_callback(wave, request)
# 2. Validate against your stored order, never against the callback alone.
# An unknown order is still recorded (step 3), never created or marked paid.
order = await orders.get(callback.order_id)
# Not every gateway sends an amount (Yoma MMQR doesn't): compare it when present.
amount_matches = order is not None and (
callback.amount is None or Amount.parse(order.amount).equals(callback.amount)
)
# 3. Persist durably and idempotently, e.g. a unique key on
# (gateway, order_id, gateway_status, gateway_reference). A duplicate
# delivery hits the key and is simply acknowledged again. If this raises,
# the request fails with a 5xx and nothing is acknowledged.
await webhook_events.insert_once(
gateway="wave_pay",
order_id=callback.order_id,
order_known=order is not None,
status=callback.status,
gateway_status=callback.gateway_status,
gateway_reference=callback.gateway_reference,
amount_matches=amount_matches,
raw=dict(callback.raw),
)
# 4. Acknowledge only after step 3 has committed, with the gateway's exact response.
return acknowledgement_response(callback.acknowledgement)
# 5. Process once, outside the request: a worker claims each stored event
# (e.g. UPDATE ... SET processed_at = now() WHERE id = ? AND processed_at IS NULL),
# fulfills the order only when it is known, PaymentStatus.SUCCESSFUL and
# amount_matches, and flags unknown orders and mismatches for review.
Failure and retry behavior:
- Verification fails → 400. The gateway may retry; a forged or corrupted callback keeps failing. Log
error.__cause__(aSignatureVerificationError) if you need it; its message never contains your secrets, but itsrawpayload is unverified. - Unknown order (a verified callback for an order you don't have, e.g. from another environment or a deleted order) → record it durably as an unknown-order event, then return the gateway's acknowledgement so it stops retrying. Never create the order or mark anything paid from it; review it by hand.
- Persisting fails (database down) → let the exception produce a 5xx. Don't return the acknowledgement: the gateway retries later, so nothing is lost.
- Persisted, but processing fails later → the event stays unprocessed in your store; retry it from your worker. The gateway already has its acknowledgement and must not be relied on to resend.
- Duplicates are normal: gateways retry until they receive the acknowledgement. The idempotent insert and the once-only claim make them harmless.
- FastAPI
BackgroundTasksrun in the same process after the response; they are lost on a crash or deploy. Use them only for work you can redo from the persisted event. - Never fulfill from a browser return or redirect; it is not proof of payment. For AYA Pay's signed return,
aya.verify_redirect(request)shows the right page; fulfill from the callback orstatus(). - Yoma MMQR also checks the
X-Webhook-Secretheader, whichCallbackRequestDeppasses through unchanged.
callback_request hands the core SDK the raw body bytes, the headers and the raw query string: it never decodes and re-encodes JSON or forms, so numbers like 1000.50 and form encoding reach the signature check exactly as sent. Don't read the body with await request.json() or await request.form() and rebuild it.
Testing
No credentials or network are needed. Run the app in-process with httpx.ASGITransport and its own lifespan, and give the facade an http_client that answers from httpx.MockTransport. (FastAPI's TestClient works too, though recent Starlette versions ask for httpx2 with it.)
import httpx
import pytest
from python_myanmar_payments import AsyncMyanmarPayments
from fastapi_myanmar_payments import get_payments
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"message": "success", "transaction_id": "T1"})
@pytest.mark.anyio
async def test_pay() -> None:
fake = AsyncMyanmarPayments(
wave_pay={...}, # test credentials
http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
)
app.dependency_overrides[get_payments] = lambda: fake
async with (
app.router.lifespan_context(app),
httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client,
):
response = await client.post("/orders/100/pay")
assert response.status_code == 303
app.dependency_overrides.clear()
- Override one gateway with
app.dependency_overrides[get_kbz_pay] = .... - Test your fulfillment code with a
PaymentCallback(order_id=..., status=PaymentStatus.SUCCESSFUL, gateway_status=...)you build yourself. - Callbacks you post through
TestClientare still signature-checked; sign them with your test secret (see the core SDK's Testing guide).
What this package does not do
No webhook router, no storage, no background processing, no retries and no automatic fulfillment: those decisions stay in your application. Request body size limits belong to your server or proxy.
License
MIT. See LICENSE.
Metadata
Release files for fastapi-myanmar-payments 0.1.0.dev0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| fastapi_myanmar_payments-0.1.0.dev0.tar.gz | 10.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fastapi_myanmar_payments-0.1.0.dev0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 22.3 kB
Release files / fastapi_myanmar_payments-0.1.0.dev0.tar.gz
| Download URL | fastapi_myanmar_payments-0.1.0.dev0.tar.gz |
|---|---|
| Size | 10.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c5efefc497fd616739c6060ed2d29281e5e85963ae92f1361f327e7b3f424c00
|
|
BLAKE2b-256 checksum How to use checksums |
485e1ac32ca650abd924e26b21a9de85d9a11b23d0e99a960c790832396c4407
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logRelease files / fastapi_myanmar_payments-0.1.0.dev0-py3-none-any.whl
| Download URL | fastapi_myanmar_payments-0.1.0.dev0-py3-none-any.whl |
|---|---|
| Size | 11.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
90481bb7c785fd56633ade90c26df280b5ac6bc9d46a27400b14488ca304aa56
|
|
BLAKE2b-256 checksum How to use checksums |
cdf30a8c9ae7c8cccb5ba8a5c93281ec8a81ea75944eab21de590054a2616369
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logSecurity for fastapi-myanmar-payments 0.1.0.dev0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for fastapi_myanmar_payments-0.1.0.dev0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.