QPay Dart SDK

A Dart/Flutter client library for the QPay V2 API with automatic token management, strong typing, and comprehensive error handling.

Features

  • Automatic access token acquisition and refresh
  • Strongly typed request/response models for all endpoints
  • Invoice creation (simple, full, and ebarimt/tax)
  • Payment checking, listing, cancellation, and refund
  • Ebarimt (electronic tax receipt) creation and cancellation
  • Named error code constants for easy matching
  • Works with Dart CLI apps and Flutter

Installation

Dart

dart pub add qpay

Flutter

flutter pub add qpay

Or add it manually to your pubspec.yaml:

dependencies:
  qpay: ^1.0.0

Then run:

dart pub get

Quick Start

import 'package:qpay/qpay.dart';

void main() async {
  final client = QPayClient(QPayConfig(
    baseUrl: 'https://merchant.qpay.mn',
    username: 'YOUR_USERNAME',
    password: 'YOUR_PASSWORD',
    invoiceCode: 'YOUR_INVOICE_CODE',
    callbackUrl: 'https://yourapp.com/callback',
  ));

  try {
    // Create a simple invoice
    final invoice = await client.createSimpleInvoice(CreateSimpleInvoiceRequest(
      invoiceCode: 'YOUR_INVOICE_CODE',
      senderInvoiceNo: 'INV-001',
      invoiceReceiverCode: 'terminal',
      invoiceDescription: 'Order #001',
      amount: 10000,
      callbackUrl: 'https://yourapp.com/callback',
    ));

    print('Invoice ID: ${invoice.invoiceId}');
    print('QR Image: ${invoice.qrImage}');
    print('Short URL: ${invoice.qPayShortUrl}');

    // Check payment status
    final check = await client.checkPayment(PaymentCheckRequest(
      objectType: 'INVOICE',
      objectId: invoice.invoiceId,
    ));

    print('Paid: ${check.count > 0}');
  } on QPayException catch (e) {
    print('QPay error: ${e.code} - ${e.message}');
  } finally {
    client.close();
  }
}

Configuration

Direct Configuration

const config = QPayConfig(
  baseUrl: 'https://merchant.qpay.mn',
  username: 'YOUR_USERNAME',
  password: 'YOUR_PASSWORD',
  invoiceCode: 'YOUR_INVOICE_CODE',
  callbackUrl: 'https://yourapp.com/callback',
);

From Environment Variables

For server-side Dart applications, you can load configuration from environment variables:

final config = QPayConfig.fromEnvironment();

This reads the following environment variables:

