Skip to content

AYA Pay ​

The AYA Payment Gateway is one hosted checkout for AYA Pay, other wallets (KBZ Pay, WavePay, UAB Pay, CB Pay…) and cards (VISA, Mastercard, JCB).

CallWhat it doesReturns
aya.services()List the channels enabled for your accountlist[AyaPayService]
aya.initiate(data)Signed form posted to AYAFormPayment
aya.status(order_id)Enquire an orderPaymentStatusResult
aya.handle_callback(request)Verify the backend callbackPaymentCallback
aya.verify_redirect(request)Verify the customer's returnPaymentCallback

AsyncAyaPay has the same methods: await services() and status(); initiate(), handle_callback() and verify_redirect() stay plain calls.

Responses shows what AYA Pay puts in each result.

How it works ​

AYA posts the result to your callback URL and also signs the query string it adds when sending the customer back.

AYA Pay: channels, signed form, callback, returnCustomerYour appAYA PayList enabled channelsaya.services()Channels and methodse.g. aya_pay: QR, NOTIPick a channel and methodaya_pay + AyaPayMethod.QRSigned form, auto-submitsaya.initiate(data)Post the form and payPOST /v1/payment/requestBackend callbackpayload + checkSumVerified callback is proofaya.handle_callback(request)Back on your return pagepayload + checkSum in the queryShow the right messageaya.verify_redirect(request)
AYA Pay: channels, signed form, callback, return

Channels and Methods ​

channel is a lowercase key such as aya_pay, kbz_pay or visa; method is how the customer pays through it. Which ones you have depends on your merchant account, so list them:

python
from python_myanmar_payments import AyaPay, AyaPayConfig, AyaPayMethod

aya = AyaPay(
    AyaPayConfig(
        app_key="...",
        app_secret="...",
        timeout_seconds=30,
    )
)

for service in aya.services():
    # service.name: "AYA Pay"
    # service.key: "aya_pay", pass it as channel
    # service.image_url: the channel's logo
    # service.methods: (AyaPayMethod.QR, AyaPayMethod.NOTI)
    if service.supports(AyaPayMethod.QR):
        ...  # offer the QR method

Methods AYA lists that this package doesn't know yet are kept in service.unknown_methods.

AyaPayMethodValueCustomer
AyaPayMethod.WEBWEBPays on a hosted web page (cards)
AyaPayMethod.QRQRScans a QR with the wallet app
AyaPayMethod.NOTINOTIApproves a push notification in the wallet app

Initiating a Payment ​

python
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from python_myanmar_payments import AyaPayMethod, AyaPayPaymentData

from shop.models import Order


def aya_checkout(request, order_id: int):
    order = get_object_or_404(Order, pk=order_id)

    data = AyaPayPaymentData(
        order_id=f"ORDER_{order.id}",
        amount=10000,
        channel="aya_pay",
        method=AyaPayMethod.QR,
        return_url="https://shop.test/payments/aya/return",
        description=f"Order #{order.id}",
    )

    payment = aya.initiate(data)

    # The page posts the signed form to AYA on load.
    return HttpResponse(payment.to_html())

AyaPayPaymentData ​

FieldTypeRequiredRules
order_idstrYesUnique, 6 to 40 characters (merchOrderId)
amountAmount | int | str | DecimalYesWhole kyat, greater than 0, e.g. 10000 or Amount.kyat(10000). AYA documents no decimals and only accepts MMK (104)
channelstrYesA key from services()
methodAyaPayMethodYesAyaPayMethod.WEB, .QR or .NOTI (or the strings "WEB", "QR", "NOTI")
return_urlstrNoAbsolute http or https URL. Unset uses the URL registered with AYA
descriptionstrNoShown to the customer
user_refsSequence[str]NoUp to 5 of your own values, as a list or tuple, echoed back in the callback

Form Encoding ​

AYA expects the form as multipart/form-data. payment.enctype carries it; use it if you render the form yourself.

Handling Callbacks ​

AYA posts to the callback URL registered with them.

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


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

    if callback.is_successful():
        # callback.order_id is your merchOrderId
        # callback.gateway_reference is AYA's tranId
        ...

    ack = callback.acknowledgement
    return HttpResponse(ack.body, status=ack.status, headers=ack.headers)

AYA signs only the fields present in its payload (wallet payments leave out the card fields); the package verifies them in the order the specification lists.

The Return Page ​

AYA signs the query string it adds when sending the customer back, so the return page can show the right message:

python
from django.http import HttpResponse
from python_myanmar_payments import CallbackRequest, SignatureVerificationError


# GET /payments/aya/return
def aya_return(request):
    callback_request = CallbackRequest(
        body=request.body,
        headers=request.headers,
        query=request.META.get("QUERY_STRING", ""),
    )
    try:
        result = aya.verify_redirect(callback_request)
    except SignatureVerificationError:
        return HttpResponse("invalid return", status=400)

    if result.is_successful():
        return HttpResponse("Thank you, your payment was received.")
    return HttpResponse(f"Payment {result.status}.")

