Skip to content

Wave Money ​

Wave Money's payment gateway sends the customer to a Wave 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 Money has no status API in this package: the callback is the only payment result.

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

Responses shows what Wave Money 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 Money: payment request, authenticate, resultCustomerYour appWave MoneyCheck 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 Money: payment request, authenticate, result

Initiating a Payment ​

python
from django.shortcuts import get_object_or_404, redirect
from python_myanmar_payments import (
    WaveMoney,
    WaveMoneyConfig,
    WaveMoneyItem,
    WaveMoneyPaymentData,
)

from shop.models import Order

wave = WaveMoney(
    WaveMoneyConfig(
        merchant_id="...",
        secret_key="...",
        merchant_name="My Shop",
        time_to_live_seconds=300,
        timeout_seconds=30,
    )
)

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

    data = WaveMoneyPaymentData(
        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=[
            WaveMoneyItem(name="Product A", amount=6000),
            WaveMoneyItem(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)

WaveMoneyPaymentData ​

FieldTypeRequiredRules
order_idstrYesYour order ID. One order can have several payment attempts
callback_urlstrYesAbsolute http or https URL that Wave posts the result to. Wave may require HTTPS with a CA-issued certificate in production
return_urlstrYesAbsolute http or https URL Wave sends the customer back to. Not proof of payment
descriptionstrYesShown to the customer
itemsSequence[WaveMoneyItem]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. Unset or empty means a random ID

WaveMoneyItem(name, amount) has a name and an amount in whole kyat, greater than 0. The items are summed with exact int arithmetic, never floats; WaveMoney.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.

Responses ​

What Wave Money 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.

initiate() → RedirectPayment ​

FieldWave Money value
flowPaymentFlow.REDIRECT
order_idYour data.order_id
url{authenticate_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 Money 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
INSUFFICIENT_BALANCEPENDING
ACCOUNT_LOCKED, BILL_COLLECTION_FAILEDFAILED
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()InvalidPaymentDataErrorWaveMoney.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, or a hashed field holds an object or a list

AsyncWaveMoney 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). When Wave can't be reached or the request times out, initiate() raises ApiError with the original httpx error as __cause__.

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