Skip to content

Wave Pay Mini App ​

Wave Pay Mini App (Wave Money JS SDK v1.8) lets the customer pay inside the WavePay app: your web page runs in the app's webview and calls Wave's JavaScript SDK. Your NestJS app covers the three parts that need secrets; the page itself calls makePayment.

CallWhat it doesReturns
await wavePayMiniApp().accessToken()Get an access token for the page, cachedstring
wavePayMiniApp().payment(data)Sign the makePayment payload, no API callMiniAppPayment
@VerifiedCallback('wave-pay-mini-app')Verify the callback against the request URLPaymentCallback

Wave Pay Mini App has no status API: the callback is the only payment result. @InjectWavePayMiniApp() (token WAVE_PAY_MINI_APP) injects the gateway directly.

How it works ​

The page logs in with a token from your app, then pays with a payload your app signed. Wave posts the result to your callback URL.

Wave Pay Mini App: token, signed payment, resultCustomerYour pageYour appWave PayOpen your mini app in WavePayAsk for an access tokenToken unless cachedwavePayMiniApp().accessToken()access_tokenLog in to the SDKauthModule.loginWithAccessToken(token)Check outSigned payloadreturn wavePayMiniApp().payment(data)PaypaymentModule.makePayment(payload)Enter PINPayment resultnot proof of paymentCallbackPOST to callbackUrl, hashValueVerified callback is proof@VerifiedCallback, request URL
Wave Pay Mini App: token, signed payment, result

Configuration ​

VariableModule optionDescription
WAVE_PAY_MINI_APP_CLIENT_IDwavePayMiniApp.clientIdClient ID issued by Wave, for the token endpoint
WAVE_PAY_MINI_APP_CLIENT_SECRETwavePayMiniApp.clientSecretClient secret issued by Wave, for the token endpoint
WAVE_PAY_MINI_APP_MINI_APP_IDwavePayMiniApp.miniAppIdYour mini app ID (miniAppID)
WAVE_PAY_MINI_APP_SECRET_KEYwavePayMiniApp.secretKeyHash secret key for payment and callback hashes
WAVE_PAY_MINI_APP_BASE_URLwavePayMiniApp.baseUrlThe API base URL, e.g. WavePayMiniAppConfig.PRODUCTION_URL. A trailing / is dropped

Every setting is required; MYANMAR_PAYMENTS_HTTP_TIMEOUT (30 seconds by default) applies to the token request. See Configuration for the production and UAT base URLs.

Access Token ​

The page needs an access token for WaveJsSDK.authModule.loginWithAccessToken(token). Return it from a route of your own:

ts
@Get('mini-app/token')
async token(): Promise<{ accessToken: string }> {
  return { accessToken: await this.payments.wavePayMiniApp().accessToken() };
}

accessToken() posts client_id, client_secret and grant_type=client_credentials as a form to {baseUrl}/auth/v1/external-utility/token and returns access_token. The token is kept in the module's token cache (@nestjs/cache-manager when it is registered, else memory) under myanmar-payments.wave-pay-mini-app.token.<sha256 of baseUrl|clientId>, the same key in every Laranex package, for Wave's expires_in minus 60 seconds (at least 60 seconds; a missing or non-positive value means 3600). Concurrent calls share one token request. await wavePayMiniApp().forgetToken() drops it. Never request the token from the browser: it needs the client secret.

Starting a Payment ​

ts
import type {
  MiniAppPayment,
  WavePayMiniAppPaymentData,
} from '@laranex/myanmar-payments';
import { MyanmarPaymentsService } from '@laranex/nestjs-myanmar-payments';
import { Controller, Param, Post } from '@nestjs/common';

import { OrdersService } from '../orders/orders.service';

@Controller('mini-app')
export class MiniAppCheckoutController {
  constructor(
    private readonly payments: MyanmarPaymentsService,
    private readonly orders: OrdersService,
  ) {}

  @Post('orders/:id/pay')
  async pay(@Param('id') id: string): Promise<MiniAppPayment> {
    const order = await this.orders.findOrFail(id);

    const data: WavePayMiniAppPaymentData = {
      orderId: `ORDER_${order.id}`,
      callbackUrl: 'https://shop.test/payments/wave-pay-mini-app/callback',
      items: [
        { name: 'Ticket', amount: 3000 },
        { name: 'Snack', amount: 2000 },
      ],
    };

    const payment = this.payments.wavePayMiniApp().payment(data);

    await this.orders.update(order, {
      waveReference: data.merchantReferenceId,
    });

    // Serialized with toJSON(): the makePayment object
    return payment;
  }
}

