SmartPay Agent API — интеграцийн заавар

SmartPay Agent API нь терминал (POS, касс, апп) болон EPay төлбөрийн шилжүүлэгчийн хооронд байрлах хаалга юм. Тэрээр хоёр ажил хийнэ: нэхэмжлэх/QR-ын REST үйлчилгээ үзүүлэх, мөн банкнаас төлбөр орсон агшинд яг тухайн терминал руу Socket.IO-оор шууд мэдэгдэл түлхэх.

Хэнд зориулсан бэ

Терминалын програм хангамж (POS/касс/mobile) хөгжүүлэгч талд. Энэ заавар нь smartpay-agent-api сервисийн бодит зан төлөвийг тайлбарлана — юу заавал, юу сонголт, аль нь чимээгүй бүтэлгүйтдэгийг нь.

Тойм#

Сервис нь дараах хоёр сувгийг зэрэг барина:

СувагЧиглэлЮунд ашиглагдах
REST HTTP Терминал → Agent API Нэхэмжлэх үүсгэх, MOF QR бэлдэх, төлбөрийн төлөв шалгах
Socket.IO WS Agent API → Терминал Банкнаас төлбөр орсныг терминалд шууд мэдэгдэх

Хамгийн чухал ойлголт: төлбөр орсныг та лавлаж мэдэхгүй, харин мэдэгдэл хүлээж авна. Терминал нь Socket.IO холболтоо байнга нээлттэй барьж notify event-ийг сонсох ёстой.

Гүйлгээний бүтэн урсгал#

Терминал Agent API MOF / tpay Банк EPay switch 1. Socket.IO холбогдож room-д нэгдэнэ 2. POST /invoices 3. МОФ-д нэхэмжлэх 4. qr_code + deep_link буцна 5. Худалдан авагч QR-аа уншуулж банкаараа төлнө 6. POST /notify (банкны callback) 7. e2c wall → switch руу «paid» дамжуулна 8. notify event (WebSocket) room = «craccount-terminalId» 6–8 дугаар алхам секундын дотор явагдана. Терминал 1-р алхмаа хийгээгүй бол 8 дахь алхам хаяглах газаргүй болж алга болно.
Нэхэмжлэхээс төлбөрийн мэдэгдэл хүртэлх дараалал

Орчин ба хаяг#

ОрчинREST base URLSocket.IO URL
Production https://agent.epayment.mn/api https://agent.epayment.mn
Dev https://dev-agent.epayment.mn/api https://dev-agent.epayment.mn
Local http://localhost:3010/api http://localhost:3010
Socket.IO дээр /api угтвар БАЙХГҮЙ

/api угтвар нь зөвхөн HTTP route-д хамаарна. Socket.IO нь өөрийн /socket.io/ замаар ажилладаг тул холбогдохдоо https://agent.epayment.mn гэж үндсэн хаягийг өг — .../api гэвэл холбогдохгүй.

Сервис амьд эсэхийг шалгах хамгийн хурдан арга:

bash
curl https://agent.epayment.mn/api
# {"message":"Hello API"}

Нэвтрэлт#

Танд EPay-ээс нэг client key олгогдоно. Түүнийгээ хоёр өөр газар өөр нэрээр дамжуулна:

СувагХаана бичихЖишээ
REST HTTP header X-Api-Key: <ТАНЫ_KEY>
Socket.IO Handshake-ийн auth объект auth: { apiKey: '<ТАНЫ_KEY>' }

Endpoint тус бүрийн шаардлага:

EndpointX-Api-KeyТайлбар
GET /apiHealth check
POST /api/invoicesЗаавалБайхгүй бол 401
GET /api/checkЗаавалБайхгүй бол 401
POST /api/generate-qrЗөвхөн тооцоолол, гадагш хандахгүй
POST /api/notifyБанк/МОФ талаас дуудагддаг
Түлхүүрээ хамгаал

Client key-г хөтчийн кодод шууд бүү суулга. Вэб дээрээс холбох шаардлагатай бол өөрийн backend-ээрээ дамжуул. Түлхүүр алдагдсан бол EPay-д нэн даруй мэдэгдэж соль.

WebSocket — холболт үүсгэх#

Socket.IO v4 ашиглана (серверийнхтэй үндсэн хувилбар таарах ёстой). Үндсэн namespace /. Холбогдохдоо handshake-ийн authapiKey болон terminalId хоёрыг өгнө.

Node.js / React Native

javascript
import { io } from 'socket.io-client'; // ^4

