qpay-mn-sdk
v0.1.0
Published
TypeScript SDK for QPay V2 (Mongolia) with Ebarimt 3.0 support. Functional API, token auto-refresh, callback verification, Express/Next middleware.
Maintainers
Readme
qpay-mn-sdk
TypeScript SDK for QPay V2 (Mongolia) with full Ebarimt 3.0 support.
- ✅ All V2 endpoints — invoice, payment, ebarimt v3
- ✅ Functional API, tree-shakeable
- ✅ Token auto-refresh
- ✅ Callback verification (always round-trips QPay)
- ✅ Express & Next.js (App Router + Pages Router) middleware
- ✅ Bundled reference data — banks, districts, GS1 classifications, VAT codes, error messages
- ✅ Strict TypeScript types for every request & response
- ✅ Zero runtime dependencies
Installation
npm i qpay-mn-sdk
# or
pnpm add qpay-mn-sdk
# or
yarn add qpay-mn-sdkRequires Node 18+ (uses global fetch).
Quick start
import {
createClient,
createSimpleInvoice,
checkPayment,
createEbarimt,
} from "qpay-mn-sdk";
const client = createClient({
username: process.env.QPAY_USERNAME!,
password: process.env.QPAY_PASSWORD!,
env: "sandbox", // or "production"
});
// 1) Create an invoice — QR + bank deeplinks
const invoice = await createSimpleInvoice(client, {
invoice_code: "TEST_INVOICE",
sender_invoice_no: "ORDER-1234",
invoice_receiver_code: "terminal",
invoice_description: "Order #1234",
amount: 25000,
callback_url: "https://example.com/qpay/webhook",
});
// 2) From your callback handler, verify the payment with QPay
const result = await checkPayment(client, {
object_type: "INVOICE",
object_id: invoice.invoice_id,
});
if (result.count > 0 && result.rows[0].payment_status === "PAID") {
// 3) Issue Ebarimt 3.0 receipt
await createEbarimt(client, {
payment_id: result.rows[0].payment_id,
ebarimt_receiver_type: "CITIZEN",
ebarimt_receiver: "88614450",
district_code: "3505",
});
}API overview
All functions take the client as the first argument:
| Group | Function | Endpoint |
| --- | --- | --- |
| Auth | authenticate | POST /v2/auth/token |
| | refresh | POST /v2/auth/refresh |
| Invoice | createInvoice | POST /v2/invoice (full) |
| | createSimpleInvoice | POST /v2/invoice (simple) |
| | createEbarimtInvoice | POST /v2/invoice (with ebarimt fields) |
| | cancelInvoice | DELETE /v2/invoice/:id |
| Payment | getPayment | GET /v2/payment/:id |
| | checkPayment | POST /v2/payment/check |
| | cancelPayment | DELETE /v2/payment/cancel/:id |
| | refundPayment | DELETE /v2/payment/refund/:id |
| | listPayments | POST /v2/payment/list |
| Ebarimt 3.0 | createEbarimt | POST /v2/ebarimt_v3/create |
| | cancelEbarimt | DELETE /v2/ebarimt_v3/:payment_id |
| Callback | verifyCallback | (server-side verification) |
| | extractPaymentId | (parse query) |
Three invoice variants — when to use which
QPay's single /v2/invoice endpoint accepts three different request shapes. The SDK exposes them as three named functions so the types stay accurate.
| Function | Use when |
| --- | --- |
| createSimpleInvoice | One-off checkout; you just need a QR + amount |
| createInvoice | Multi-account routing, partial payments, subscriptions |
| createEbarimtInvoice | A tax receipt (Ebarimt) is required at payment time |
Ebarimt 3.0 (V3)
This SDK calls the new V3 endpoint /v2/ebarimt_v3/create, which returns the extra fields required by the Mongolian tax authority:
const receipt = await createEbarimt(client, { ... });
receipt.merchant_tin; // ← V3 only
receipt.ebarimt_receipt_id; // ← V3 onlyTwo issuance modes:
// To a citizen (uses phone number registered in Ebarimt app)
await createEbarimt(client, {
payment_id,
ebarimt_receiver_type: "CITIZEN",
ebarimt_receiver: "88614450",
district_code: "3505",
});
// To a company (uses TIN)
await createEbarimt(client, {
payment_id,
ebarimt_receiver_type: "COMPANY",
ebarimt_receiver: "5395305", // company TIN
district_code: "3505",
});Callback verification
Important: QPay does not sign callbacks. You MUST round-trip the payment ID back to QPay before trusting it.
import { verifyCallback } from "qpay-mn-sdk";
const result = await verifyCallback(client, req.url, {
invoiceId: storedOrder.invoice_id, // required
expectedAmount: storedOrder.amount, // recommended
});
if (result.isPaid) {
// safe to mark order paid
}The function throws QPayCallbackError if:
qpay_payment_idisn't in the query string- The payment isn't found for the supplied invoice
- The paid amount is less than
expectedAmount
Required response
Per QPay spec, your webhook MUST respond with HTTP 200 and the literal body SUCCESS. The middleware does this for you; if you're rolling your own:
import { CALLBACK_SUCCESS_BODY } from "qpay-mn-sdk";
res.status(200).send(CALLBACK_SUCCESS_BODY);Express middleware
import express from "express";
import { qpayCallback } from "qpay-mn-sdk/middleware/express";
app.get("/qpay/webhook", qpayCallback({
client,
resolve: async (req) => {
const paymentId = req.query.qpay_payment_id as string;
const order = await db.findOrderByPaymentRef(paymentId);
return order ? { invoiceId: order.invoice_id, expectedAmount: order.amount } : null;
},
onPaid: async (result) => {
await db.markOrderPaid(result.paymentId);
},
}));Next.js middleware
App Router:
// app/api/qpay/webhook/route.ts
import { createQPayCallbackHandler } from "qpay-mn-sdk/middleware/next";
export const GET = createQPayCallbackHandler({
client,
resolve: async ({ paymentId }) => {
const order = await db.findOrderByPaymentRef(paymentId);
return order && { invoiceId: order.invoice_id, expectedAmount: order.amount };
},
onPaid: async (result) => { /* ... */ },
});Pages Router: use createQPayCallbackApiRoute instead.
Reference data
Bundled lookup tables for everything QPay's docs list as a static table:
import {
getBank, getCurrency, getDistrict,
getClassification, searchClassifications,
getZeroVatCode, getVatExemptCode,
describeError,
} from "qpay-mn-sdk/data";
getBank("050000"); // { code: "050000", nameEn: "Khan bank", nameMn: "Хаан банк" }
getDistrict("3505"); // { code: "3505", branchName: "...", subBranchName: "..." }
getClassification("0111100"); // { code: "0111100", name: "Улаан буудайн үр" }
searchClassifications("буудай", 10);
describeError("INVOICE_PAID"); // { messageMn: "...", messageEn: "Invoice is paid!" }The GS1 classification table is ~500 KB. Import it on the server only — it's a separate entry so it won't end up in your client bundle unless you ask for it.
Token management
The client authenticates lazily on the first request and refreshes the token before it expires (60s leeway by default). You can also:
import { authenticate, isAuthenticated, getCurrentToken, restoreToken } from "qpay-mn-sdk";
await authenticate(client); // validate creds on app start
if (!isAuthenticated(client)) await authenticate(client);
// Persist + restore across processes:
const token = getCurrentToken(client);
await redis.set("qpay:token", JSON.stringify(token));
const cached = JSON.parse(await redis.get("qpay:token") ?? "null");
if (cached) restoreToken(client, cached);Error handling
import { QPayError, QPayAuthError, QPayValidationError, QPayHttpError } from "qpay-mn-sdk";
try {
await createInvoice(client, ...);
} catch (e) {
if (e instanceof QPayValidationError) {
console.error("bad request:", e.code, e.message);
} else if (e instanceof QPayAuthError) {
console.error("auth failed:", e.message);
} else if (e instanceof QPayError) {
console.error("qpay error:", e.status, e.code, e.body);
}
}Common business error codes are typed as a literal union (QPayErrorKey) — autocomplete works in switch/if statements.
TypeScript
Every request/response is strictly typed. Literal unions for BankCode, CurrencyCode, PaymentStatus, TransactionType, TaxType, etc.
import type { CreateEbarimtInvoiceRequest, EbarimtResponse } from "qpay-mn-sdk";Comparison
| | qpay-mn-sdk | @mnpay/qpay | @togtokh.dev/qpay | | --- | --- | --- | --- | | Ebarimt 3.0 (V3) | ✅ | ❌ V2 only | ❌ V2 only | | Full TypeScript | ✅ | partial | partial | | Bundled lookup tables | ✅ | ❌ | ❌ | | Callback verification helper | ✅ | ❌ | ❌ | | Express/Next middleware | ✅ | ❌ | ❌ | | Token auto-refresh | ✅ | manual | manual | | Zero deps | ✅ | axios | axios |
License
MIT
Disclaimer
This is an unofficial SDK. QPay™ is a trademark of QPay LLC, Mongolia. Refer to the official documentation for the authoritative API specification.
