Skip to content

Yoma MMQR ​

Yoma MMQR is Yoma Bank's MMQR gateway: it issues ready-made QR images that customers scan with any MMQR wallet.

CallWhat it doesReturns
yoma.Initiate(ctx, data)Check out the order and generate its first QR*QrPayment
yoma.RenewQR(ctx, orderID)Generate a new QR for a checked-out order*QrPayment
yoma.Status(ctx, reference)Check a QR's payment status*PaymentStatusResult
yoma.HandleCallback(request)Verify the callback*PaymentCallback

Responses shows what Yoma MMQR puts in each result.

How it works ​

Yoma checks each order out once, then issues QR codes that each stay payable for 120 seconds.

Yoma MMQR: token, checkout, QR, renewal, resultCustomerYour appYoma MMQRGet a token unless cachedPOST /token, then cachedCheck out the orderyoma.Initiate(ctx, data)Generate the QRqr/generate: QR + refLabelShow the QR for 120 spayment.QRImageDataURI("")Expired? Renew the QRyoma.RenewQR(ctx, orderID)Scan with an MMQR walletPayment callbackorderNumber, status, hashValueVerified callback is proofyoma.HandleCallback(request)No callback? Check statusyoma.Status(ctx, reference)
Yoma MMQR: token, checkout, QR, renewal, result

Initiating a Payment ​

go
import (
	"fmt"

	myanmarpayments "github.com/laranex/go-myanmar-payments/v4"
	"github.com/laranex/go-myanmar-payments/v4/yomammqr"
)

yoma, err := yomammqr.New(
	yomammqr.Config{
		MerchantID:     "...",
		ClientID:       "...",
		ClientSecret:   "...",
		WebhookHashKey: "...",
		APIVersion:     "v1rc",
		TimeoutSeconds: 30,
	},
	nil, // default HTTP client
	nil, // in-memory token cache; share this gateway across requests
)
if err != nil {
	return err
}

data := yomammqr.PaymentData{
	OrderID:     fmt.Sprintf("ORDER_%d", order.ID),
	Amount:      myanmarpayments.Kyat(10000),
	Description: fmt.Sprintf("Order #%d", order.ID),
}

payment, err := yoma.Initiate(ctx, data)
if err != nil {
	return err
}

// Store payment.Reference with the order for status checks.

src := payment.QRImageDataURI("")
fmt.Fprintf(w, `<img src="%s" alt="Scan with any MMQR wallet">`, src)

yomammqr.PaymentData ​

FieldTypeRequiredRules
OrderIDstringYesUnique order number, at most 20 characters
Amountmyanmarpayments.AmountYesWhole kyat, greater than 0, e.g. Kyat(10000). Yoma documents no decimals or currency
DescriptionstringYesAt most 50 characters

QR Image ​

QRImage is an already rendered base64 PNG of the payment slip: display it as is, no QR library needed.

Handling Callbacks ​

go
import (
	"net/http"

	myanmarpayments "github.com/laranex/go-myanmar-payments/v4"
)

// POST /payments/yoma/callback
request, err := myanmarpayments.NewCallbackRequestFromHTTP(r)
if err != nil {
	http.Error(w, "invalid callback", http.StatusBadRequest)
	return
}
callback, err := yoma.HandleCallback(request)
if err != nil {
	http.Error(w, "invalid callback", http.StatusBadRequest)
	return
}

if callback.IsSuccessful() {
	// callback.OrderID is your orderNumber
}

callback.Acknowledgement.Write(w)

The callback URL is registered with Yoma, not sent per order. When WebhookSecret is configured, callbacks must carry it in the X-Webhook-Secret header. The hash is checked with HMAC-SHA256 keyed with your order number plus WebhookHashKey.

QR Lifetime and Renewal ​

A QR is payable for 120 seconds (yomammqr.QRLifetime); payment.ExpiresAt tells you when. Yoma accepts each order number once, so never call Initiate again for the same order. Renew the QR instead:

go
import "fmt"

payment, err := yoma.RenewQR(ctx, fmt.Sprintf("ORDER_%d", order.ID))
if err != nil {
	return err
}

// Store the new payment.Reference: the previous one stops working.

Each renewal retires the previous Reference; only the newest one answers status checks.

Status Checks ​

go
// reference is the payment.Reference you stored
result, err := yoma.Status(ctx, reference)
if err != nil {
	return err
}

if result.IsSuccessful() {
	// the QR was paid
}