const craccount  = '100900020012'; // танай хүлээн авах данс
const terminalNo = '1';            // терминалын дугаар (зөвхөн цифр)

const socket = io('https://agent.epayment.mn', {
  auth: {
    apiKey: process.env.EPAY_CLIENT_KEY,
    terminalId: `${craccount}-${terminalNo}`,
  },
  // transports-ыг ЗААВАЛ default-аар нь үлдээ — доорх анхааруулгыг үз
  reconnection: true,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 10000,
});

socket.on('connect', () => {
  console.log('holbogdloo', socket.id, socket.io.engine.transport.name);
});

socket.on('notify', ({ body, wallResponse }) => {
  // body        — банкнаас ирсэн гүйлгээний мэдээлэл
  // wallResponse — EPay switch рүү дамжуулсны хариу (алдаатай ч байж болно)
  console.log('tolbor orloo', body.tamount, body.bank_refnum);
});

socket.on('connect_error', (err) => console.error('connect_error', err.message));
socket.on('disconnect', (reason) => console.warn('salsan', reason));

Хөтөч

html
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
<script>
  const socket = io('https://agent.epayment.mn', {
    auth: { apiKey: 'CLIENT_KEY', terminalId: '100900020012-1' },
  });
  socket.on('notify', (data) => console.log('notify', data));
</script>
Анхаар: transports: ['websocket'] гэж бүү бич

Сервис нь Cloudflare-ийн ард байрладаг бөгөөд цэвэр WebSocket upgrade амжилтгүй болдог нь хэмжигдсэн. transports-ыг зааж өгөхгүй орхивол Socket.IO эхлээд polling-оор холбогдож, боломжтой үед өөрөө upgrade хийнэ — энэ нь найдвартай ажилладаг.

transports: ['websocket'] гэж хатуу заавал холболт connect_error: websocket error өгч огт бүтэхгүй. Олон жишээ код (манай хуучин README ч оруулаад) үүнийг зөвлөсөн байдаг — бүү дага.

Room нэршил#

Сервер нь холбогдсон socket бүрийг terminalId-тэй яг ижил нэртэй room-д оруулна. Мэдэгдэл ирэхэд дараах томьёогоор room-ыг олж түлхэнэ:

text
room = "<craccount>-<terminalId>"
        │            └── notify биетийн tmean доторх ST: -ийн ард байгаа ЦИФРҮҮД
        └── notify биетийн craccount талбар (хүлээн авагчийн данс)

Жишээ:  craccount = 100900020012
        tmean     = "QPAY 915023802387369 ST:1,REF:0000093;"
        →  room   = "100900020012-1"

Тиймээс таны терминал auth.terminalId-даа яг энэ мөрийг дамжуулах ёстой. Ганц тэмдэгт зөрвөл (жишээ нь ST:01 гэж бодоод ...-01 гэж холбогдвол) мэдэгдэл огт хүрэхгүй бөгөөд ямар ч алдаа гарахгүй.

ST нь зөвхөн цифр

Сервер tmean-ээс терминалын дугаарыг ST:-ийн ард байгаа цифрүүдээр л таньдаг. ST: байхгүй, эсвэл араас нь цифр биш зүйл ирвэл мэдэгдэл 400 болж унана.

notify event#

Терминал зөвхөн нэг event сонсоно — notify. Payload нь хоёр хэсэгтэй:

json
{
  "body": {
    "crBankbic": "MOFUMNUB",
    "dbBankbic": "KHANMNUB",
    "craccount": "100900020012",
    "dbaccount": "5027328504",
    "craccountname": "ТӨРИЙН САН",
    "dbaccountname": "БАТ",
    "tmean": "QPAY 915023802387369 ST:1,REF:0000093;",
    "bank_refnum": "001240573605",
    "tdate": "2026-09-08 11:42:07",
    "tamount": 1000,
    "add_data": "",
    "debt_bankid": "",
    "crbankcode": "",
    "ref_num": "0000093"
  },
  "wallResponse": {
    "success": true,
    "message": "Received",
    "switchResponse": { }
  }
}
ТалбарУтга
body.tamountТөлөгдсөн дүн (тоо, MNT)
body.bank_refnumБанкны гүйлгээний дугаар — баримт дээр хэвлэх/тулгахад
body.ref_numНэхэмжлэхийн лавлах дугаар
body.tmeanГүйлгээний утга — ST (терминал), REF-ийг үүнээс салгана
body.tdateБанкны гүйлгээний огноо
body.dbaccountnameТөлөгчийн нэр
wallResponse EPay switch рүү дамжуулсны үр дүн. undefined байж болно (сервер тохиргоогүй үед), success: false ч байж болно.
wallResponse-оор төлбөрийг битгий үгүйсгэ