payment() sends no request: it validates the data, signs the payload and returns it.

WavePayMiniAppPaymentData ​

FieldTypeRequiredRules
orderIdstringYesYour order ID (orderID)
callbackUrlstringYesAbsolute http or https URL that Wave posts the result to (callBackUrl). It is signed, so it must be the URL of your callback route: @VerifiedCallback verifies against the URL the callback arrives at
itemsWavePayMiniAppItem[]YesAt least one item
amountAmount | number | bigintNoWhole kyat, greater than 0 (Wave doesn't accept decimals). Unset or null charges the exact sum of the items
merchantReferenceIdstringNoYour ID for this attempt (merchantRefID). Unset or empty means a random 32-character hex ID, written back to data.merchantReferenceId

WavePayMiniAppItem has a name (required) and an amount in whole kyat, greater than 0. The items are summed with exact bigint arithmetic. Errors are collected in this order: orderId, callbackUrl, items, merchantReferenceId, each item's items.0.name and items.0.amount, then amount. Without items or amount, amount reports The amount field is required.; a negative amount reports The amount field must not be negative.; a decimal one reports Wave Pay Mini App does not accept decimal amounts; the amount field must be a whole number. Invalid data is left untouched.

The makePayment Payload ​

Returning the MiniAppPayment from a handler serializes it with toJSON(), exactly the object Wave's makePayment takes, in this order:

json
{
  "amount": 5000,
  "miniAppID": "MINI456",
  "merchantRefID": "3f9c0d6e5b2a41c8a7e1f0b9d4c3a2e1",
  "orderID": "ORDER_1",
  "callBackUrl": "https://shop.test/payments/wave-pay-mini-app/callback",
  "hashValue": "…",
  "items": [
    { "name": "Ticket", "amount": 3000 },
    { "name": "Snack", "amount": 2000 }
  ]
}

Amounts are JSON integers. hashValue is the lowercase hex HMAC-SHA256, keyed with the secret key, of amount, orderID, miniAppID, merchantRefID and callBackUrl joined with no separator, e.g. 5000ORDER123MINI456MERCHANT789https://yourdomain.com/callback. For an amount above Number.MAX_SAFE_INTEGER, send payment.toJsonString(), which writes every digit on every Node.js version.

On the Page ​

Load Wave's SDK as Wave's guide shows, then pass the payload from your server unchanged. Never compute the token or the hash in the browser.

js
const sdk = new WaveJsSDK({ appName: 'My Shop', appIconUrl: 'https://shop.test/icon.png' })
await sdk.init()

const { accessToken } = await (await fetch('/mini-app/token')).json()
await window.WaveJsSDK.authModule.loginWithAccessToken(accessToken)

const payload = await (await fetch('/mini-app/orders/1/pay', { method: 'POST' })).json()
const result = await window.WaveJsSDK.paymentModule.makePayment(payload)

makePayment's result only tells the page how the payment screen ended; fulfill from the verified callback.

Merchant Reference ID ​

Give every attempt, including a retry of the same order, a new merchantReferenceId. Leave it empty to get a fresh random ID, and store it after payment(): the callback's merchantRefId tells you which attempt was paid.

Handling Callbacks ​

The callback carries neither the mini app ID nor the callback URL. @VerifiedCallback('wave-pay-mini-app') and this.payments.handleCallback('wave-pay-mini-app', req) verify against the URL the callback was posted to, callbackUrlFrom(req); the mini app ID comes from the configuration.

ts
import { PaymentCallback } from '@laranex/myanmar-payments';
import {
  AcknowledgeCallback,
  VerifiedCallback,
} from '@laranex/nestjs-myanmar-payments';
import { Controller, Post } from '@nestjs/common';

@Controller('payments')
export class WavePayMiniAppCallbackController {
  @Post('wave-pay-mini-app/callback')
  @AcknowledgeCallback()
  handle(
    @VerifiedCallback('wave-pay-mini-app') callback: PaymentCallback,
  ): PaymentCallback {
    if (callback.isSuccessful()) {
      // callback.orderId is your orderId
      // callback.raw.merchantRefId is the attempt's reference
      // callback.gatewayReference is Wave's transactionId
    }

    return callback;
  }
}

callbackUrlFrom(req) is the protocol, host and path, without the query string: the protocol and host name Express or Fastify resolve (X-Forwarded-Proto and X-Forwarded-Host count only from a proxy the app trusts: Express trust proxy, Fastify trustProxy), the port of the matching Host (or trusted X-Forwarded-Host) value, and the path as received (Express originalUrl, so a mounted app keeps its mount path). A request without a host throws. handleCallback('wave-pay-mini-app', ...) needs the Nest request, not a CallbackRequest; passing one throws. When the URL the callback arrives at isn't the one the payment was signed with (e.g. behind a proxy the app doesn't trust), verify with the signed URL yourself:

