Mistok
Documentation

HTTP API

Quick start newArchitectureGatewaysHTTP APIPrepayment and rulesFiscal receiptsNotifications and CRMPayment linksAnalytics newPolandNative Shopify gatewaySecurity new

Machine-readable specification: /openapi.yaml (OpenAPI 3.1), Postman collection: /postman_collection.json (variables baseUrl, adminKey).

Buyer

RouteDescription
GET /pay/:orderId?c=<confirmation>&p=<provider>&lang=uk|pl|enCreates (or reuses) an invoice and redirects to the payment page. c is the order’s confirmation number (protection against enumeration). Rate limit 30 per 5 min per IP.
GET /l/:tokenPayment link for a draft order. 96-bit token.
GET /return/:orderId?c=…Return from the PSP: status sync → Order status page. For mono installments — a waiting page.
GET /api/order/:orderId?c=…JSON for the Order status block: {orderName, provider, status: paid|pending|failed|…, prepay, amount, currency, fiscalUrl, payUrl}. CORS *, read-only.

Webhooks

RouteVerification
POST /webhooks/mono[/<MONO_WEBHOOK_SECRET>]ECDSA X-Sign
POST /webhooks/monoparts · privatparts · liqpay · wayforpay · novapay · p24 · payu · stripeeach gateway’s own signature — see the gateway page
POST /webhooks/shopifyHMAC-SHA256 X-Shopify-Hmac-Sha256; topics: orders/create, refunds/create, orders/cancelled, orders/edited, orders/fulfilled, orders/paid; dedup by webhook id

Responses: 200 — accepted; 400 — bad signature/body (the gateway does not retry); 500 — temporary error (the gateway retries).

Nova Poshta branch on the Thank-you page

Pickup points inside Shopify Checkout are Plus-only (Functions). Mistok moves the choice to the Thank-you page: the mistok-np-pickup block appears when the shipping method is Nova Poshta, the buyer searches a city and a branch / parcel locker, and the choice is written to the order as custom attributes (Нова Пошта: місто, Нова Пошта: відділення, np_warehouse_ref), tag np-branch, a note and an np_branch analytics event — CRM/logistics pick the branch ref up for the waybill.

RouteDescription
GET /api/np/cities?q=Kisettlement search (Address.searchSettlements), 10-min cache
GET /api/np/warehouses?city=<ref>&q=12branches and parcel lockers of a city (Address.getWarehouses), filter by number/street
POST /api/order/:id/np {c, cityRef, cityName, warehouseRef, warehouseName, type}save the choice (order confirmation code required); CORS for the extension

Variable: NP_API_KEY (NP business cabinet). 60 requests/min per IP. Without a key the block simply does not render.

“From ₴X/mo” badge on the product page

Shoppers see installments before the cart: the block shows the lowest monthly payment and, expanded, every program — 🐈‍⬛ mono “Purchase in parts” and 💚 PrivatBank “Payment in parts” — with the number of parts and a “no overpayment” / “buyer pays the fee” tag. The price follows the selected variant.

How to addFor whom
Theme app extension extension/mistok-installments — theme editor: Product → Add block → Apps → “Mistok — pay in parts”, set the Mistok base URLyou have a Partner app (shopify app deploy)
Snippet extension/installments-snippet.liquid → Themes → Edit code → Snippets, then in main-product.liquid: {% render 'mistok-installments', product: product, base: 'https://pay.example.com' %}no Partner app, 5 minutes

Data: GET /api/installments/quote?amount=<minor units>&currency=UAH → { best, offers[], providers[] } (CORS *, cached 5 min, 240 req/min per IP). Offers are built only from configured providers and within the banks' limits (mono 500–100 000 ₴, Privat 300–50 000 ₴); each part is rounded up to a whole hryvnia, the last part is the remainder. If the backend is unreachable the block shows the fallback parts count from its settings, or hides.

Variables: MONOPARTS_PARTS (e.g. 3,4,6,9), PRIVATPARTS_WIDGET_PARTS (e.g. 2,4,10; else PRIVATPARTS_PARTS), INSTALLMENTS_PARTS / INSTALLMENTS_PROVIDERS to narrow what is shown, INSTALLMENTS_OFF=1 to disable, MONOPARTS_MIN_UAH/MONOPARTS_MAX_UAH for your contract limits.

Owner admin