notify event өөрөө банк мөнгө шилжүүлсэн гэсэн үг. wallResponse.success === false гэдэг нь EPay-ийн дотоод бүртгэлийн алхам бүтсэнгүй гэсэн үг болохоос төлбөр болоогүй гэсэн үг биш. Кассын дэлгэц дээр төлбөрийг баталгаажуулахдаа body-г үндэслэ.

Заавал мөрдөх дүрэм#

  1. terminalId заавал. Түүнгүйгээр холбогдвол сервер холболтыг тэр дор нь таслана (disconnect).
  2. Холболтоо байнга нээлттэй бай. Мэдэгдэл ирэх агшинд тухайн room-д нэг ч socket байхгүй бол сервер юу ч хийхгүй — мэдэгдэл алга болно. Дараалал, дахин илгээлт, хадгалалт байхгүй.
  3. Reconnect-ээ асаа. Socket.IO-гийн үндсэн reconnect-ыг унтраахгүй. Сүлжээ тасарсан богино хугацаанд орсон төлбөр буцаж ирэхгүй тул (2-р дүрэм) сэргэсний дараа GET /check-ээр нөхөж тулгах нь зүйтэй.
  4. Нэг room-д олон socket байж болно. Хоёр төхөөрөмж ижил terminalId-гаар холбогдвол хоёуланд нь мэдэгдэл очно. Давхардлаас сэргийлэхийн тулд bank_refnum-аар дедупликац хий.
  5. Идэмхий бус (idempotent) боловсруул. Ижил bank_refnum дахин ирвэл хоёр дахь баримт бүү хэвлэ.

REST endpoint-ууд#

GET/api нээлттэй

Health check. Сервис амьд эсэхийг шалгана.

bash
curl https://agent.epayment.mn/api
# 200  {"message":"Hello API"}
POST/api/invoices X-Api-Key

МОФ (Төрийн сан)-д нэхэмжлэх үүсгэнэ. Agent API нь биетийг шалгаад МОФ-ийн /epayment руу дамжуулж, хариуг нь өөрчлөхгүйгээр буцаана.

Хүсэлт

json
{
  "header": {
    "from_": {
      "system_owner":     "…",
      "institution_id":   "…",
      "institution_name": "…"
    },
    "to_": {
      "system_owner":     "…",
      "institution_id":   "…",
      "institution_name": "…"
    },
    "version":               "…",
    "bussines_service":      "…",
    "bussines_message_type": "…",
    "creation_date":         "…",
    "expire_date":           "…",
    "timestamp":             "…"
  },
  "detail": {
    "ref_num":     "0000093",
    "terminal_id": "1",
    "tamount":     1000,
    "tmean":       "…",
    "url":         "…"
  }
}
Шалгалтын дүрэм
  • header-ийн бүх талбар болон from_/to_-ийн гурван талбар заавал, бүгд мөр (string) байх ёстой.
  • detail.ref_num заавал, мөр байна.
  • detail.terminal_id заавал — мөр эсвэл тоо аль нь ч болно.
  • detail-ийн бусад талбар (tamount, tmean, url…) шалгагдахгүй, шууд дамжина — утгуудыг МОФ-ийн гэрээгээр бөглөнө.

Хариу

МОФ-ийн хариу тэр чигтээ буцна. Гол талбарууд нь detail.qr_code (QR-ын EMVCo payload) болон detail.deep_link (банкны аппуудын жагсаалт).

СтатусУтга
200Амжилттай — МОФ-ийн биет
400Биетийн бүтэц буруу (алдааны мессежид ямар талбар дутууг заана)
401X-Api-Key буруу эсвэл байхгүй
502МОФ-той холбогдож чадсангүй
бусадМОФ-ийн статус, биетийг тэр чигт нь дамжуулна
POST/api/generate-qr нээлттэй

МОФ форматын EMVCo QR-ын payload-ыг локалаар үүсгэнэ. Гадагш ямар ч дуудлага хийхгүй, ямар ч нэхэмжлэх бүртгэхгүй — зөвхөн мөр эвлүүлж CRC-г нь бодно.

