Payment Flows
Starting a payment always follows the same pattern: build the gateway's payment data, call the gateway, then act on the typed result. Each flow has its own result type with exactly the fields that flow needs, and every result implements myanmarpayments.PaymentResult, whose Flow() returns FlowRedirect, FlowForm, FlowQR or FlowApp, so a type switch tells them apart.
| Result | What you do | Returned by |
|---|---|---|
*RedirectPayment | Redirect the customer to payment.URL | kbz.PWA, wave.Initiate |
*FormPayment | Write payment.HTML() | aya.Initiate, cs.Initiate |
*QrPayment | Show the QR to the customer | kbz.QR, yoma.Initiate, yoma.RenewQR |
*AppPayment | Return the signed payload to your mobile app | kbz.App |
Every method validates the payment data first and returns *myanmarpayments.InvalidPaymentDataError before any request is sent. Call data.Validate() yourself to check a request earlier, e.g. while handling a form. The customer finishing on the gateway's side is never proof of payment: fulfill orders from the verified callback or a status check.
The samples on this page and the gateway pages are net/http handlers; Framework Integration shows chi, Gin and Echo.
Redirect Payments
Here is the flow with the KBZ Pay PWA; Wave Money works the same way with its own payment page.
The gateway hosts its own payment page. Send the customer there.
func checkout(w http.ResponseWriter, r *http.Request) {
payment, err := kbz.PWA(r.Context(), kbzpay.PaymentData{
OrderID: "ORDER_1",
Amount: myanmarpayments.Kyat(10000),
CallbackURL: "https://shop.test/payments/kbz/callback",
})
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
http.Redirect(w, r, payment.URL, http.StatusFound)
}payment.GatewayReference holds the gateway's ID for the attempt (KBZ prepay_id, Wave transaction_id).
Form Payments
Here is the flow with AYA Pay; CyberSource works the same way with its hosted checkout.
The gateway expects the customer's browser to POST a signed form. HTML() returns a complete page that submits the form as soon as it loads, with every value escaped:
func ayaCheckout(w http.ResponseWriter, r *http.Request) {
// no network call, so no context
payment, err := aya.Initiate(data)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
io.WriteString(w, payment.HTML())
}To build the form yourself, use Action, Fields (an ordered []FormField) and Enctype with html/template, which escapes every value:
var form = template.Must(template.New("form").Parse(`
<form id="payment-form" method="POST"
action="{{.Action}}" enctype="{{.Enctype}}">
{{range .Fields}}
<input type="hidden" name="{{.Name}}" value="{{.Value}}">
{{end}}
</form>`))
form.Execute(w, payment)Post the fields unchanged: they are signed. payment.Field(name) looks up one value and payment.Values() returns them as a map[string]string. AYA expects multipart/form-data, which Enctype carries.
QR Payments
Here is the flow with Yoma MMQR, whose QR expires after 120 seconds; a KBZ Pay QR follows the same steps without renewals.
Gateways return QR codes in two shapes:
| Field | Gateway | Use it as |
|---|---|---|
QRString | KBZ Pay | A payload: encode it into a QR image with any QR library, e.g. skip2/go-qrcode |
QRImage | Yoma MMQR | A base64 image: display it as is, e.g. with QRImageDataURI("") |
func yomaCheckout(w http.ResponseWriter, r *http.Request) {
payment, err := yoma.Initiate(r.Context(), data)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
page.Execute(w, payment)
}<img src="{{.QRImageDataURI ""}}" alt="Scan to pay">
<p>Payable until {{.ExpiresAt.Format "15:04:05"}}</p>ExpiresAt is a time.Time when the gateway limits how long the QR is payable (the zero time otherwise), and Reference holds the ID used for status checks (Yoma refLabel, KBZ prepay_id).
App Payments
Here is the flow with the KBZ Pay mobile SDK.
The KBZ Pay mobile SDK needs a signed order string. AppPayment encodes to JSON with the SDK's names, so return it to your app as is; the app passes the values to KBZPay.startPay():
func kbzAppCheckout(w http.ResponseWriter, r *http.Request) {
payment, err := kbz.App(r.Context(), data)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
// {"orderId", "orderInfo", "sign", "signType"}
json.NewEncoder(w).Encode(payment)
}The SDK's own result only means the payment screen closed; rely on the callback or kbz.Status.
Handling Any Result
package shop
import (
"encoding/json"
"io"
"net/http"
myanmarpayments "github.com/laranex/go-myanmar-payments/v4"
)
func respond(
w http.ResponseWriter,
r *http.Request,
payment myanmarpayments.PaymentResult,
) {
switch payment := payment.(type) {
case *myanmarpayments.RedirectPayment:
http.Redirect(w, r, payment.URL, http.StatusFound)
case *myanmarpayments.FormPayment:
w.Header().Set("Content-Type", "text/html; charset=utf-8")
io.WriteString(w, payment.HTML())
case *myanmarpayments.QrPayment:
qr := payment.QRImageDataURI("")
if qr == "" {
qr = payment.QRString
}
io.WriteString(w, qr)
case *myanmarpayments.AppPayment:
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(payment)
}
}See Results for every field.