Skip to content

AYA Pay ​

The AYA Payment Gateway is one hosted checkout for AYA Pay, other wallets (KBZ Pay, WavePay, UAB Pay, CB Pay…) and cards (VISA, Mastercard, JCB).

CallWhat it doesReturns
ayaPay().services()List the channels enabled for your accountAyaPayService[]
ayaPay().initiate(data)Signed form posted to AYAFormPayment
ayaPay().status(orderId)Enquire an orderPaymentStatusResult
ayaPay().handleCallback(request)Verify the backend callbackPaymentCallback
ayaPay().verifyRedirect(request)Verify the customer's returnPaymentCallback

Responses shows what AYA Pay puts in each result.

How it works ​

AYA posts the result to your callback URL and also signs the query string it adds when sending the customer back.

AYA Pay: channels, signed form, callback, returnCustomerYour appAYA PayList enabled channelsayaPay().services()Channels and methodse.g. aya_pay: QR, NOTIPick a channel and methodaya_pay + AyaPayMethod.QrRedirect to autoSubmitUrlayaPay().initiate(data)Post the form and payPOST /v1/payment/requestBackend callbackpayload + checkSumVerified callback is proofayaPay().handleCallback(request)Back on your return pagepayload + checkSum in the queryShow the right messageayaPay().verifyRedirect(request)
AYA Pay: channels, signed form, callback, return

Channels and Methods ​

channel is a lowercase key such as aya_pay, kbz_pay or visa; method is how the customer pays through it. Which ones you have depends on your merchant account, so list them:

ts
import { AyaPayMethod } from '@laranex/myanmar-payments';

for (const service of await this.payments.ayaPay().services()) {
  // service.name: "AYA Pay"
  // service.key: "aya_pay", pass it as channel
  // service.imageUrl: the channel's logo
  // service.methods: [AyaPayMethod.Qr, AyaPayMethod.Noti]
  if (service.supports(AyaPayMethod.Qr)) {
    // offer the QR method
  }
}

Methods AYA lists that this package doesn't know yet are kept in service.unknownMethods.

AyaPayMethodValueCustomer
AyaPayMethod.WebWEBPays on a hosted web page (cards)
AyaPayMethod.QrQRScans a QR with the wallet app
AyaPayMethod.NotiNOTIApproves a push notification in the wallet app

Initiating a Payment ​

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

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

@Controller('checkout')
export class AyaPayCheckoutController {
  constructor(
    private readonly payments: MyanmarPaymentsService,
    private readonly orders: OrdersService,
  ) {}

  @Post(':id/aya-pay')
  @Redirect()
  async initiate(@Param('id') id: string): Promise<{ url: string }> {
    const order = await this.orders.findOrFail(id);

    const data: AyaPayPaymentData = {
      orderId: `ORDER_${order.id}`,
      amount: 10000,
      channel: 'aya_pay',
      method: AyaPayMethod.Qr,
      returnUrl: 'https://shop.test/payments/aya/return',
      description: `Order #${order.id}`,
    };

    const payment = this.payments.ayaPay().initiate(data);

    return { url: this.payments.autoSubmitUrl(payment) };
  }
}

AyaPayPaymentData ​

ParameterTypeRequiredRules
orderIdstringYesUnique, 6 to 40 characters (merchOrderId)
amountAmountInputYesWhole kyat, greater than 0, e.g. 10000 or Amount.kyat(10000). AYA documents no decimals and only accepts MMK (104)
channelstringYesA key from services()
methodAyaPayMethodYesA method the channel supports
returnUrlstringNoAbsolute http or https URL. Unset uses the URL registered with AYA
descriptionstringNoShown to the customer
userRefsreadonly string[]NoUp to 5 of your own values, echoed back in the callback

Form Encoding ​

AYA expects the form as multipart/form-data. payment.enctype carries it; use it if you render the form yourself.

Handling Callbacks ​

AYA posts to the callback URL registered with them.

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 AyaPayCallbackController {
  @Post('aya/callback')
  @AcknowledgeCallback()
  handle(
    @VerifiedCallback('aya-pay') callback: PaymentCallback,
  ): PaymentCallback {
    if (callback.isSuccessful()) {
      // callback.orderId is your merchOrderId
      // callback.gatewayReference is AYA's tranId
    }

    return callback;
  }
}

A bad signature answers 400 before the handler runs. AYA signs only the fields present in its payload (wallet payments leave out the card fields); the package verifies them in the order the specification lists.

The Return Page ​

AYA signs the query string it adds when sending the customer back, so the return page can show the right message:

ts
import { SignatureVerificationError } from '@laranex/myanmar-payments';
import {
  callbackRequestFrom,
  MyanmarPaymentsService,
  type NestRequestLike,
} from '@laranex/nestjs-myanmar-payments';
import {
  BadRequestException,
  Controller,
  Get,
  Render,
  Req,
} from '@nestjs/common';

@Controller('payments')
export class AyaPayReturnController {
  constructor(private readonly payments: MyanmarPaymentsService) {}

  @Get('aya/return')
  @Render('payments/result')
  async show(@Req() req: NestRequestLike): Promise<{ status: string }> {
    const request = await callbackRequestFrom(req);

    try {
      const result = this.payments.ayaPay().verifyRedirect(request);

      return { status: result.status };
    } catch (error) {
      if (error instanceof SignatureVerificationError) {
        throw new BadRequestException();
      }
      throw error;
    }
  }
}