🧪 Sandbox: on an empty database Mistok seeds fictional payments and buyers (#D…) so the dashboard and lists are not blank; they are never sent anywhere. Switch off and purge in Settings → “Disable and delete test data”, or DEMO_DATA=false.

Sign in at /admin/login with ADMIN_PASSWORD (12-hour session cookie, HttpOnly, SameSite=Lax; 5 failed attempts lock for 15 min). Optional ADMIN_2FA=telegram — after the password a code arrives in the owner's Telegram. Scripts and the API keep working with ?key=ADMIN_KEY.

👥 Users, roles and the audit log

Several people in the admin, each with their own login (/admin/users, owner only). Passwords are stored as scrypt hashes; deactivating a user kills their session at once. 2FA: with ADMIN_2FA=telegram the code goes to the user's Telegram chat id (or TELEGRAM_CHAT_ID). The owner + ADMIN_PASSWORD login from .env stays as a break-glass (disable with ADMIN_ENV_LOGIN=off; then at least one active owner must exist in the DB). ?key=ADMIN_KEY = owner (logged as api-key).

RoleCan
👑 ownereverything: settings, users, sandbox, audit
🛠️ manageroperations: sync, reminders, pay links, phone rules, QR cash registers, receipts, event redelivery; reads the audit log; no settings or users
👁️ viewerpages and CSV exports only; action buttons hidden, POST → 403

🧾 Audit log (/admin/audit, CSV, /admin/api/audit): every POST action, login, logout, failed login and permission denial — with login, role, target (order, user), safe form fields (amounts, phones, settings keys — never passwords), IP and the resulting HTTP status. Filters: text, user, action, dates. Kept for AUDIT_KEEP_DAYS (365) days.

PageWhat's there
📊 /admin/dashboard?days=7|30|90KPIs, funnel, drop-off points with amounts, trend, countries 🇺🇦🇵🇱🇩🇪 / regions / cities, providers with sparklines, devices, shares, weekday × hour heatmap, “stuck today”, anomalies
📈 /admin/analytics?days=7|30|90analytics module: KPIs vs the previous period, 📣 traffic sources (UTM / referrer / Shopify source) with conversion and AOV, 🎯 campaigns, 🔁 new vs repeat buyers, 🏆 top buyers, 📦 top products and basket size, ⏱️ time-to-pay, 🔔 reminder recovery, 📅 revenue by weekday, 🌍×💳 country × provider matrix, 🧑‍🤝‍🧑 retention cohorts, insights; /admin/analytics.csv, /admin/api/analytics
💳 /admin/paymentssearch (order, phone, email, invoice), filters status / paid / PSP / country / dates, pagination, /admin/payments.csv export
🧾 /admin/order/:idorder card: payment, buyer, event timeline, fiscal receipts (PDF), refunds, KeyCRM link, outgoing events; actions: sync, pay link, remind now, amount onto a QR register, re-check receipt
🔗 /admin/links · 📵 /admin/phones · 📤 /admin/events · ▦ /admin/qr · ⚙️ /admin/settingspayment links, phone rules, outgoing events log, QR registers, settings overview

JSON and actions (?key=ADMIN_KEY or cookie)

RouteDescription
GET /admin/api/stats?days=N · /admin/api/payments?q=&status=&provider=&country=&from=&to=&paid= · /admin/api/events/:orderId · /admin/api/events · /admin/api/qrJSON
POST /admin/sync/:orderId · POST /admin/remind/:orderId · POST /admin/refund-retry/:refundId · POST /admin/fiscal/:orderId/poll · GET /admin/fiscal/:orderId.pdf?type=sale|returnactions
POST /admin/links (form or JSON: title, amount, email, phone, provider | draftId)create a payment link; with Accept: application/json returns {url, token, draft, amount}
POST /admin/phone-rules (phone, mode=prepay|block|trusted|delete, note)phone rules
POST /admin/qr/:qrId/show {orderId} · POST /admin/qr/:qrId/clear · POST /admin/events/:id/redeliverQR registers, redeliver an event
GET /status/:orderIdraw record

Outgoing events (CRM / Zapier / your own backend)

ON_EVENT_WEBHOOK_URL receives a POST application/json with the header X-Mistok-Signature = hex HMAC-SHA256(body, ON_EVENT_WEBHOOK_SECRET).

{
  "event": "paid",              // paid | prepaid | failed | expired | reversed | refunded | cancelled | reminder_sent | fiscalized
  "at": "2026-10-31T12:00:00.000Z",
  "order": { "id": "5551112223", "gid": "gid://shopify/Order/5551112223", "name": "#1001", "email": "b@x.ua", "phone": "+380501234567" },
  "payment": { "provider": "mono", "externalId": "2410…", "status": "success", "amount": 125000, "currency": "UAH",
               "prepay": false, "paidAt": "2026-10-31T11:59:58.000Z", "fiscalUrl": "https://cabinet.tax.gov.ua/cashregs/check?…" }
}

Amounts are in the currency’s minor units (kopiykas, groszy, cents). Event filter — ON_EVENT_WEBHOOK_EVENTS=paid,refunded.

Delivery and retries

How this differs from Workflow in GetCheckout v2: there you get checkout-funnel events (page view, abandoned cart, coupon, place_order, purchase) without a signature and with 2 retries; Mistok emits post-order payment lifecycle events (paid, failed, refunded, reversed, fiscal receipt, reminder) with an HMAC signature and 5 retries. The pre-payment funnel is covered by Mistok's own analytics.