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(ctx, data)Redirect to Wave's payment page*RedirectPayment
wave.HandleCallback(request)Verify the callback*PaymentCallback

wave is the *wavepay.Gateway that paymentsfacades.MyanmarPayments().WavePay() returns. Wave Pay has no status API: the callback is the only payment 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(ctx, data)transaction_idRedirect to authenticate/authenticate?transaction_id=…Pay with WavePayBack to the frontend URLReturnURL: not proof of paymentBackend result URL callbackPOST to CallbackURL, hashValueVerified callback is proofwave.HandleCallback(request)
Wave Pay: payment request, authenticate, result

Initiating a Payment ​

go
import (
	"fmt"

	"github.com/goravel/framework/contracts/http"
	myanmarpayments "github.com/laranex/go-myanmar-payments/v4"
	"github.com/laranex/go-myanmar-payments/v4/wavepay"
	paymentsfacades "github.com/laranex/goravel-myanmar-payments/v4/facades"
)

func (r *CheckoutController) Wave(ctx http.Context) http.Response {
	order := findOrder(ctx) // your own order lookup

	wave, err := paymentsfacades.MyanmarPayments().WavePay()
	if err != nil {
		return ctx.Response().String(http.StatusInternalServerError, "%s", err)
	}
	data := &wavepay.PaymentData{
		OrderID:     fmt.Sprintf("ORDER_%d", order.ID),
		CallbackURL: "https://shop.test/payments/wave/callback",
		ReturnURL:   fmt.Sprintf("https://shop.test/orders/%d", order.ID),
		Description: fmt.Sprintf("Order #%d", order.ID),
		Items: []wavepay.Item{
			{Name: "Product A", Amount: myanmarpayments.Kyat(6000)},
			{Name: "Product B", Amount: myanmarpayments.Kyat(4000)},
		},
	}

	payment, err := wave.Initiate(ctx, data)
	if err != nil {
		return ctx.Response().String(http.StatusBadGateway, "%s", err)
	}

	order.WaveReference = data.MerchantReferenceID
	saveOrder(order) // your own persistence

	return ctx.Response().Redirect(http.StatusFound, payment.URL)
}

Initiate() posts JSON, in MMK, to {base_url}/payment and sends the customer to {base_redirect_url}/authenticate?transaction_id=….

wavepay.PaymentData ​

FieldTypeRequiredRules
OrderIDstringYesYour order ID, at most 100 characters: letters, digits, _ and - only. One order can have several payment attempts
CallbackURLstringYesAbsolute http or https URL that Wave posts the result to, at most 255 characters. Wave may require HTTPS with a CA-issued certificate in production
ReturnURLstringYesAbsolute http or https URL Wave sends the customer back to, at most 255 characters. Not proof of payment
DescriptionstringYesShown to the customer, at most 255 characters
Items[]wavepay.ItemYesAt least one item
Amountmyanmarpayments.AmountNoWhole kyat, greater than 0 (Wave doesn't accept decimals). Unset charges the sum of the items. Wave only accepts MMK
MerchantReferenceIDstringNoUnique ID of this attempt, at most 100 characters: letters, digits, _ and - only. Empty means a random ID
MerchantNamestringNoBusiness name shown on Wave's page for this payment. Empty uses the configured merchant_name (WAVE_PAY_MERCHANT_NAME); a name made only of whitespace is rejected
TimeToLiveSeconds*intNoSeconds the customer has to pay this payment, greater than 0, e.g. seconds := 600 then TimeToLiveSeconds: &seconds. nil uses the configured time_to_live_in_seconds (WAVE_PAY_TIME_TO_LIVE_IN_SECONDS); 0 is rejected
AdditionalField3, AdditionalField4, AdditionalField5stringNoExtra information, at most 50 characters each, sent as additional_field_3–5 and echoed in the callback's additionalField3–5. Empty is not sent

Length limits count UTF-8 bytes. wavepay.Item takes a Name and an Amount (myanmarpayments.Amount) in whole kyat, greater than 0. The items are summed with exact integer arithmetic, never floats. Initiate() takes a pointer to the data, so it can fill in MerchantReferenceID.

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. Read it from data.MerchantReferenceID.

Handling Callbacks ​

go
import (
	"github.com/goravel/framework/contracts/http"
	payments "github.com/laranex/goravel-myanmar-payments/v4"
	paymentsfacades "github.com/laranex/goravel-myanmar-payments/v4/facades"

	"yourapp/app/facades"
)

facades.Route().Post("/payments/wave/callback", func(
	ctx http.Context,
) http.Response {
	request, err := payments.CallbackRequestFromContext(ctx)
	if err != nil {
		return ctx.Response().String(http.StatusBadRequest, "bad request")
	}
	wave, err := paymentsfacades.MyanmarPayments().WavePay()
	if err != nil {
		return ctx.Response().String(http.StatusInternalServerError, "%s", err)
	}
	callback, err := wave.HandleCallback(request)
	if err != nil { // *myanmarpayments.SignatureVerificationError
		return ctx.Response().String(http.StatusBadRequest, "invalid")
	}

	if callback.IsSuccessful() {
		// callback.OrderID is your OrderID
		// callback.Raw["merchantReferenceId"] is the attempt's reference
		// callback.GatewayReference is Wave's transactionId
	}

	return payments.Acknowledge(ctx, callback)
})

callback.OrderID 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 HandleCallback() returns a *myanmarpayments.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 types.

Initiate() → *RedirectPayment ​

FieldWave Pay value
OrderIDYour OrderID
URL{base_redirect_url}/authenticate?transaction_id=… (URL-encoded), e.g. https://payments.wavemoney.io/authenticate?transaction_id=…
GatewayReferenceWave transaction_id. Always set
RawWave's /payment response: message (success), transaction_id

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

HandleCallback() → *PaymentCallback ​

FieldWave Pay value
OrderIDWave orderId, falling back to merchantReferenceId when it is missing, null or empty
Statusstatus mapped, see Statuses
GatewayStatusWave status, trimmed, e.g. PAYMENT_CONFIRMED
GatewayReferenceWave 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_CONFIRMEDStatusSuccessful
BILL_COLLECTION_FAILED, INSUFFICIENT_BALANCE, ACCOUNT_LOCKEDStatusFailed
PAYMENT_REQUEST_CANCELLEDStatusCanceled
TRANSACTION_TIMED_OUT, SCHEDULER_TRANSACTION_TIMED_OUTStatusExpired
anything elseStatusUnknown

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

Errors ​

CallReturnsWhen
Initiate()*InvalidPaymentDataErrorA value breaks the rules above. Nothing is sent
Initiate()*APIErrorWave answers with an HTTP error, a message other than success, or no transaction_id
HandleCallback()*SignatureVerificationErrorhashValue doesn't match, a hashed field holds an object or array, amount isn't a decimal, both orderId and merchantReferenceId are empty, or one of them holds anything but letters, digits, _ and -

HTTPStatus tells Wave's rejections apart: 400 invalid hash, 404 unknown merchant, 409 reused reference, 422 validation (GatewayCode is VALIDATION_ERROR).