Status takes the QR's Reference, not your OrderID. An expired QR returns StatusExpired instead of an error.

Responses ​

What Yoma MMQR puts in each field. See Results and PaymentCallback & Status for the full structs. On error the result is nil. A field the gateway didn't send is "". Raw holds plain Go values (JSON numbers become json.Numbers).

Initiate() → *myanmarpayments.QrPayment ​

Field / MethodYoma MMQR value
Flow()FlowQR
OrderIDYour data.OrderID (Yoma orderNumber)
QRStringAlways ""
QRImageYoma qrString, a base64 PNG of the payment slip. Always set
ExpiresAtNow + 120 seconds (yomammqr.QRLifetime). Always set
ReferenceYoma refLabel, e.g. 100000083331. Pass it to Status. Always set
RawThe qr/generate response: refLabel, qrString, errorCode (nil), errorDescription
QRImageDataURI("")data:image/png;base64,…

Initiate checks the order out (payment/checkout), then generates its first QR; the result comes from the generate call.

RenewQR() → *myanmarpayments.QrPayment ​

The same values as Initiate() for the orderID you passed, with a new QRImage, Reference and ExpiresAt. The previous Reference stops answering status checks.

Status() → *myanmarpayments.PaymentStatusResult ​

FieldYoma MMQR value
OrderIDAlways "": Yoma only returns the reference
StatuspaymentStatus mapped case-insensitively, see Statuses. StatusExpired for a QR EXPIRED error
GatewayStatusYoma paymentStatus, trimmed, e.g. SUCCESS. QR EXPIRED for an expired QR
GatewayReferenceYoma refLabel, falling back to the reference you passed. Always set
AmountAlways "": Yoma's status response has no amount
RawThe payment/check-status response: refLabel, paymentStatus, errorCode, errorDescription

HandleCallback() → *myanmarpayments.PaymentCallback ​

FieldYoma MMQR value
OrderIDYoma orderNumber (your OrderID)
Statusstatus mapped case-insensitively, see Statuses
GatewayStatusYoma status, trimmed, as sent, e.g. success
GatewayReferenceAlways "": Yoma's callback has no reference
AmountAlways "": Yoma's callback has no amount
RawThe verified body: orderNumber, status, hashValue
AcknowledgementHTTP 200, empty body, Content-Type: text/plain

Statuses ​

Yoma valuePaymentStatus
success (callback), SUCCESS (status)StatusSuccessful
PENDINGStatusPending
fail (callback), FAILED (status)StatusFailed
QR EXPIRED errorStatusExpired
anything elseStatusUnknown

Access Tokens ​

Yoma authenticates with an OAuth token that lasts hours. The gateway keeps it in the token cache, shares one token request between concurrent calls, and fetches a new token, retrying once, when Yoma answers 401. The shared token request runs without any one call's cancellation and is bounded by the HTTP client timeout, so canceling one call's ctx never fails the others; that call stops waiting and returns an *APIError. yoma.ForgetToken() drops the cached token, e.g. after rotating the client secret.

The token is cached under myanmar-payments.yoma-mmqr.token.<sha256(baseURL|clientID)>, the same key every Laranex SDK uses, so services in different languages can share one cache, for Yoma's expires_in minus 60 seconds (at least 60 seconds). expires_in is read from its leading digits, so 28800.0 is 28800 seconds; a missing or non-positive value means 3600.

Errors ​

CallReturnsWhen
Initiate()*InvalidPaymentDataErrordata.Validate() fails. Nothing is sent
Initiate()*APIErrorThe token request fails, Yoma answers with an HTTP error or an errorCode (e.g. PAYMENT ALREADY EXISTS), checkOutStatus isn't true, or there is no qrString or refLabel
RenewQR()*APIErrorAs Initiate(), without the checkout
Status()*APIErrorThe token request fails, or Yoma answers with an HTTP error or any errorCode other than QR EXPIRED
HandleCallback()*SignatureVerificationErrorX-Webhook-Secret is missing or wrong (when WebhookSecret is set), orderNumber is missing, status holds an object or array, or hashValue doesn't match

The error types live in the root myanmarpayments package. Yoma reports business errors with HTTP 200 and an errorCode; *APIError carries it in GatewayCode and Yoma's errorDescription in GatewayMessage. When Yoma can't be reached or ctx is canceled, the calls return *APIError, which unwraps to the cause (errors.Is(err, context.DeadlineExceeded)).

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