Skip to content

KBZ Pay ​

KBZ Pay is KBZ Bank's mobile wallet: customers pay in the KBZ Pay PWA, by scanning a QR code, or from your mobile app.

CallWhat it doesReturns
kbz.pwa(data)Redirect to the KBZ Pay PWARedirectPayment
kbz.qr(data)Customer scans a QRQrPayment
kbz.app(data)Your mobile app opens the KBZ Pay SDKAppPayment
kbz.status(order_id)Query an orderPaymentStatusResult
kbz.handle_callback(request)Verify the notificationPaymentCallback

AsyncKbzPay has the same methods: await pwa(), qr(), app() and status(); handle_callback() stays a plain call.

Responses shows what KBZ Pay puts in each result.

How it works ​

Every KBZ Pay flow starts with the same precreate call and ends with KBZ's signed notification.

KBZ Pay: precreate, pay, notifyCustomerYour appKBZ PayPick the flowkbz.pwa() / qr() / app()Precreate the orderPWAAPP / PAY_BY_QRCODE / APPprepay_idplus qrCode for QRPWA URL, QR or signed orderurl / qr_string / order_info + signPay in the KBZ Pay appNotify callbackUrlsigned JSON under RequestVerified callback is proofkbz.handle_callback(request)Plain-text successwithin about 10 s, or KBZ retriesNo notify? Query the orderkbz.status(order_id)
KBZ Pay: precreate, pay, notify

Initiating a Payment ​

python
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from python_myanmar_payments import KbzPay, KbzPayConfig, KbzPayPaymentData

from shop.models import Order

kbz = KbzPay(
    KbzPayConfig(
        app_id="...",
        app_key="...",
        merchant_code="...",
        timeout_seconds=30,
    )
)


def kbz_data(order: Order) -> KbzPayPaymentData:
    return KbzPayPaymentData(
        order_id=f"ORDER_{order.id}",
        amount=10000,
        callback_url="https://shop.test/payments/kbz/callback",
    )


def kbz_pwa(request, order_id: int):
    data = kbz_data(get_object_or_404(Order, pk=order_id))

    # PWA: send the customer to the KBZ Pay PWA
    payment = kbz.pwa(data)
    return redirect(payment.url)


def kbz_qr(request, order_id: int):
    data = kbz_data(get_object_or_404(Order, pk=order_id))

    # QR: encode payment.qr_string into a QR image
    payment = kbz.qr(data)
    return render(request, "kbz_qr.html", {"payment": payment})


def kbz_app(request, order_id: int):
    data = kbz_data(get_object_or_404(Order, pk=order_id))

    # In-app: hand the signed values to your mobile app
    payment = kbz.app(data)
    return JsonResponse(payment.to_dict())

KbzPayPaymentData ​

FieldTypeRequiredRules
order_idstrYesUnique per order. Letters, digits and _ only, at most 40 characters
amountAmount | int | str | DecimalYesKyat, greater than 0, at most 2 decimal places, e.g. 10000 or Amount.parse("1000.50"). KBZ only accepts MMK
callback_urlstrYesPublic URL KBZ posts the result to. Absolute http or https URL, at most 512 characters, no query string
titlestrNoProduct name shown to the customer
timeout_minutesintNoAn integer from 1 to 120. Unset leaves it to KBZ (120)
callback_infostrNoFree text echoed back in the callback, at most 512 characters once URL-encoded

PWA Notes ​

  • The PWA only opens on a phone with the KBZ Pay app installed.
  • KBZ checks the redirect's Referer against the URL registered with them (error AOP08512). Redirect from that domain and don't strip the referrer.
  • After payment, KBZ sends the customer to the return URL registered with them during onboarding; it cannot be set per payment.

Handling Callbacks ​

KBZ Pay posts JSON nested under a Request key; build the CallbackRequest from the whole request.

python
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from python_myanmar_payments import CallbackRequest, SignatureVerificationError


# POST /payments/kbz/callback
@csrf_exempt
def kbz_callback(request):
    callback_request = CallbackRequest(
        body=request.body,
        headers=request.headers,
        query=request.META.get("QUERY_STRING", ""),
    )
    try:
        callback = kbz.handle_callback(callback_request)
    except SignatureVerificationError:
        return HttpResponse("invalid callback", status=400)

    if callback.is_successful():
        # callback.order_id is your merch_order_id
        # callback.gateway_reference is KBZ's mm_order_id
        ...

    ack = callback.acknowledgement  # plain-text "success"
    return HttpResponse(ack.body, status=ack.status, headers=ack.headers)

KBZ requires an HTTP 200 with the plain-text body success, answered within about 10 seconds. Otherwise it retries after 60 and 600 seconds; when no callback arrives, query the order.

