Skip to content

Wave Pay ​

Wave Pay (Pay with Wave API v1.3) sends the customer to Wave's payment page to pay with their WavePay wallet.

CallWhat it doesReturns
wave.initiate(data)Redirect to Wave's payment pageRedirectPayment
wave.handle_callback(request)Verify the callbackPaymentCallback

Wave Pay has no status API in this package: the callback is the only payment result.

AsyncWavePay has the same methods: await initiate(); handle_callback() stays a plain call.

Responses shows what Wave Pay puts in each result.

How it works ​

Wave sends the customer back to your return URL and posts the result to your callback URL separately.

Wave Pay: payment request, authenticate, resultCustomerYour appWave PayCheck outPayment request with hashwave.initiate(data)transaction_idRedirect to authenticate/authenticate?transaction_id=…Pay with WavePayBack to the frontend URLreturn_url: not proof of paymentBackend result URL callbackPOST to callback_url, hashValueVerified callback is proofwave.handle_callback(request)
Wave Pay: payment request, authenticate, result

Initiating a Payment ​

python
from django.shortcuts import get_object_or_404, redirect
from python_myanmar_payments import (
    WavePay,
    WavePayConfig,
    WavePayItem,
    WavePayPaymentData,
)

from shop.models import Order

wave = WavePay(
    WavePayConfig(
        merchant_id="...",
        secret_key="...",
        merchant_name="My Shop",
        base_url=WavePayConfig.PRODUCTION_URL,
        base_redirect_url=WavePayConfig.PRODUCTION_URL,
        time_to_live_seconds=300,
    )
)

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

    data = WavePayPaymentData(
        order_id=f"ORDER_{order.id}",
        callback_url="https://shop.test/payments/wave/callback",
        return_url=f"https://shop.test/orders/{order.id}",
        description=f"Order #{order.id}",
        items=[
            WavePayItem(name="Product A", amount=6000),
            WavePayItem(name="Product B", amount=4000),
        ],
    )

    payment = wave.initiate(data)

    # Store data.merchant_reference_id with the order: initiate() filled it in.

    return redirect(payment.url)

initiate() posts JSON, in MMK, to {base_url}/payment and redirects to {base_redirect_url}/authenticate?transaction_id=…; see Configuration for both URLs.

WavePayPaymentData ​

FieldTypeRequiredRules
order_idstrYesYour order ID, at most 100 characters: letters, digits, _ and - only. One order can have several payment attempts
callback_urlstrYesAbsolute http or https URL that Wave posts the result to, at most 255 characters
return_urlstrYesAbsolute http or https URL Wave sends the customer back to, at most 255 characters. Not proof of payment
descriptionstrYesShown to the customer, at most 255 characters
itemsSequence[WavePayItem]YesAt least one item, as a list or tuple
amountAmount | int | str | DecimalNoWhole kyat, greater than 0 (Wave doesn't accept decimals). Unset charges the sum of the items. Wave only accepts MMK
merchant_reference_idstrNoUnique ID of this attempt, at most 100 characters: letters, digits, _ and - only. Unset or empty means a random ID
merchant_namestrNoBusiness name shown on Wave's page for this payment. Unset uses WavePayConfig's merchant_name (also in the request hash); a blank one is rejected
time_to_live_secondsintNoSeconds the customer has to pay this payment, an int greater than 0. Unset uses WavePayConfig's time_to_live_seconds (also in the request hash)
additional_field_3, additional_field_4, additional_field_5strNoExtra information, at most 50 characters each, sent as additional_field_3–5 and echoed in the callback's additionalField3–5. Unset, None or empty is not sent

Length limits count UTF-8 bytes. WavePayItem(name, amount) has a name and an amount in whole kyat, greater than 0. The items are summed with exact int arithmetic, never floats; WavePay.resolved_amount(data) returns the total that will be charged as an Amount (None when an item amount is invalid).

Merchant Reference ID ​

Wave rejects a reused merchant_reference_id (409 Record already exists), so every attempt, including a retry of the same order, needs a new one. Leave it empty to get a fresh random ID, and store it: Wave marks orderId as optional in callbacks, while merchantReferenceId is always present. initiate() writes the generated ID to data.merchant_reference_id once data passes validation, so keep a reference to the object you pass.

Handling Callbacks ​

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

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

    if callback.is_successful():
        # callback.order_id is your order_id
        # callback.raw["merchantReferenceId"] is the attempt's reference
        # callback.gateway_reference is Wave's transactionId
        ...

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

callback.order_id falls back to merchantReferenceId when Wave's orderId is missing, null or empty.

Wave hashes the values joined with no separator, so a matching hashValue alone doesn't pin down where one value ends and the next begins. After the hash matches, amount must be a decimal (e.g. 10000 or 10000.00), at least one of orderId and merchantReferenceId must be present, and each one sent may only hold letters, digits, _ and -; otherwise handle_callback() raises SignatureVerificationError. New payments are held to the same ID rules, so every callback for a payment made with this package passes them.

Responses ​

What Wave Pay puts in each field. See Results and PaymentCallback & Status for the full classes.

initiate() → RedirectPayment ​

FieldWave Pay value
flowPaymentFlow.REDIRECT
order_idYour data.order_id
url{base_redirect_url}/authenticate?transaction_id=… (URL-encoded), e.g. https://payments.wavemoney.io/authenticate?transaction_id=…
gateway_referenceWave transaction_id. Always set
rawWave's /payment response: message (success), transaction_id

The attempt's merchant_reference_id is not on the result: read it from data.merchant_reference_id.

handle_callback() → PaymentCallback ​

Field / MethodWave Pay value
order_idWave orderId, falling back to merchantReferenceId when it is missing, null or empty
statusstatus mapped, see Statuses
gateway_statusWave status, trimmed, e.g. PAYMENT_CONFIRMED
gateway_referenceWave transactionId
amountWave amount, e.g. 10000
rawThe verified body: status, merchantId, orderId, merchantReferenceId, frontendResultUrl, backendResultUrl, initiatorMsisdn, amount, timeToLiveSeconds, paymentDescription, currency, additionalField1–5, transactionId, paymentRequestId, requestTime, hashValue
acknowledgementHTTP 200, empty body, Content-Type: text/plain

Statuses ​

Only PAYMENT_CONFIRMED means the customer paid.

Wave statusPaymentStatus
PAYMENT_CONFIRMEDSUCCESSFUL
BILL_COLLECTION_FAILED, INSUFFICIENT_BALANCE, ACCOUNT_LOCKEDFAILED
PAYMENT_REQUEST_CANCELLEDCANCELED
TRANSACTION_TIMED_OUT, SCHEDULER_TRANSACTION_TIMED_OUTEXPIRED
anything elseUNKNOWN

SCHEDULER_TRANSACTION_TIMED_OUT arrives up to 15 minutes after the time-to-live ends.

Errors ​

CallRaisesWhen
initiate()InvalidPaymentDataErrorWavePay.validate(data) fails. Nothing is sent and data is left untouched. Item errors use items.0.amount keys
initiate()ApiErrorWave answers with an HTTP error, a message other than success, or no transaction_id
handle_callback()SignatureVerificationErrorhashValue doesn't match, a hashed field holds an object or a list, amount isn't a decimal, both orderId and merchantReferenceId are empty, or one of them holds anything but letters, digits, _ and -

AsyncWavePay raises the same errors. http_status tells Wave's rejections apart: 400 invalid hash, 404 unknown merchant, 409 reused reference, 422 validation (gateway_code is VALIDATION_ERROR).