ts
// callbackRequestFrom comes from '@laranex/nestjs-myanmar-payments'; req is @Req()
const callback = this.payments.wavePayMiniApp().handleCallback(
  await callbackRequestFrom(req),
  'https://shop.test/payments/wave-pay-mini-app/callback',
);

The expected hashValue is the same HMAC as the payment's, over the callback's amount (its exact JSON text), orderId, your mini app ID, merchantRefId and the callback URL, compared in constant time and ignoring the case of the hex digits. A missing or non-string hashValue, a mismatch, or an object or array in any callback field fails with Wave Pay Mini App callback hash verification failed.. After the hash matches, orderId and merchantRefId must not be empty and amount must be a whole number; otherwise it fails with Wave Pay Mini App callback is malformed.. @VerifiedCallback answers both with 400 Bad Request. JSON callbacks need rawBody: true in NestFactory.create().

Statuses ​

Only successful means the customer paid. transactionStatus is matched trimmed and case-insensitively.

Wave transactionStatusPaymentStatus
successfulSuccessful
processingPending
failedFailed
anything elseUnknown

Responses ​

What Wave Pay Mini App puts in each property. See Results and PaymentCallback & Status for the full classes. raw holds plain JavaScript values, with JSON numbers kept as their exact text in a string.

payment() → MiniAppPayment ​

Property / MethodWave Pay Mini App value
flowmini_app
orderIdYour data.orderId
amountThe signed total, an Amount in whole kyat
miniAppIdThe configured mini app ID
merchantReferenceIdYour data.merchantReferenceId, generated when empty
callbackUrlYour data.callbackUrl
hashValueThe payload's hash
itemsreadonly MiniAppPaymentItem[]: { name, amount }, amount an Amount
toJSON()The makePayment object: amount, miniAppID, merchantRefID, orderID, callBackUrl, hashValue, items (name, amount), amounts as numbers
toJsonString()The same object as compact JSON, amounts with every digit

handleCallback() → PaymentCallback ​

Property / MethodWave Pay Mini App value
orderIdWave orderId
statustransactionStatus mapped, see Statuses
gatewayStatusWave transactionStatus, trimmed, e.g. successful
gatewayReferenceWave transactionId
amountWave amount, e.g. 1000
rawThe verified body: merchantRefId, orderId, amount, msisdn, hashValue, transactionId, transactionStatus
acknowledgementHTTP 200, empty body, Content-Type: text/plain

Errors ​

CallThrowsWhen
wavePayMiniApp()ConfigurationErrorA setting is missing or blank, e.g. The wave_pay_mini_app configuration is missing [client_id].
payment()InvalidPaymentDataErrorA value breaks the rules above. Nothing is signed. Item errors use items.0.name and items.0.amount keys
accessToken()ApiErrorWave answers with an HTTP error, a body that isn't JSON, or no access_token. The message is Wave Pay Mini App token request failed: [<error>] <error_description> when Wave sends an error, else Wave Pay Mini App token request failed with HTTP <status>.; gatewayCode is Wave's error, gatewayMessage its error_description (or message)
handleCallback()SignatureVerificationErrorWave Pay Mini App callback hash verification failed.: hashValue is missing, not a string or doesn't match, or a callback field holds an object or array. Wave Pay Mini App callback is malformed.: the hash matched but orderId or merchantRefId is empty, or amount isn't a whole number

When Wave can't be reached, accessToken() throws ApiError with httpStatus 0.