Status Checks ​

python
result = kbz.status(f"ORDER_{order.id}")

if result.is_successful():
    # result.gateway_reference is KBZ's mm_order_id
    ...

status() takes your order_id. An order KBZ doesn't know raises ApiError.

Responses ​

What KBZ Pay puts in each field. See Results and PaymentCallback & Status for the full classes. A field the gateway didn't send is None. raw holds plain Python values; every JSON number is kept as its exact text in a str (1000.50 stays "1000.50"), never a float.

pwa() → RedirectPayment ​

FieldKBZ Pay value
flowPaymentFlow.REDIRECT
order_idYour data.order_id
url{pwa_url}?appid=…&merch_code=…&nonce_str=…&prepay_id=…&timestamp=…&sign=…
gateway_referenceKBZ prepay_id. Always set
rawThe precreate response: result, code, msg, merch_order_id, prepay_id, nonce_str, sign_type, sign

qr() → QrPayment ​

Field / MethodKBZ Pay value
flowPaymentFlow.QR
order_idYour data.order_id
qr_stringKBZ qrCode, a payload to encode into a QR image. Always set
qr_imageAlways None
expires_atNow + timeout_minutes, in UTC. None when timeout_minutes is unset (KBZ then allows 120 minutes)
referenceKBZ prepay_id. Always set
rawThe precreate response, as for pwa() plus qrCode
qr_image_data_uri()Always None, as qr_image is

app() → AppPayment ​

Field / MethodKBZ Pay value
flowPaymentFlow.APP
order_idYour data.order_id
order_infoappid=…&merch_code=…&nonce_str=…&prepay_id=…&timestamp=…
signSHA-256 signature of order_info, uppercase hex. See Signing
sign_typeSHA256
rawThe precreate response, as for pwa()
to_dict()orderId, orderInfo, sign and signType, without raw

status() → PaymentStatusResult ​

FieldKBZ Pay value
order_idKBZ merch_order_id, falling back to the order_id you passed. Always set
statustrade_status mapped, see Statuses
gateway_statusKBZ trade_status, trimmed, e.g. PAY_SUCCESS
gateway_referenceKBZ mm_order_id. None until KBZ has created the payment
amountKBZ total_amount, e.g. 10000
rawThe queryorder response: result, code, msg, merch_order_id, mm_order_id, total_amount, trans_currency, trade_status, trans_end_time, nonce_str, sign_type, sign

handle_callback() → PaymentCallback ​

Field / MethodKBZ Pay value
order_idKBZ merch_order_id (your order_id)
statustrade_status mapped, see Statuses
gateway_statusKBZ trade_status, trimmed, e.g. PAY_SUCCESS
gateway_referenceKBZ mm_order_id
amountKBZ total_amount, e.g. 10000
rawThe verified Request: appid, notify_time, merch_code, merch_order_id, mm_order_id, total_amount, trans_currency, trade_status, trans_end_time, callback_info, nonce_str, sign_type, sign
acknowledgementHTTP 200, body success, Content-Type: text/plain

Statuses ​

KBZ trade_statusPaymentStatus
PAY_SUCCESSSUCCESSFUL
WAIT_PAY, PAYINGPENDING
PAY_FAILEDFAILED
ORDER_CLOSEDCANCELED
ORDER_EXPIREDEXPIRED
anything elseUNKNOWN

Signing ​

KBZ signs requests, the in-app order_info and notifications the same way: every non-empty single-value field except sign and sign_type, sorted by key, joined as raw key=value pairs, with &key=<app key> appended, hashed with SHA-256 and uppercased. The package signs every request and verifies every notification for you; kbz.signer (a KbzPaySigner, also exported from python_myanmar_payments) exposes the same signature for custom calls: sign_string(fields), sign(fields) and verify(fields).

Errors ​

CallRaisesWhen
pwa(), qr(), app()InvalidPaymentDataErrorKbzPay.validate(data) fails. Nothing is sent
pwa(), qr(), app()ApiErrorKBZ answers with an HTTP error, result other than SUCCESS or code other than 0, or without a prepay_id
qr()ApiErrorKBZ returns no qrCode
status()ApiErrorKBZ answers with an HTTP error, result other than SUCCESS or code other than 0, e.g. for an unknown order
handle_callback()SignatureVerificationErrorsign doesn't match, or a field holds an object or a list

AsyncKbzPay raises the same errors. ApiError carries KBZ's code (e.g. ORDER_ID_USED, AOP08508) in gateway_code and its msg in gateway_message. When KBZ can't be reached or the request times out, the calls raise ApiError with the original httpx error as __cause__.

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