A + in the base64 payload that reached you as a space (an unencoded query string) is read back as + before decoding; the checksum is still verified. The payload may carry its = padding or leave it out; partial padding, the URL-safe alphabet, line breaks and text that isn't UTF-8 are rejected. Still fulfill orders from the backend callback.

Status Checks ​

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

if result.is_successful():
    # result.gateway_reference is AYA's tranId
    ...

status() takes your order_id. An order AYA doesn't know raises ApiError (20 Transaction not found).

Responses ​

What AYA 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.

services() → list[AyaPayService] ​

AyaPayService is AYA-only, so it is listed in full here. It is a frozen dataclass.

Field / MethodAYA Pay value
nameAYA name, e.g. AYA Pay. Falls back to key
keyAYA key, e.g. aya_pay, kbz_pay, visa. Pass it as channel. Always set
image_urlAYA image_url, the channel's logo. None when AYA sends none
methodsA tuple[AyaPayMethod, ...] of the methods this package knows, e.g. (AyaPayMethod.QR, AyaPayMethod.NOTI)
unknown_methodsA tuple[str, ...] of methods AYA listed that this package doesn't know yet. Usually ()
supports(method)Whether methods contains method (an AyaPayMethod or its string)

Entries AYA sends without a key are skipped.

initiate() → FormPayment ​

Field / MethodAYA Pay value
flowPaymentFlow.FORM
order_idYour data.order_id
action{base_url}/v1/payment/request, e.g. https://pgw.ayainnovation.com/v1/payment/request
fieldsThe signed fields below, in signing order. Post them unchanged
enctypemultipart/form-data
to_html()A full HTML page that posts fields to action on load
Form fieldValue
merchOrderIddata.order_id
amountdata.amount, e.g. 10000
appKeyconfig.app_key
timestampUnix time in seconds
userRef1 … userRef5data.user_refs, "" when unused
descriptiondata.description, "" when unset
currencyCode104 (MMK)
channeldata.channel, e.g. aya_pay
methoddata.method, e.g. QR
overrideFrontendRedirectUrldata.return_url, "" when unset
checkSumHMAC-SHA256 of the values above joined with :

initiate() makes no HTTP call, so it is never awaited, on AsyncAyaPay too. FormPayment has no raw: nothing is sent to AYA until the customer's browser posts the form.

status() → PaymentStatusResult ​

FieldAYA Pay value
order_idAYA merchOrderId, falling back to the order_id you passed. Always set
statusstatusCode mapped, see Statuses
gateway_statusAYA statusCode, trimmed, e.g. 00
gateway_referenceAYA tranId
amountAYA amount, e.g. 10000
rawThe verified, decoded enquiry payload: merchOrderId, tranId, amount, currencyCode, statusCode, paymentCardNumber, paymentMobileNumber, cardTypeName, cardExpiryDate, nameOnCard, approvalCode, tranRef, userRef1–5, description, dateTime

AYA leaves out the fields that don't apply (wallet payments have no card fields), so raw only has the keys AYA sent. Some payloads spell currencyCode as currenyCode.

handle_callback() → PaymentCallback ​

Field / MethodAYA Pay value
order_idAYA merchOrderId (your order_id)
statusstatusCode mapped, see Statuses
gateway_statusAYA statusCode, trimmed, e.g. 00
gateway_referenceAYA tranId
amountAYA amount, e.g. 10000
rawThe verified, decoded payload, with the same keys as status()
acknowledgementHTTP 200, empty body, Content-Type: text/plain

verify_redirect() → PaymentCallback ​

The same values as handle_callback(), read from the signed payload and checkSum AYA adds to your return URL (query string first, then the body). There is nothing to acknowledge: render your own page.

Statuses ​

AYA statusCodePaymentStatus
00SUCCESSFUL
01PENDING
02 (fail), 03 (reject)FAILED
04EXPIRED
anything elseUNKNOWN

Errors ​

CallRaisesWhen
initiate()InvalidPaymentDataErrorAyaPay.validate(data) fails. Nothing is signed
services()ApiErrorAYA answers with an HTTP error or a status other than 00
status()ApiErrorAYA answers with an HTTP error or a status other than 00, e.g. 20 Transaction not found
status()SignatureVerificationErrorThe enquiry payload's checkSum doesn't match
handle_callback(), verify_redirect()SignatureVerificationErrorpayload is missing or not base64 JSON, a signed field holds an object or a list, or checkSum doesn't match

AsyncAyaPay raises the same errors. ApiError carries AYA's status (e.g. 20 Transaction not found, 09 Duplicate order ID) in gateway_code and its message in gateway_message. When AYA 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.