Skip to content

Payment Flows ​

Starting a payment always follows the same pattern: build the gateway's payment data, call the gateway, then act on the typed result. Each flow has its own result class with exactly the fields that flow needs, and every result has a flow class attribute (PaymentFlow.REDIRECT, FORM, QR or APP), so a match statement or isinstance check tells them apart.

ResultWhat you doReturned by
RedirectPaymentRedirect the customer to payment.urlkbz.pwa, wave.initiate
FormPaymentReturn payment.to_html()aya.initiate, cs.initiate
QrPaymentShow the QR to the customerkbz.qr, yoma.initiate, yoma.renew_qr
AppPaymentReturn the signed payload to your mobile appkbz.app

Every method validates the payment data first and raises InvalidPaymentDataError before any request is sent. Call the gateway's validate() yourself (e.g. KbzPay.validate(data)) to check a request earlier, e.g. while handling a form. The customer finishing on the gateway's side is never proof of payment: fulfill orders from the verified callback or a status check.

The samples on this page and the gateway pages are Django views using the sync classes; Framework Integration shows Flask and FastAPI, and the async classes take the same calls with await.

Redirect Payments ​

Here is the flow with the KBZ Pay PWA; Wave Money works the same way with its own payment page.

Redirect payment with the KBZ Pay PWACustomerYour appKBZ PayCheck outStart the paymentkbz.pwa(data)Create the orderprecreate, trade_type PWAAPPprepay_idRedirect to the PWA302 to payment.urlPay in the KBZ Pay appPayment notificationPOST to callback_urlVerified callback is proofkbz.handle_callback(request)
Redirect payment with the KBZ Pay PWA

The gateway hosts its own payment page. Send the customer there.

python
from django.shortcuts import redirect
from python_myanmar_payments import Amount, KbzPay, KbzPayPaymentData

kbz = KbzPay.from_env()


def checkout(request):
    data = KbzPayPaymentData(
        order_id="ORDER_1",
        amount=Amount.kyat(10000),
        callback_url="https://shop.test/payments/kbz/callback",
    )
    payment = kbz.pwa(data)
    return redirect(payment.url)

payment.gateway_reference holds the gateway's ID for the attempt (KBZ prepay_id, Wave transaction_id).

Form Payments ​

Here is the flow with AYA Pay; CyberSource works the same way with its hosted checkout.

Form payment with AYA PayCustomerYour appAYA PayCheck outSign the form, no API callaya.initiate(data)Auto-submitting form pageHttpResponse(payment.to_html())Post the signed formPOST /v1/payment/requestBack to your return URLnot proof of paymentBackend callbackpayload + checkSumVerified callback is proofaya.handle_callback(request)
Form payment with AYA Pay

The gateway expects the customer's browser to POST a signed form. to_html() returns a complete page that submits the form as soon as it loads, with every value escaped:

python
from django.http import HttpResponse


def aya_checkout(request):
    # no network call: never awaited, on AsyncAyaPay too
    payment = aya.initiate(data)
    return HttpResponse(payment.to_html())

To build the form yourself, use action, fields (a tuple of FormField(name, value)) and enctype with your template engine, which escapes every value:

html
<form id="payment-form" method="POST"
      action="{{ payment.action }}" enctype="{{ payment.enctype }}">
  {% for field in payment.fields %}
    <input type="hidden" name="{{ field.name }}" value="{{ field.value }}">
  {% endfor %}
</form>

Post the fields unchanged: they are signed. payment.field(name) looks up one value and payment.values() returns them as a dict. AYA expects multipart/form-data, which enctype carries.

QR Payments ​

Here is the flow with Yoma MMQR, whose QR expires after 120 seconds; a KBZ Pay QR follows the same steps without renewals.

QR payment with Yoma MMQRCustomerYour appYoma MMQRCheck outCheck out the orderyoma.initiate(data)Generate the first QRqr/generateQR image and refLabelpayable for 120 secondsShow the QRqr_image, a base64 PNGExpired? Renew the QRyoma.renew_qr(order_id)Scan with an MMQR walletPayment callbackorderNumber, status, hashValueVerified callback is proofyoma.handle_callback(request)
QR payment with Yoma MMQR

Gateways return QR codes in two shapes:

FieldGatewayUse it as
qr_stringKBZ PayA payload: encode it into a QR image with any QR library, e.g. qrcode or segno
qr_imageYoma MMQRA base64 image: display it as is, e.g. with qr_image_data_uri()
python
from django.shortcuts import render


def yoma_checkout(request):
    payment = yoma.initiate(data)
    return render(request, "pay.html", {"payment": payment})
html
<img src="{{ payment.qr_image_data_uri }}" alt="Scan to pay">
<p>Payable until {{ payment.expires_at|time:"H:i:s" }}</p>

expires_at is an aware UTC datetime when the gateway limits how long the QR is payable (None otherwise), and reference holds the ID used for status checks (Yoma refLabel, KBZ prepay_id).

App Payments ​

Here is the flow with the KBZ Pay mobile SDK.

In-app payment with the KBZ Pay SDKCustomerYour appKBZ PayPay in your mobile appStart the paymentkbz.app(data)Create the orderprecreate, trade_type APPprepay_idorderInfo, sign, signTypeJsonResponse(payment.to_dict())KBZPay.startPay()the customer pays in KBZ PayPayment screen closednot proof of paymentPayment notificationPOST to callback_urlVerified callback is proofkbz.handle_callback(request)
In-app payment with the KBZ Pay SDK

The KBZ Pay mobile SDK needs a signed order string. AppPayment.to_dict() returns the values with the SDK's names, so return it to your app as JSON; the app passes the values to KBZPay.startPay():

python
from django.http import JsonResponse


def kbz_app_checkout(request):
    payment = kbz.app(data)
    # {"orderId", "orderInfo", "sign", "signType"}
    return JsonResponse(payment.to_dict())

The SDK's own result only means the payment screen closed; rely on the callback or kbz.status().

Handling Any Result ​

python
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect
from python_myanmar_payments import (
    AppPayment,
    FormPayment,
    PaymentResult,
    QrPayment,
    RedirectPayment,
)


def respond(payment: PaymentResult) -> HttpResponse:
    match payment:
        case RedirectPayment():
            return redirect(payment.url)
        case FormPayment():
            return HttpResponse(payment.to_html())
        case QrPayment():
            qr = payment.qr_image_data_uri() or payment.qr_string
            return HttpResponse(qr)
        case AppPayment():
            return JsonResponse(payment.to_dict())

See Results for every field.

Released under the MIT License, except where a package says otherwise.