Variable Description
QPAY_BASE_URL QPay API base URL (e.g. https://merchant.qpay.mn)
QPAY_USERNAME QPay merchant username
QPAY_PASSWORD QPay merchant password
QPAY_INVOICE_CODE Default invoice code
QPAY_CALLBACK_URL Payment callback URL

Throws ArgumentError if any required variable is missing.

Custom HTTP Client

You can provide your own http.Client for custom configurations or testing:

import 'package:http/http.dart' as http;

final httpClient = http.Client();
final client = QPayClient(config, httpClient: httpClient);

When you provide your own HTTP client, calling client.close() will not close it -- you are responsible for its lifecycle.

Usage

Authentication

The client handles authentication automatically. Tokens are acquired on the first request and refreshed transparently when they expire. You can also manage tokens manually:

// Manually get a new token
final token = await client.getToken();
print('Access token: ${token.accessToken}');

// Manually refresh the token
final refreshed = await client.refreshToken();

Create a Simple Invoice

final invoice = await client.createSimpleInvoice(CreateSimpleInvoiceRequest(
  invoiceCode: 'YOUR_INVOICE_CODE',
  senderInvoiceNo: 'INV-001',
  invoiceReceiverCode: 'terminal',
  invoiceDescription: 'Coffee x2',
  amount: 12000,
  callbackUrl: 'https://yourapp.com/callback',
));

print('Invoice ID: ${invoice.invoiceId}');
print('QR: ${invoice.qrText}');

// Deep links for bank apps
for (final url in invoice.urls) {
  print('${url.name}: ${url.link}');
}

Create a Full Invoice

final invoice = await client.createInvoice(CreateInvoiceRequest(
  invoiceCode: 'YOUR_INVOICE_CODE',
  senderInvoiceNo: 'INV-002',
  invoiceReceiverCode: 'terminal',
  invoiceDescription: 'Detailed order',
  amount: 25000,
  callbackUrl: 'https://yourapp.com/callback',
  allowPartial: false,
  allowExceed: false,
  note: 'Order note',
  senderBranchData: SenderBranchData(
    name: 'Main Branch',
    email: 'branch@example.com',
    phone: '99001122',
    address: Address(city: 'Ulaanbaatar', district: 'KHN'),
  ),
  invoiceReceiverData: InvoiceReceiverData(
    name: 'Customer',
    email: 'customer@example.com',
    phone: '88001122',
  ),
  lines: [
    InvoiceLine(
      lineDescription: 'Widget A',
      lineQuantity: '2',
      lineUnitPrice: '10000',
      taxes: [
        TaxEntry(taxCode: 'VAT', description: 'VAT 10%', amount: 2000),
      ],
    ),
    InvoiceLine(
      lineDescription: 'Widget B',
      lineQuantity: '1',
      lineUnitPrice: '5000',
    ),
  ],
));

Create an Ebarimt Invoice (Tax Invoice)

final invoice = await client.createEbarimtInvoice(CreateEbarimtInvoiceRequest(
  invoiceCode: 'YOUR_INVOICE_CODE',
  senderInvoiceNo: 'INV-003',
  invoiceReceiverCode: 'terminal',
  invoiceDescription: 'Tax invoice',
  taxType: 'VAT',
  districtCode: 'UB01',
  callbackUrl: 'https://yourapp.com/callback',
  lines: [
    EbarimtInvoiceLine(
      taxProductCode: 'TP001',
      lineDescription: 'Service fee',
      lineQuantity: '1',
      lineUnitPrice: '50000',
      classificationCode: 'CL01',
    ),
  ],
));

Cancel an Invoice

await client.cancelInvoice('invoice-id');

Check Payment Status

final result = await client.checkPayment(PaymentCheckRequest(
  objectType: 'INVOICE',
  objectId: 'invoice-id',
));

if (result.count > 0) {
  print('Paid amount: ${result.paidAmount}');
  for (final row in result.rows) {
    print('Payment ${row.paymentId}: ${row.paymentStatus}');
    print('  Amount: ${row.paymentAmount} ${row.paymentCurrency}');
    print('  Wallet: ${row.paymentWallet}');
  }
}

Get Payment Details

final payment = await client.getPayment('payment-id');

print('Status: ${payment.paymentStatus}');
print('Amount: ${payment.paymentAmount}');
print('Date: ${payment.paymentDate}');

// Card transaction details
for (final ct in payment.cardTransactions) {
  print('Card: ${ct.cardType} ${ct.cardNumber}');
  print('Settlement: ${ct.settlementStatus}');
}

// P2P transaction details
for (final p2p in payment.p2pTransactions) {
  print('Bank: ${p2p.accountBankName}');
  print('Account: ${p2p.accountNumber}');
}

List Payments

final payments = await client.listPayments(PaymentListRequest(
  objectType: 'INVOICE',
  objectId: 'invoice-id',
  startDate: '2025-01-01',
  endDate: '2025-12-31',
  offset: Offset(pageNumber: 1, pageLimit: 20),
));

print('Total: ${payments.count}');
for (final item in payments.rows) {
  print('${item.paymentId}: ${item.paymentAmount} ${item.paymentCurrency}');
}

Cancel a Payment

await client.cancelPayment(
  'payment-id',
  PaymentCancelRequest(
    callbackUrl: 'https://yourapp.com/cancel-callback',
    note: 'Customer requested cancellation',
  ),
);

Refund a Payment

await client.refundPayment(
  'payment-id',
  PaymentRefundRequest(
    callbackUrl: 'https://yourapp.com/refund-callback',
    note: 'Defective product',
  ),
);

Create an Ebarimt (Tax Receipt)

final ebarimt = await client.createEbarimt(CreateEbarimtRequest(
  paymentId: 'payment-id',
  ebarimtReceiverType: 'INDIVIDUAL',
  ebarimtReceiver: '99001122',
  districtCode: 'UB01',
));

print('Ebarimt ID: ${ebarimt.id}');
print('Lottery: ${ebarimt.ebarimtLottery}');
print('QR Data: ${ebarimt.ebarimtQrData}');
print('VAT: ${ebarimt.vatAmount}');

Cancel an Ebarimt

final cancelled = await client.cancelEbarimt('payment-id');
print('Status: ${cancelled.barimtStatus}');

Error Handling

All API errors throw QPayException with structured information:

try {
  await client.cancelInvoice('nonexistent-id');
} on QPayException catch (e) {
  print('Status: ${e.statusCode}');   // HTTP status code (e.g. 404)
  print('Code: ${e.code}');           // QPay error code (e.g. "INVOICE_NOTFOUND")
  print('Message: ${e.message}');     // Human-readable message
  print('Body: ${e.rawBody}');        // Raw response body for debugging

  // Match against known error codes
  if (e.code == QPayException.invoiceNotFound) {
    print('Invoice does not exist');
  } else if (e.code == QPayException.invoicePaid) {
    print('Invoice has already been paid');
  } else if (e.code == QPayException.authenticationFailed) {
    print('Check your credentials');
  }
}

Error Code Constants

The QPayException class provides named constants for all known QPay error codes:

QPayException.authenticationFailed    // "AUTHENTICATION_FAILED"
QPayException.invoiceNotFound         // "INVOICE_NOTFOUND"
QPayException.invoicePaid             // "INVOICE_PAID"
QPayException.invoiceAlreadyCanceled  // "INVOICE_ALREADY_CANCELED"
QPayException.invoiceCodeInvalid      // "INVOICE_CODE_INVALID"
QPayException.invoiceLineRequired     // "INVOICE_LINE_REQUIRED"
QPayException.paymentNotFound         // "PAYMENT_NOTFOUND"
QPayException.paymentAlreadyCanceled  // "PAYMENT_ALREADY_CANCELED"
QPayException.paymentNotPaid          // "PAYMENT_NOT_PAID"
QPayException.permissionDenied        // "PERMISSION_DENIED"
QPayException.invalidAmount           // "INVALID_AMOUNT"
QPayException.minAmountErr            // "MIN_AMOUNT_ERR"
QPayException.maxAmountErr            // "MAX_AMOUNT_ERR"
QPayException.merchantNotFound        // "MERCHANT_NOTFOUND"
QPayException.merchantInactive        // "MERCHANT_INACTIVE"
QPayException.noCredentials           // "NO_CREDENDIALS"
QPayException.ebarimtNotRegistered    // "EBARIMT_NOT_REGISTERED"
QPayException.transactionRequired     // "TRANSACTION_REQUIRED"
// ... and more

Flutter Integration Tips

Display QR Code

The qrImage field in InvoiceResponse contains a base64-encoded PNG image:

import 'dart:convert';
import 'package:flutter/material.dart';

Widget buildQrImage(InvoiceResponse invoice) {
  final bytes = base64Decode(invoice.qrImage);
  return Image.memory(bytes);
}

Use url_launcher to open bank app deep links from InvoiceResponse.urls:

import 'package:url_launcher/url_launcher.dart';

Future<void> openBankApp(Deeplink deeplink) async {
  final uri = Uri.parse(deeplink.link);
  if (await canLaunchUrl(uri)) {
    await launchUrl(uri);
  }
}

// Show a list of available banks
Widget buildBankList(InvoiceResponse invoice) {
  return ListView.builder(
    itemCount: invoice.urls.length,
    itemBuilder: (context, index) {
      final bank = invoice.urls[index];
      return ListTile(
        leading: Image.network(bank.logo),
        title: Text(bank.name),
        subtitle: Text(bank.description),
        onTap: () => openBankApp(bank),
      );
    },
  );
}

Payment Polling

Poll for payment status after showing the QR code:

import 'dart:async';

Future<bool> waitForPayment(QPayClient client, String invoiceId) async {
  for (var i = 0; i < 60; i++) {
    await Future.delayed(Duration(seconds: 5));

    final result = await client.checkPayment(PaymentCheckRequest(
      objectType: 'INVOICE',
      objectId: invoiceId,
    ));

    if (result.count > 0) {
      return true; // Payment received
    }
  }
  return false; // Timeout after 5 minutes
}

Provider / Riverpod Setup

// Using a simple singleton
class QPayService {
  late final QPayClient _client;

  QPayService() {
    _client = QPayClient(QPayConfig(
      baseUrl: 'https://merchant.qpay.mn',
      username: 'USERNAME',
      password: 'PASSWORD',
      invoiceCode: 'CODE',
      callbackUrl: 'https://yourapp.com/callback',
    ));
  }

  QPayClient get client => _client;

  void dispose() => _client.close();
}

API Reference

QPayClient Methods

Method Description
getToken() Authenticate and get a new token pair
refreshToken() Refresh the current access token
createInvoice(request) Create a full invoice with all options
createSimpleInvoice(request) Create a simple invoice with minimal fields
createEbarimtInvoice(request) Create an invoice with ebarimt (tax) info
cancelInvoice(invoiceId) Cancel an existing invoice
getPayment(paymentId) Get payment details by ID
checkPayment(request) Check if a payment has been made
listPayments(request) List payments with filters and pagination
cancelPayment(paymentId, request) Cancel a card payment
refundPayment(paymentId, request) Refund a card payment
createEbarimt(request) Create an electronic tax receipt
cancelEbarimt(paymentId) Cancel an electronic tax receipt
close() Close the underlying HTTP client

Models

Request models (used as method arguments):

  • CreateInvoiceRequest -- Full invoice creation
  • CreateSimpleInvoiceRequest -- Simple invoice creation
  • CreateEbarimtInvoiceRequest -- Invoice with tax info
  • PaymentCheckRequest -- Check payment status
  • PaymentListRequest -- List payments with filters
  • PaymentCancelRequest -- Cancel a payment
  • PaymentRefundRequest -- Refund a payment
  • CreateEbarimtRequest -- Create a tax receipt

Response models (returned from methods):

  • TokenResponse -- Authentication token pair
  • InvoiceResponse -- Created invoice with QR code and deep links
  • PaymentDetail -- Full payment information
  • PaymentCheckResponse / PaymentCheckRow -- Payment check results
  • PaymentListResponse / PaymentListItem -- Payment list results
  • EbarimtResponse -- Tax receipt information

Common models (used in requests and responses):

  • Address -- Physical address
  • SenderBranchData -- Branch information
  • SenderStaffData -- Staff information
  • InvoiceReceiverData -- Invoice receiver info
  • Account -- Bank account
  • Transaction -- Invoice transaction
  • InvoiceLine -- Invoice line item
  • EbarimtInvoiceLine -- Tax invoice line item
  • TaxEntry -- Tax/discount/surcharge entry
  • Deeplink -- Bank app deep link
  • Offset -- Pagination offset
  • EbarimtItem -- Tax receipt line item
  • EbarimtHistory -- Tax receipt history record

License

MIT License. See LICENSE for details.

Libraries

qpay
QPay V2 API client SDK for Dart/Flutter.