HTTP API
Machine-readable specification: /openapi.yaml (OpenAPI 3.1), Postman collection: /postman_collection.json (variables baseUrl, adminKey).
Buyer
| Route | Description |
|---|---|
GET /pay/:orderId?c=<confirmation>&p=<provider>&lang=uk|pl|en | Creates (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/:token | Payment 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
| Route | Verification |
|---|---|
POST /webhooks/mono[/<MONO_WEBHOOK_SECRET>] | ECDSA X-Sign |
POST /webhooks/monoparts · privatparts · liqpay · wayforpay · novapay · p24 · payu · stripe | each gateway’s own signature — see the gateway page |
POST /webhooks/shopify | HMAC-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.
| Route | Description |
|---|---|
GET /api/np/cities?q=Ki | settlement search (Address.searchSettlements), 10-min cache |
GET /api/np/warehouses?city=<ref>&q=12 | branches 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 add | For whom |
|---|---|
Theme app extension extension/mistok-installments — theme editor: Product → Add block → Apps → “Mistok — pay in parts”, set the Mistok base URL | you 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>¤cy=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).
| Role | Can |
|---|---|
| 👑 owner | everything: settings, users, sandbox, audit |
| 🛠️ manager | operations: sync, reminders, pay links, phone rules, QR cash registers, receipts, event redelivery; reads the audit log; no settings or users |
| 👁️ viewer | pages 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.
| Page | What's there |
|---|---|
📊 /admin/dashboard?days=7|30|90 | KPIs, 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|90 | analytics 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/payments | search (order, phone, email, invoice), filters status / paid / PSP / country / dates, pagination, /admin/payments.csv export |
🧾 /admin/order/:id | order 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/settings | payment links, phone rules, outgoing events log, QR registers, settings overview |
JSON and actions (?key=ADMIN_KEY or cookie)
| Route | Description |
|---|---|
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/qr | JSON |
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|return | actions |
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/redeliver | QR registers, redeliver an event |
GET /status/:orderId | raw 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
- Headers:
X-Mistok-Event(event name),X-Mistok-Delivery(unique delivery id for de-duplication on your side),X-Mistok-Signature. - Success is a 2xx response within 8 s. Otherwise the same body with the same id is retried after 1, 5, 30, 120 and 720 minutes.
- Delivery log for 30 days —
/admin/events(state, attempts, HTTP code, error, "redeliver" button); JSON —/admin/api/events. - Signature check (Node):
crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex') === req.get('X-Mistok-Signature').
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.