bash
curl -X POST https://agent.epayment.mn/api/generate-qr \
  -H 'Content-Type: application/json' \
  -d '{
    "amount":         "1000.00",
    "merchantName":   "TEST MERCHANT",
    "billNumber":     "BILL-1",
    "storeLabel":     "STORE-1",
    "referenceLabel": "REF-1",
    "registerNO":     "1234567"
  }'
json
{
  "qrCode": "00020101021226540014A00000084300010108MOFUMNUB0220MN63009001009000000052049399530349654071000.005502025802MN5913TEST MERCHANT6011Ulaanbaatar62540106BILL-10307STORE-10505REF-1060712345670809MOFQRCODE7403GOV8002036304A000"
}

Үүсэх QR-ын тогтмол утгууд

TagУтгаТайлбар
0112Динамик QR
26.00A0000008430001Globally unique identifier
26.01MOFUMNUBAcquirer — Төрийн сан
26.02MN630090010090000000IBAN
529399Төрийн үйлчилгээний MCC
53496MNT
54таны amountМөр хэлбэрээр шууд тавигдана
58 / 60MN / UlaanbaatarУлс, хот
62.01billNumberНэхэмжлэхийн дугаар
62.03storeLabelСалбар / дэлгүүр
62.05referenceLabelЛавлах
62.06registerNOРегистр
62.08MOFQRCODEМОФ урсгалыг таних тэмдэг
74 / 80GOV / 03Merchant type, transaction type
Дутуу талбар чимээгүй өнгөрнө

Энэ endpoint дээр биетийн шалгалт ажилладаггүй. Талбар дутуу явуулбал 400 гарахгүй200-той хамт худалдагчийн нэр, нэхэмжлэхийн дугаар нь дутуу QR буцна. Ийм QR уншигдах ч төлбөр нь буруу газар очих, эсвэл таних боломжгүй болно.

Тиймээс зургаан талбарыг бүгдийг нь дүүргэж, буцсан qrCode-ын уртыг өөрийн талдаа шалгаж бай.

amount-ыг форматлаж өг

amount нь мөр бөгөөд QR-ын tag 54-д яг тэр хэвээрээ ордог. Сервер бөөрөнхийлөх, аравтын орон нэмэх зэрэг юу ч хийхгүй. МОФ-ийн жишгээр "1000.00" гэж хоёр орны нарийвчлалтай явуулахыг зөвлөнө.

GET/api/check?ref=<ref_num> X-Api-Key

tpay.gov.mn дээрх нэхэмжлэхийн төлвийг лавлана. Мэдэгдэл алдагдсан үед нөхөн тулгах зориулалттай.

bash
curl -H 'X-Api-Key: CLIENT_KEY' \
  'https://agent.epayment.mn/api/check?ref=0000093'

Боломжит хариунууд

СтатусБиетУтга
200 {"status":"paid", …} Төлөгдсөн.
200 {"status":"pending","deeplinks":[…],"qpay":{…}} Хараахан төлөгдөөгүй. Банкны deep link-үүд болон qPay-ийн хариу дагалдана.
404 {"status":"not_found", …} Ийм ref-тэй нэхэмжлэх олдсонгүй.
400 ref parameter is required ?ref= дамжуулаагүй.
422 {"status":"unknown"|"error", …} Хуудсаас төлөв ч, deep link ч танигдсангүй.
502 Failed to reach tpay.gov.mn Гадаад систем хариу өгсөнгүй (15 сек timeout).
Polling-ийн оронд ашиглахгүй

Энэ endpoint нь tpay.gov.mn-ийн HTML хуудсыг уншиж боловсруулдаг тул удаан бөгөөд гадны системээс хамаарна. Гүйлгээ бүр дээр давтан дуудах (polling) хэрэглээнд тохиромжгүй — үндсэн замаа notify event байлга, энэ endpoint-ыг зөвхөн эргэлзээтэй тохиолдол, өдрийн эцсийн тулгалтад хэрэглэ.

POST/api/notify нээлттэй

Банк / МОФ талаас дуудагддаг callback. Терминалын програм үүнийг дуудахгүй — гэхдээ юу болж байгааг ойлгоход хэрэгтэй тул баримтжуулав.

Хүсэлт хүлээж авмагц сервер дараах гурван зүйлийг хийнэ:

  1. tmean-ээс ST:<цифр>-ийг салгаж терминалыг тодорхойлно.
  2. EPay-ийн epay-to-customer-wall руу дамжуулж switch-д «paid» бүртгүүлнэ.
  3. <craccount>-<terminalId> room руу notify event түлхэнэ.