A + in the base64 payload that reached you as a space (an unencoded query string) is read back as + before decoding; the checksum is still verified. The payload may be correctly padded or carry no padding at all; partial padding, the URL-safe alphabet, line breaks and text that isn't UTF-8 are rejected. Still fulfill orders from the backend callback.

Status Checks ​

ts
const result = await this.payments.ayaPay().status(`ORDER_${order.id}`);

if (result.isSuccessful()) {
  // result.gatewayReference is AYA's tranId
}

status() takes your orderId. An order AYA doesn't know throws ApiError (20 Transaction not found).

Responses ​

What AYA Pay 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 (1000.50 stays '1000.50').

services() → AyaPayService[] ​

AyaPayService is AYA-only, so it is listed in full here.

Property / MethodAYA Pay value
nameAYA name, e.g. AYA Pay. Falls back to key
keyAYA key, e.g. aya_pay, kbz_pay, visa. Pass it as channel. Always set
imageUrlAYA image_url, the channel's logo. undefined when AYA sends none
methodsreadonly AyaPayMethod[] this package knows, e.g. ['QR', 'NOTI']
unknownMethodsreadonly string[] of methods AYA listed that this package doesn't know yet. Usually []
supports(method)Whether methods contains method

Entries AYA sends without a key are skipped.

initiate() → FormPayment ​

Property / MethodAYA Pay value
orderIdYour orderId
action{baseUrl}/v1/payment/request, e.g. https://pgw.ayainnovation.com/v1/payment/request
fieldsThe signed fields below. Post them unchanged
enctypemultipart/form-data
toHtml()A full HTML page that posts fields to action on load

this.payments.autoSubmitUrl(payment) returns an encrypted link to the module's form route, e.g. https://shop.test/myanmar-payments/form?payload=…. It expires after formLink.ttlMinutes; the call throws when formRoute.enabled is false.

fields, in the order AYA signs them:

Form fieldValue
merchOrderIdYour orderId
amountYour amount, e.g. 10000
appKeyYour configured app key
timestampUnix time in seconds
userRef1 … userRef5Your userRefs, "" when unused
descriptionYour description, "" when unset
currencyCode104 (MMK)
channelYour channel, e.g. aya_pay
methodYour method, e.g. QR
overrideFrontendRedirectUrlYour returnUrl, "" when unset
checkSumHMAC-SHA256 of the values above joined with :

initiate() makes no HTTP call. FormPayment has no raw: nothing is sent to AYA until the customer's browser posts the form.

status() → PaymentStatusResult ​

PropertyAYA Pay value
orderIdAYA merchOrderId, falling back to the orderId you passed. Always set
statusstatusCode mapped, see Statuses
gatewayStatusAYA statusCode, trimmed, e.g. 00
gatewayReferenceAYA tranId
amountAYA amount, e.g. 10000
rawThe verified, decoded enquiry payload: merchOrderId, tranId, amount, currencyCode, statusCode, paymentCardNumber, paymentMobileNumber, cardTypeName, cardExpiryDate, nameOnCard, approvalCode, tranRef, userRef1–5, description, dateTime

AYA leaves out the fields that don't apply (wallet payments have no card fields), so raw only has the keys AYA sent. Some payloads spell currencyCode as currenyCode.

handleCallback() → PaymentCallback ​

Property / MethodAYA Pay value
orderIdAYA merchOrderId (your orderId)
statusstatusCode mapped, see Statuses
gatewayStatusAYA statusCode, trimmed, e.g. 00
gatewayReferenceAYA tranId
amountAYA amount, e.g. 10000
rawThe verified, decoded payload, with the same keys as status()
acknowledgementHTTP 200, empty body, Content-Type: text/plain

@AcknowledgeCallback() (or acknowledge(res, callback)) sends acknowledgement.

verifyRedirect() → PaymentCallback ​

The same values as handleCallback(), read from the signed payload and checkSum AYA adds to your return URL (query string first, then the body). There is nothing to acknowledge: return your own page.

handleCallback() and verifyRedirect() take a CallbackRequest; build one from a Nest request with callbackRequestFrom(req), or call this.payments.handleCallback('aya-pay', req).

Statuses ​

AYA statusCodePaymentStatus
00Successful
01Pending
02 (fail), 03 (reject)Failed
04Expired
anything elseUnknown

Errors ​

CallThrowsWhen
initiate()InvalidPaymentDataErrorA value breaks the rules above. Nothing is signed
services()ApiErrorAYA answers with an HTTP error or a status other than 00
status()ApiErrorAYA answers with an HTTP error or a status other than 00, e.g. 20 Transaction not found
status()SignatureVerificationErrorThe enquiry payload's checkSum doesn't match
handleCallback(), verifyRedirect()SignatureVerificationErrorpayload is missing or not base64 JSON, a signed field holds an object or array, or checkSum doesn't match

ApiError carries AYA's status (e.g. 20 Transaction not found, 09 Duplicate order ID) in gatewayCode and its message in gatewayMessage. When AYA can't be reached, the calls throw ApiError with httpStatus 0.

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