СтатусНөхцөл
200{"message":"Successfully","error":false,"wallResponse":…}
400tmean байхгүй, эсвэл ST:<цифр> / craccount олдсонгүй
2-р алхам бүтэлгүйтсэн ч 200 буцна

Wall руу дамжуулах алхам амжилтгүй болбол алдаа нь wallResponse.success = false болж биет дотор ирнэ, HTTP статус нь 200 хэвээр үлдэнэ. Мөн notify event нэгэн адил түлхэгдэнэ.

Түгээмэл алдаа#

АлдааҮр дагаварЗөв нь
transports: ['websocket'] гэж хатуу заасан Огт холбогдохгүй transports-ыг огт заахгүй орхи
Socket.IO-д https://…/api гэж холбогдсон Огт холбогдохгүй /api-гүй үндсэн хаяг өг
terminalId-д зөвхөн терминалын дугаар өгсөн Холбогдоно, гэхдээ мэдэгдэл хэзээ ч ирэхгүй <craccount>-<terminalId> бүтнээр нь өг
Терминалын дугаарыг 01 гэж тэглэсэн Room зөрж мэдэгдэл ирэхгүй ST:-ийн ард яг байгаа цифрийг нь ашигла
Гүйлгээ бүрийн дараа socket-оо хаадаг Дараагийн мэдэгдэл алга болно Холболтоо байнга нээлттэй барь
generate-qr-д талбар дутуу өгсөн 200, гэхдээ QR нь эвдэрсэн Зургаан талбарыг бүгдийг нь дүүрг
bank_refnum-аар дедупликац хийгээгүй Давхар баримт хэвлэгдэнэ Боловсруулсан bank_refnum-уудаа хадгал
wallResponse.success-ээр төлбөрийг үгүйсгэсэн Төлсөн үйлчлүүлэгч татгалзагдана notify ирсэн = төлбөр орсон

Оношилгоо#

1. Сервис амьд эсэх

bash
curl -i https://agent.epayment.mn/api
# 200 {"message":"Hello API"} → сервис амьд

2. Socket.IO хүрч байгаа эсэх

bash
curl 'https://agent.epayment.mn/socket.io/?EIO=4&transport=polling'
# 0{"sid":"…","upgrades":["websocket"],"pingInterval":25000,…}

Ийм хариу ирвэл сүлжээ, Cloudflare, ingress бүгд журамтай — асуудал таны клиентийн тохиргоонд байна (ихэвчлэн transports эсвэл URL).

3. Холбогдсон боловч мэдэгдэл ирэхгүй байна

Дараах дарааллаар шалга:

  1. socket.connected === true мөн үү? disconnect event-ээ лог руу бич.
  2. Холбогдохдоо өгсөн terminalId-гаа хэвлэ. Тэр нь <craccount>-<ST-ийн цифр> хэлбэртэй байна уу?
  3. Бодит гүйлгээний tmean-ийг банкнаас/МОФ-оос ав. Түүн доторх ST:-ийн ард байгаа цифр таны terminalId-ийн сүүлийн хэсэгтэй яг таарч байна уу?
  4. Таарахгүй байвал room зөрсөн байна — terminalId-гаа засаад дахин холбогд.
  5. Бүгд таарч байгаад ирэхгүй бол EPay-д хандаж, тухайн bank_refnum-ыг хэлж лог шалгуулна.
EPay талд юу харагддаг вэ

Мэдэгдэл ирээд room хоосон байвал сервер No active sockets for terminalId: <room> гэсэн анхааруулга үлдээдэг. Асуудал мэдүүлэхдээ гүйлгээний цаг, дүн, bank_refnum гурвыг хамт өгвөл хамгийн хурдан олдоно.

Нэвтрүүлэх өмнөх шалгалт#

  • Socket.IO клиент v4 эсэх.
  • transports заагаагүй (default) эсэх.
  • Socket.IO URL-д /api байхгүй эсэх.
  • terminalId нь <craccount>-<terminalId> хэлбэртэй, дансны дугаар нь бодитой эсэх.
  • Reconnect асаалттай, disconnect/connect_error логлогдож байгаа эсэх.
  • bank_refnum-аар дедупликац хийдэг эсэх.
  • Апп нээгдмэгц socket холбогдож, хаагдтал нээлттэй үлддэг эсэх.
  • Client key нь тохиргооноос ирдэг, эх кодод хатуу бичигдээгүй эсэх.
  • Эхлээд dev-agent.epayment.mn дээр туршсан эсэх.