# HolestPay Integration Guide — FrontCore > This guide is written for AI coding assistants. It describes how to implement HolestPay payment integration using the **FrontCore approach** — a JavaScript-only integration that does **not require a server-side signature** for the initial payment request. > Reference implementation: `hpay_frontcore_sample.html` > > **Sample data files — read these to understand real data structures:** > - `pos_as_read.json` — https://apps.holest.com/holest-pay/pos_as_read.json > Full `client.POS` (same as HPay.POS) object as returned by `HPayInit()`. Contains `payment`, `shipping`, and `fiscal` method arrays with all their properties (`Uid`, `HPaySiteMethodId`, `Name`, `Hidden`, `SubsciptionsType`, `POps`, `PayInputUrl`, `Use IFRAME`, etc.). > - `response_sample.json` — https://apps.holest.com/holest-pay/response_sample.json > Full `hpay_response` object as received in `onHPayResult` event and as POST-back `hpay_forwarded_payment_response`. Contains `payment_status`, `status`, `transaction_uid`, `transaction_user_info`, `vault_token_uid`, `vault_card_brand`, `vault_card_umask`, `vault_exp`, `payment_html`, `fiscal_html`, `integr_html`, `shipping_html`, and all other result fields. --- ## What is FrontCore? FrontCore is a HolestPay integration mode where: - The HPay script is **automatically loaded** from the payment server when the buyer's browser visits a **whitelisted origin** (domain). - **No Secret Key is needed on the frontend** — the payment request is signed internally by the HPay infrastructure based on the trusted origin. - The developer only needs the **Merchant Site UID** in the browser. - The **Secret Key is still required on your server** for backend charges (COF/MIT) , admin operations, and result signature verification. ### Recommendation Use FrontCore selectively. - For most production implementations, **Standard integration** should be the primary/default choice. - FrontCore is best when you need to connect a site quickly and start payment flow fast. - Even with FrontCore, production-grade backend verification, webhook idempotency, and secure server ownership are still required. --- ## Prerequisites 1. A configured HolestPay POS on `pay.holest.com` (production) or `sandbox.pay.holest.com` (sandbox). 2. All desired payment methods, fiscal methods, and shipping methods activated on the POS in the HPay panel. 3. In the HPay panel ? site/POS settings: - **Merchant Site UID** (`merchant_site_uid`) — **not** required as an input parameter to `HPayInit()` for FrontCore. It is automatically available as `client.MerchantsiteUid` after `HPayInit()` resolves, and must be saved for use in backend charge requests and admin operations. - **POS Secret Key** — needed only on your **server** (for charges, admin ops, result verification). - **Frontend Script-Core Origins** — add your site's domain (e.g. `yoursite.com` or `*.yoursite.com`) to the whitelist. **Without this, the FrontCore script will not load.** --- ## Pre-Implementation Client Questions (Ask Before Building) Before writing integration code, confirm the following with the client: - Do you want us to implement the bank-required footer logotypes strip (card logos, bank logos, 3DS logos) using HolestPay POS parameters, visible in the site footer on **all pages** (not only checkout)? - For Terms of Service, do you want to use the HolestPay-provided TOS page directly, compare/merge its clauses into your existing TOS page, or handle TOS in another way? - Do you require an `I accept Terms of Service` checkbox on checkout with a clickable Terms link (page link or modal)? - **Subscriptions / recurring billing** — Do you want to implement subscriptions (recurring charges using saved cards)? Important: automatic renewal charges (MIT — Merchant Initiated Transaction) require a merchant account at a bank that supports MIT. Subscriptions can still be charged manually without automatic MIT. Implement subscriptions **only if the user confirms**. See **Subscriptions Integration Recommendations** below. - **Admin order list / sync / edit** — Do you need your site admin (order list, order detail) to list, load, or update HolestPay orders via server API (`getOrders` / `getOrder` / `updateOrder`)? See **Admin Order REST API**. Requires Secret Key on the server; never call from the browser. - **Payment admin handlers** — Do you need server-side `queryRequest` / void / refund / capture (or fiscal/shipping/integration handlers) without relying only on the HPay `orderActions` UI? See **Payment / Module Backend Handlers**. - **Store / Pay-By-Link** — Do you need HolestPay fiscal/shipping/integration (or a Pay-By-Link) on orders that were **not** paid via HPay checkout? See **Store Order API** (`POST /clientpay/store`). --- ## Recommended Quick-Start from HPay Panel Before implementing from scratch, open HPay panel: - `PLATFORM MODULES` -> at the bottom use: - `Get HTML with embeded POS (selected POS) credentials (production requires server-side sigining)...` - `Get HPay-FrontCore HTML with embeded POS (selected POS) credentials (signing automatic, javascript-only implementable) ...` - Download generated sample files and deploy them to an HTTPS test location. - For FrontCore, explicitly add that HTTPS test location to `Frontend Script-Core Origins`; otherwise the FrontCore script will not load there. - Use these files for immediate end-to-end checks (form rendering, payment flow, event payloads, response format, order fields). - AI assistants and developers can inspect these generated samples during development to discover integration details that may not be fully documented. For this FrontCore guide, start from the FrontCore generated sample (based on `hpay_frontcore_sample.html`) and validate script loading + origin whitelist behavior first. --- ## Platform Clarifications (Important) - In HolestPay terminology, a `POS` means your website/app sales endpoint (web, Android, iOS, desktop), not a physical in-store terminal. - `sandbox` and `production` are intentionally isolated environments. Configure POS, methods, and credentials separately in each environment. - For status processing, treat HolestPay `status` format as canonical for order lifecycle across panel/API/webhooks: - `PAYMENT:` - optional fiscal/integration segments: `_FISCAL:` or `_INTEGR:` - optional shipping segments: `_SHIPPING:@` - Keep section order in composed status as: `PAYMENT` -> `FISCAL/INTEGRATION` -> `SHIPPING`. - Handle additional payment statuses beyond only paid/failed flows, especially `AWAITING`, `PAYING`, `RESERVED`, and `OBLIGATED`, depending on your business process. --- ## HolestPay Order Status Format ```shell [PAYMENT:payment_status][ (fmethod1_uid)_FISCAL:(fmethod1_status) [(fmethod2_uid)_FISCAL:(fmethod2_status)]...][ (imethod1_uid)_INTEGR:(imethod1_status) [(imethod2_uid)_INTEGR:(imethod2_status)]...][ (smethod1_uid)_SHIPPING:packet_no@shipping_status [(smethod2_uid)_SHIPPING:packet_no@shipping_status]...] ``` - ORDER OF SUB-STATUSES SECTIONS PAYMENT -> FISCAL & INTEGRATION -> SHIPPING IS IMPORTANT. - ORDER OF METHOD STATUSES WITHIN SAME SUB-STATUSES SECTION IS NOT IMPORTANT. - ONE AND ONLY ONE SPACE CHARACTER AS SUB-STATUSES SEPARATOR IS IMPORTANT. ```shell Possible payment status: SUCCESS (alias of PAID) PAID PAYING (partially paid, indicates all partial payments are on time; used for advance payments or multi-source payments) AWAITING (waiting bank transfer, for example) REFUNDED PARTIALLY-REFUNDED VOID OVERDUE RESERVED (amount is reserved but still not captured from buyer card) EXPIRED (used with methods that have expiration) OBLIGATED (same as AWAITING but when service delivery has started or there is legal means to guarantee payment will happen) REFUSED FAILED CANCELED ``` `PAYMENT:payment_status` may not exist if HolestPay payment module is not used and you do not set it explicitly. ```shell Possible fiscal module status: - varies depending on module ``` Fiscal/Integration statuses exist only if fiscal/integration modules add status and are executed. ```shell Possible packet shipping status: PREPARING - initial status if shipping address is OK; instructions can be submitted to courier from this status READY - used by some companies to indicate goods are checked and ready for courier submission SUBMITTED - request submitted to courier DELIVERY - under delivery DELIVERED - delivered ERROR - error in courier API request RESOLVING - shipping address (or something else) needs backend attention FAILED - delivery permanently failed, or courier API refused the request REVOKED - explicitly canceled by buyer or company ``` Shipping statuses exist only when packets are handled by HolestPay shipping modules. --- ## How It Works — Overview ``` HPay Server Browser (your site) Your Server | | | |-- auto-serve hpay.frontcore.js -->| | | (only for whitelisted origins) | | | | | |<-- HPayInit() --------------------| | |<-- presentHPayPayForm(request) ---| | | (no verificationhash needed) | | | | | |-- onHPayResult ------------------>| | | |-- verify & fulfil -------->| | | | | (server-to-server webhook) -------|----------> notify_url ---->| ``` The key difference from Standard: **no `verificationhash` field is needed** in the initial pay_request because the trusted origin acts as the authorization. --- ## Step 0 — Configure Origins and Get the FrontCore Script URL ### 1. Whitelist your domain/origin patterns In the HPay panel, under your POS/site settings, add allowed entries to **"Frontend Script-Core Origins"**. - Enter **one origin/pattern per line**. - `*` wildcard is supported. - Example pattern: `*holest.com/all-fontcore-tests/*` This allows matching subdomains and subfolders that satisfy the pattern. ### 2. Copy the FrontCore script tag from HPay panel After saving origins, the HPay panel outputs a **full ` ``` In this URL, the `07f689c5-5bc8-44b6-a563-8facc6870fab` segment is the **Merchant Site UID**. When this FrontCore script loads on an allowed origin, `HolestPayCheckout` and FrontCore signing (`hpay_frontend_script_core_sign`) become available. After loading, the global `HolestPayCheckout` object and `HPayInit()` become available. When the HPay script is loaded, it also exposes these globals on `window`: - `window.presentHPayPayForm` — function - `window.HPayIsSandbox` — environment flag variable Add a check to warn about misconfiguration (add after page loads): ```javascript setTimeout(function() { if (typeof HolestPayCheckout === 'undefined') { alert( 'HOLESTPAY FRONT-CORE SCRIPT IS NOT LOADED. ' + 'YOU PROBABLY FORGOT TO ADD CURRENT ORIGIN TO ' + '"Frontend Script-Core Origins" PARAMETER UNDER SITE/POS SETTINGS!' ); } }, 5000); ``` --- ## Step 1 — Initialize HPay and Fetch POS Configuration `HPayInit()` returns a Promise resolving to `client` (= global `HPay`). ```javascript HPayInit( language // string — e.g. "en", "rs", "de" — also optional. // If omitted, HPay will use the HTML attribute, // or fall back to the fixed language configured in HPay panel POS settings. // NOTE: merchant_site_uid and environment are NOT required for FrontCore — // they are determined automatically by the POS-specific script URL. ).then(async client => { // client.MerchantsiteUid — the Merchant Site UID read from the loaded POS config const merchantSiteUid = client.MerchantsiteUid; // save for use in charge_request / admin ops // client.POS.payment — array of payment method objects // client.POS.shipping — array of shipping method objects // client.POS.fiscal — array of fiscal method objects // Filter by buyer country (optional but recommended): let availablePayment = []; let availableShipping = []; const country = 'RS'; try { availablePayment = await HPay.availablePaymentMethods(country, orderAmount, orderCurrency); } catch(e) { console.error(e); } try { availableShipping = await HPay.availableShippingMethods(country, orderAmount, orderCurrency); } catch(e) { console.error(e); } // Build payment method selector from client.POS.payment: client.POS.payment.forEach(pm => { if (!pm.Hidden && (!availablePayment.length || availablePayment.find(m => m.Uid == pm.Uid))) { // pm.HPaySiteMethodId — use as pay_request.payment_method value // pm.Name — display name // pm.SubsciptionsType — contains "cof" or "mit" if card saving is supported // pm.POps — available backend operations e.g. "charge,refund" // pm.PayInputUrl — set means docking (embedded form) is supported // pm['Use IFRAME'] — if false, method uses redirect flow } }); // Build shipping method selector from client.POS.shipping: if (client.POS.shipping) { client.POS.shipping.forEach(sm => { if (!sm.Hidden && (!availableShipping.length || availableShipping.find(m => m.Uid == sm.Uid))) { // sm.HPaySiteMethodId — use as pay_request.shipping_method value } }); } }); ``` --- ## Step 2 — Build the `pay_request` Object ```javascript const pay_request = { // NOTE: merchant_site_uid is NOT included in pay_request for FrontCore. // The POS-specific script authenticates the request automatically. hpaylang: "en", // optional UI language order_uid: "20260315-621417", // required — technical unique order ID (maps to order.Uid) order_name: "#Order 204", // optional — order label (order.Name): shop order number, NOT product name — products go in order_items[].name order_amount: "15000", // required order_currency: "RSD", // required ISO 4217 payment_method: "179", // required pm.HPaySiteMethodId shipping_method: "45", // optional sm.HPaySiteMethodId order_user_url: "https://yoursite.com/thanks", // optional redirect/thank-you URL notify_url: "https://yoursite.com/webhook", // optional public webhook URL order_sitedata: { // optional — custom site metadata -> order.Data.sitedata (no order_data on pay_request; store API uses order_data for PBL) id: 11260, customer_id: 1, source: "checkout-web" }, cof: "optional", // optional: optional|required|none vault_token_uid: "saved-token-uuid", // optional on pay_request: 'new'|1|true to save card; existing token for logged-in fast checkout (presentHPayPayForm) — NOT for server MIT; use /clientpay/charge for MIT // Optional billing — stored on HolestPay order as order.Data.billing (not order.Billing) order_billing: { email: "customer@example.com", first_name: "TEST", last_name: "TEST", phone: "+38111111111", is_company: 0, company: "", // company legal name company_tax_id: "", // company tax ID in merchant's country company_reg_id: "", // company registration ID in merchant's country address: "TEST", // street name address2: "", // recommended for street number / address addition city: "Beograd", country: "RS", state: "Beograd", postcode: "11000", lang: "sr_RS" // language from merchant platform/system }, // Optional shipping — stored on HolestPay order as order.Data.shipping (not order.Shipping) order_shipping: { shippable: false, is_cod: 1, // set to 1 when COD logic is used/allowed first_name: "", last_name: "", phone: "", company: "", address: "", // street name address2: "", // recommended for street number / address addition city: "", country: "", state: "", postcode: "" }, // Optional line items — stored on HolestPay order as order.Data.items (not order.Items) order_items: [ { posuid: 114, // merchant's own internal item ID (string or number) type: "product", name: "Sample product name", sku: "000550", qty: 1, price: 4695.99, subtotal: 4695.99, refunded: 0, refunded_qty: 0, tax_label: "", tax_amount: 0, length: "", width: "", height: "", weight: "", split_pay_uid: "", virtual: true, tax_percent: 0 } ] }; // NOTE: For initial FrontCore checkout, do not send signature hash. // Remove empty fields: Object.keys(pay_request).forEach(k => { if (pay_request[k] === '') delete pay_request[k]; }); ``` **Important — `order_items` naming must use HolestPay keys (do not pass raw platform keys):** - Use `name`, not `title` - Use `posuid`, not `variantId` - Use `qty`, not `quantity` - `subtotal` is mandatory for each item line - `posuid` can be any identifier from the merchant's system (SKU/variant/product/internal DB ID) - **Product name / title** → `order_items[].name` (→ `order.Data.items[].name`). **Do not** put product names in `order_name` — that field is the **order** label (`order.Name`), e.g. `#Order 204`. Common Shopify mapping before send: - `variantId -> posuid` - `title -> name` - `quantity -> qty` - top-level `items -> order_items` The same order payload structure is used across all three markups/docs (Lovable prompt, FrontCore guide, Standard guide). Address convention recommendation: use `address` for street name and `address2` for street number/additional address details. ### Request/response vs HolestPay order object — key mapping `pay_request` / `charge_request` / `hpay_response` use **different field names** than the HolestPay order object (`getOrder`, webhook `order`, admin API). Do not copy request keys onto the order root. | Request/response field | HolestPay order object path | Notes | |---|---|---| | `order_uid` | `Uid` | Technical unique order ID — signatures, webhooks, `/clientpay/charge` | | `order_name` | `Name` | **Order** label (e.g. `#Order 204`, `Narudžbina 18`) — **not** product name; products use `order_items[].name` → `Data.items[].name` | | `order_amount` | `Amount` | | | `order_currency` | `Currency` | | | `order_sitedata` | `Data.sitedata` | Custom site metadata from your platform (e.g. internal order id, customer_id) — **not** `order_data` | | **`order_items`** | **`Data.items`** | Line items array — **not** `order.Items` at root | | **`order_billing`** | **`Data.billing`** | Billing address/contact — **not** `order.Billing` at root | | **`order_shipping`** | **`Data.shipping`** | Shipping address/contact — **not** `order.Shipping` at root | When reading `HPay.getOrder(...)` or webhook `order`, access billing/items/shipping/sitedata as `ord.Data.billing`, `ord.Data.items`, `ord.Data.shipping`, `ord.Data.sitedata`. When sending **checkout** `pay_request`, use top-level `order_billing`, `order_items`, `order_shipping`, `order_sitedata` — HPay maps them into `Data` on the stored order. **There is no `order_data` field on checkout `pay_request`.** (For server **`POST /clientpay/store`**, optional `order_data` merges into top-level `order.Data` — used for Pay-By-Link; see **Store Order API**.) Set `order_name` from your **shop order number** (what buyers see as "your order #204"). **Do not** put a product name, item title, or catalog description in `order_name` — that belongs in `order_items[].name` for each line item. ### Full Template Field Catalog (Standalone) - `Top-level pay_request` (FrontCore — `merchant_site_uid` is **not** included here): - `hpaylang`, `order_uid`, `order_name`, `order_amount`, `order_currency` - `payment_method`, `shipping_method`, `order_user_url`, `notify_url` - `order_sitedata`, `cof`, `vault_token_uid` - `order_billing`: - `email`, `first_name`, `last_name`, `phone` - `is_company`, `company` (legal name), `company_tax_id` (tax ID), `company_reg_id` (registration ID) - `address`, `address2`, `city`, `country`, `state`, `postcode`, `lang` - `order_shipping`: - `shippable`, `is_cod` - `first_name`, `last_name`, `phone`, `company` - `address`, `address2`, `city`, `country`, `state`, `postcode` - `dispenser`, `dispenser_desc`, `dispenser_method_id` (locker/paket-shop flows) - `order_items[]`: - `posuid`, `type`, `name`, `sku`, `qty`, `price`, `subtotal` - Required minimum per line for reliable processing: `posuid`, `name`, `qty`, `subtotal` - `refunded`, `refunded_qty`, `tax_label`, `tax_amount` - `length`, `width`, `height`, `weight`, `split_pay_uid`, `virtual`, `warehouse` - `setPaymentMethodDock(...)` data: - `order_amount`, `order_currency`, `monthly_installments`, `vault_token_uid`, `hpaylang`, `cof` - Signature input/result fields used for backend charge and verification: - `transaction_uid`, `status`, `order_uid`, `order_amount`, `order_currency`, `vault_token_uid`, `subscription_uid`, `rand` - Compare generated signature with response `vhash` (not request `verificationhash`). - `Extensibility`: - `order_sitedata` may contain any custom key/value pairs; persisted to `order.Data.sitedata`. Other keys under `order.Data` (module statuses, exchange rates, etc.) are added by HPay — do not set them via request. - `order_billing` and `order_shipping` may include additional custom fields besides the listed ones. - Total serialized order payload should stay below **64 KB**. --- ## Step 3 — Present the Payment Form ```javascript // Option A: Modal HPay.presentHPayPayForm(pay_request); // Option B: Docked (embedded) — only if pm.PayInputUrl is set const dockElement = document.getElementById('paymentMethodDock'); HPay.setPaymentMethodDock( pay_request.payment_method, { order_amount: pay_request.order_amount, order_currency: pay_request.order_currency, monthly_installments: null, vault_token_uid: pay_request.vault_token_uid || null, hpaylang: pay_request.hpaylang, cof: pay_request.cof }, dockElement ); // Trigger payment on Pay button click: HPay.presentHPayPayForm(pay_request); ``` Dock container CSS: ```css #paymentMethodDock { background: #ffffff9e; } ``` --- ## Step 4 — Handle the Result Events Use these event handlers on your FrontCore page: ```javascript document.addEventListener('onHPayResult', function(e) { const r = e.hpay_response; if (!r) return; if (r.error && r.error.code) { HPay.presentHPayPayForm(pay_request); // retry return; } if (/PAID|RESERVED|SUCCESS|PAYING|OBLIGATED|AWAITING/i.test(r.payment_status)) { // r.payment_html, r.fiscal_html, r.integr_html, r.shipping_html — HTML receipts // r.transaction_user_info — object with card/transaction details // r.order_user_url — redirect to thank-you page if needed // IMPORTANT: AWAITING/OBLIGATED are NOT failed; show r.payment_html on thank-you page // IMPORTANT: clear cart for PAID/RESERVED and also for PAYING/AWAITING/OBLIGATED. // window.location.href = r.order_user_url; if (r.vault_token_uid) { // IMPORTANT: save to your database linked to the user account! const saveCardData = { vault_token_uid: r.vault_token_uid, vault_card_brand: r.vault_card_brand, vault_card_umask: r.vault_card_umask, vault_exp: r.vault_exp, vault_scope: r.vault_scope, // terminal/routing scope — which terminals token can be used on vault_onlyforuser: r.vault_onlyforuser, // 1 = presentHPayPayForm only; 0 = presentHPayPayForm and /clientpay/charge pay_method_uid: r.pay_method_uid }; // TODO: POST saveCardData to your backend and store in DB } } // r.status — order status (always present) }); document.addEventListener('onHPayPanelClose', function(e) { const r = e.hpay_response; // null if closed without completing const reason = String((r && r.reason) || '').toLowerCase(); // If pay button was locked during payment start, unlock it on close reasons below. if (/^(user|timeout|cancel|error)$/.test(reason)) { const payBtn = document.getElementById('do-pay'); if (payBtn) payBtn.disabled = false; } }); document.addEventListener('onHPayOrderOpExecuted', function(e) { // admin operation result }); ``` ### Thank-You Page Rendering Rule (Mandatory) On the thank-you page, always render: 1) `transaction_user_info` block first, 2) `payment_html`, `fiscal_html`, `shipping_html`, `integr_html` blocks. Bank production approval also requires a complete order summary on this page (for all outcomes: success, failed, awaiting payment): - buyer/customer identity data - billing and email data - shipping data - order number - ordered products list with quantity, unit price, and line totals - shipping cost - payment method name and shipping method name - grand total amount If any field is missing in response, keep the block visible and show a placeholder message. Keys in `transaction_user_info` can be translated for UI labels, but values should be shown exactly as received. ```javascript function renderTransactionUserInfo(info, keyMap) { const host = document.getElementById('hpay-transaction-user-info'); if (!host) return; host.innerHTML = ''; if (!info || typeof info !== 'object' || !Object.keys(info).length) { host.innerHTML = '

Transaction details are not available.

'; return; } const dl = document.createElement('dl'); Object.entries(info).forEach(([k, v]) => { const dt = document.createElement('dt'); const dd = document.createElement('dd'); dt.textContent = keyMap[k] || k; // translate keys only dd.textContent = String(v ?? ''); // do not alter values dl.appendChild(dt); dl.appendChild(dd); }); host.appendChild(dl); } renderTransactionUserInfo(r.transaction_user_info, { 'Order UID': 'Order UID', 'Payment Status': 'Payment Status', 'Transaction Time': 'Transaction Time', 'Amount in order currency': 'Amount in order currency', 'Amount in payment currency': 'Amount in payment currency', 'Bank Account': 'Bank Account', 'REF MOD97 PNB': 'REF MOD97 PNB', 'Purphose': 'Purpose' }); const FALLBACK_HTML = '

Not available for this payment.

'; document.getElementById('hpay-receipt-payment').innerHTML = r.payment_html || FALLBACK_HTML; document.getElementById('hpay-receipt-fiscal').innerHTML = r.fiscal_html || FALLBACK_HTML; document.getElementById('hpay-receipt-shipping').innerHTML = r.shipping_html || FALLBACK_HTML; document.getElementById('hpay-receipt-integr').innerHTML = r.integr_html || FALLBACK_HTML; ``` Example payload: ```json { "transaction_user_info": { "Order UID": "NIPI-1777290504591-8X6YD2", "Payment Status": "AWAITING", "Transaction Time": "2026-04-27 13:48:27.847Z", "Amount in order currency": "1360.00 RSD", "Amount in payment currency": "1360.00 RSD", "Bank Account": "160-6000002552312-02", "REF MOD97 PNB": "(97) 2726042713", "Purphose": "Order npinipi17772905045918x6yd2" } } ``` --- ## Result Verification — Webhook (Notify URL) HPay calls webhook URL via HTTP POST JSON (server-to-server). If `notify_url` is sent in request payload, it overrides panel setting: - `I(P|S|F|I)N - Link za instant notifikacije - plaćanje/isporuka/fiskal/integracije` - `I(P|S|F|I)N - Instant payment/shipping/fiscal/integation notification url` HPay appends query string parameter `topic` and sends POST JSON. Supported `topic` values: - `payresult`: same payload shape as `onHPayResult` (`e.hpay_response`) / `https://apps.holest.com/holest-pay/response_sample.json`. - `orderupdate`: root contains at least `order_uid`, `status`, `vhash`; payload fields are the same as `payresult`; and includes `order` object in the same format as `ord` from `HPay.getOrder(order_uid).then(ord => {...})` (`https://apps.holest.com/holest-pay/hpay_order_sample.json`) (`"id"` is ID from HPay system); verify `vhash` the same way. - `posconfig-updated`: contains POS config (`HPay.POS` compatible + non-public fields), `environment`, and `checkstr = md5(${merchant_site_uid}${secret_token})`. Important payload mapping note: - Request and response payloads are similar in shape; response usually contains request fields plus normalization and additional result fields. - Hash field names are different by direction: request -> `verificationhash`, response -> `vhash`. - `order` payload is a different model (same as `HPay.getOrder(...)` `ord` object), so do not map it as request/response. - Example key differences (request/response field → HolestPay order object): - `order_uid` → `Uid` - `order_name` → `Name` (**order** label / shop order number — **not** product name; use `order_items[].name` for products) - `order_amount` → `Amount` - `order_currency` → `Currency` - **`order_sitedata` → `Data.sitedata`** (custom site metadata — no `order_data` on checkout `pay_request`; see Store API for PBL `order_data`) - **`order_items` → `Data.items`** (line items array — nested under `Data`, not at order root) - **`order_billing` → `Data.billing`** (billing address/contact — nested under `Data`, not at order root) - **`order_shipping` → `Data.shipping`** (shipping address/contact — nested under `Data`, not at order root) - fiscal method specific data -> `order.FiscalData[fiscal_method_uid] = {...}` (module-specific payload) - integration method data -> `order.Data[integr_method_uid] = {...}` (may exist or may be empty/missing) - shipping method data -> `order.ShippingData[real_or_temp_shipping_code] = { method_uid: shipping_method_uid, ... }` The same payment result may arrive from browser callback and webhook. Use `vhash` (with order/transaction IDs) for idempotency to avoid duplicate processing. ### Redirect Method POST-back Note (Bank Redirect Flows) For redirect methods, HPay may return result to `order_user_url` with auto-submitted form: ```html
``` Robust parse fallback example: ```javascript function parseForwardedHPayResponse(raw) { if (!raw) return null; if (typeof raw === 'object') return raw; try { return JSON.parse(raw); } catch (_) {} const unescaped = String(raw).replace(/\\\\\"/g, '"').replace(/\\\\\\\\/g, '\\'); try { return JSON.parse(unescaped); } catch (_) {} return null; } ``` ## Step 5 — Verify Response `vhash` on Server (Node.js) Rule of thumb: - request to HPay (`pay_request` / `charge_request`) -> field `verificationhash` -> generate with `generatePOSRequestSignature(...)` - response from HPay (browser callback / webhook) -> field `vhash` -> validate with `verifyHPayResponse(...)` ```javascript const crypto = require('crypto'); const md5 = require('md5'); function generatePOSRequestSignature(merchant_site_uid, secretKey, payload) { const amount = Number(payload.order_amount ?? 0).toFixed(8); const src = String(payload.transaction_uid ?? '').trim() + '|' + String(payload.status ?? '').trim() + '|' + String(payload.order_uid ?? '').trim() + '|' + amount + '|' + String(payload.order_currency ?? '').trim() + '|' + String(payload.vault_token_uid ?? '').trim() + '|' + String(payload.subscription_uid ?? '').trim() + String(payload.rand ?? '').trim(); const srcMd5 = md5(src + merchant_site_uid); return crypto.createHash('sha512').update(srcMd5 + secretKey).digest('hex').toLowerCase(); } function verifyHPayResponse(result, merchant_site_uid, secretKey) { if (!result || !result.vhash) return false; if (!result.order_uid || !String(result.order_uid).trim()) return false; const expected = generatePOSRequestSignature(merchant_site_uid, secretKey, { transaction_uid: result.transaction_uid ?? '', status: result.status ?? '', order_uid: result.order_uid, order_amount: result.order_amount ?? 0, order_currency: result.order_currency ?? '', vault_token_uid: result.vault_token_uid ?? '', subscription_uid: result.subscription_uid ?? '', rand: result.rand ?? '' }); return expected === String(result.vhash).toLowerCase(); } ``` --- ## Backend Charge (Server-Server MIT) — `/clientpay/charge` **Do not use buyer checkout (`presentHPayPayForm(pay_request)`) for server-initiated billing.** A saved `vault_token_uid` appears in both flows, but how you trigger payment is different. FrontCore does not expose the Secret Key on the frontend, but **server-server charges still require the Secret Key on your server**. ### `presentHPayPayForm(pay_request)` vs `/clientpay/charge` — critical distinction | | **`presentHPayPayForm(pay_request)`** (CIT / buyer checkout) | **`/clientpay/charge`** (MIT / server-server) | |---|--------------------------------------------------------------|-----------------------------------------------| | **Who initiates** | Buyer at checkout (browser) | Your server — cron, subscription renewal job, merchant admin action | | **Buyer present** | Yes — HPay payment UI opens from `presentHPayPayForm` | No — silent charge, no card form, no redirect | | **`vault_token_uid`** | Optional on `pay_request` — see below | **Required** on `charge_request` — token from DB (`user_vault_token`) | | **`cof`** | May be set (`optional` / `required` / `none`) | Omit — not used | | **`order_user_url`** | Thank-you / redirect URL | Omit — no buyer to redirect | | **Typical trigger** | Checkout Pay button → `presentHPayPayForm(pay_request)` | Server `fetch` to `/clientpay/charge` only | **`vault_token_uid` in `pay_request` (buyer checkout — not MIT):** - `vault_token_uid: 'new'` (or `1` / `true`) — buyer saves a new card during this payment (first subscription CIT, or card-save-only with amount `0`). - `vault_token_uid: 'Ij0BZTRhgkuhDG2nqSg8IQ'` (existing token) — **logged-in buyer** chose one of their saved `user_vault_token` records in checkout UI for **faster payment** instead of typing card again. Still CIT via **`presentHPayPayForm(pay_request)`** — **not** server-server MIT. **`/clientpay/charge` (server-server MIT):** - Subscription renewal cron, merchant billing from admin, any charge **without** the buyer at the keyboard. - Uses the same `vault_token_uid` value stored after the first successful payment (e.g. `"Ij0BZTRhgkuhDG2nqSg8IQ"`), but must call **`/clientpay/charge`** — never `presentHPayPayForm` for renewals. `charge_request` has the same field structure as `pay_request`. Omit `order_user_url` and `cof`. Sign with the **same** `generatePOSRequestSignature` as `pay_request`. **POST body shape:** wrap the signed payload as `{ "request_data": charge_request }` (not the raw `charge_request` alone). ```javascript // Node.js backend — SERVER ONLY const crypto = require('crypto'); const md5 = require('md5'); function generatePOSRequestSignature(merchant_site_uid, secretkey, request) { const amt = parseFloat(request.order_amount || 0).toFixed(8); let cstr = String(request.transaction_uid || '').trim() + '|'; cstr += String(request.status || '').trim() + '|'; cstr += String(request.order_uid || '').trim() + '|'; cstr += String(amt).trim() + '|'; cstr += String(request.order_currency || '').trim() + '|'; cstr += String(request.vault_token_uid || '').trim() + '|'; cstr += String(request.subscription_uid || '').trim(); cstr += String(request.rand || '').trim(); const md5hash = md5(cstr + merchant_site_uid); return crypto.createHash('sha512').update(md5hash + secretkey).digest('hex').toLowerCase(); } const MERCHANT_SITE_UID = 'YOUR-MERCHANT-SITE-UID'; const SECRET_KEY = 'YOUR-POS-SECRET-KEY'; const BASE_URL = 'https://sandbox.pay.holest.com'; // or https://pay.holest.com const charge_request = { merchant_site_uid: MERCHANT_SITE_UID, hpaylang: 'en', order_uid: '20260823-519442', // technical ID (order.Uid) order_name: '#Order 18', // order label (order.Name) — order number, NOT product name order_amount: '15000', order_currency: 'RSD', payment_method: '841', vault_token_uid: 'Ij0BZTRhgkuhDG2nqSg8IQ', // from user_vault_token — /clientpay/charge (server MIT, e.g. subscription renewal) only; not presentHPayPayForm order_sitedata: { source: 'subscription-renewal' }, // optional -> order.Data.sitedata // same optional fields as pay_request (billing, shipping, items, notify_url, etc.) // omit order_user_url: no user exists to be redirected in backend charge flow // omit cof: not applicable for backend charge }; charge_request.verificationhash = generatePOSRequestSignature( MERCHANT_SITE_UID, SECRET_KEY, charge_request ); const result = await fetch(BASE_URL + '/clientpay/charge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ request_data: charge_request }), }).then(r => r.json()); // Verify result vhash before fulfilling: // const valid = verifyHPayResponse(result, MERCHANT_SITE_UID, SECRET_KEY); if (/PAID|PAYING|RESERVED/.test(result.payment_status)) { // success } ``` PHP equivalent: ```php function generatePOSRequestSignature($merchant_site_uid, $secretkey, $request) { $amt = number_format((float)($request['order_amount'] ?? 0), 8, '.', ''); $cstr = trim($request['transaction_uid'] ?? '') . '|'; $cstr .= trim($request['status'] ?? '') . '|'; $cstr .= trim($request['order_uid'] ?? '') . '|'; $cstr .= trim($amt) . '|'; $cstr .= trim($request['order_currency'] ?? '') . '|'; $cstr .= trim($request['vault_token_uid'] ?? '') . '|'; $cstr .= trim($request['subscription_uid'] ?? ''); $cstr .= trim($request['rand'] ?? ''); return hash('sha512', md5($cstr . $merchant_site_uid) . $secretkey); } $charge_request['verificationhash'] = generatePOSRequestSignature( $merchant_site_uid, $secret_key, $charge_request ); $body = json_encode(['request_data' => $charge_request]); $ch = curl_init($base_url . '/clientpay/charge'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], // for simplicity only - do not disable in production: CURLOPT_SSL_VERIFYHOST => 0, CURLOPT_SSL_VERIFYPEER => false, ]); $result = json_decode(curl_exec($ch), true); curl_close($ch); ``` > **Common mistake:** implementing cron / admin / subscription renewal by calling **`presentHPayPayForm(pay_request)`** (or otherwise simulating buyer checkout). Renewals are **server MIT** — use **`/clientpay/charge`** with `{ request_data: charge_request }`. --- ## Subscriptions Integration Recommendations Ask the user whether they want subscriptions before implementing any of this. If they decline, skip this section entirely. ### Bank / MIT prerequisite - Automatic renewal charges (MIT) require a **merchant account at a bank that supports MIT**. - Subscriptions can still be charged **manually** (operator-triggered charge) even without MIT support. - Only payment methods where `pm.SubsciptionsType` contains `"cof"` or `"mit"` and `pm.POps` contains `"charge"` support this flow. - FrontCore checkout itself does not need the Secret Key, but **every MIT / backend charge still requires the Secret Key on your server**. ### Terms of Service If subscriptions are offered, the site Terms of Service / purchase conditions **must** state that this option exists and describe, from the **buyer's perspective**, what they can expect (e.g. how renewals work, how to cancel, how saved cards are used, when charges happen). ### First payment (CIT) — create or reuse a vault token via `presentHPayPayForm` For the initial customer-initiated (CIT) subscription payment, use **`presentHPayPayForm(pay_request)`** (buyer checkout), not `/clientpay/charge`. Set in `pay_request`: ```javascript vault_token_uid: 'new' // save a new card token // OR, if the buyer already has a saved card from a previous checkout: vault_token_uid: 'existing-token-uuid' // logged-in buyer picked user_vault_token — still presentHPayPayForm (CIT), not MIT ``` Renewals and merchant-initiated billing use **`/clientpay/charge`** (see Backend Charge section above). ### Successful CIT response — vault fields to persist After a successful CIT payment, the response typically includes: > **`vault_token_uid`** is reused in two ways: > - **`presentHPayPayForm(pay_request)`** — logged-in buyer selects a saved `user_vault_token` at checkout (user/site-initiated CIT; buyer present). > - **`/clientpay/charge`** — server-server MIT only (cron renewal, merchant admin); only `vault_token_uid` is required in `charge_request`. > **Store all `vault_*` fields** in `user_vault_token` — `vault_card_brand`, `vault_card_umask`, and `vault_exp` let the buyer recognize the card and let you show/warn about expiry; `vault_scope` and `vault_onlyforuser` help scope and manage tokens. > **`vault_onlyforuser`:** `1` = token usable only with **`presentHPayPayForm`** (logged-in saved-card checkout); `0` = usable with **`presentHPayPayForm`** and **`/clientpay/charge`** (server MIT). For subscriptions / renewal charges, persist tokens with `0`. > **`vault_scope`:** e.g. `"13IN002780"` — useful when you use **many payment methods and routing**; indicates **which terminal(s)** the token can be applied on. Store it with the token and match `payment_method` / terminal when reusing in `pay_request` or `charge_request`. ```javascript { "vault_token_uid": "-BuyA0LCCB1dEoXxX8OCeIg", // reuse in pay_request (presentHPayPayForm) or charge_request (/clientpay/charge, e.g. subscription renewal) "vault_card_brand": "VISA", // store — card identification in account UI "vault_card_umask": "432484***0474", // store — masked PAN for buyer "vault_exp": "10/27", // store — expiry display / warnings "vault_scope": "13IN002780", // store — terminal/routing scope; which terminals token applies to "vault_onlyforuser": 0 // 1 = presentHPayPayForm only; 0 = presentHPayPayForm and /clientpay/charge — subscriptions need 0 } ``` ### Data model — `user_vault_token` and subscription reference - Persist all `vault_*` fields in a structure such as `user_vault_token`, keyed to the customer, together with the payment method identifier (`payment_method` / `pay_method_uid`). - A customer may have **multiple** `user_vault_token` records. - The **subscription** object should store a **reference** to the specific `user_vault_token` used for that subscription. - If an MIT charge fails and the customer has multiple tokens, try them **in order** until a `charge_request` succeeds. - The customer should be able to: - choose a **default** vault token when they have more than one; - **remove** a vault token if they no longer want that card charged; - choose, **per subscription**, which of their `user_vault_token`s to use (when they have multiple tokens and/or multiple subscriptions). ### Card-save only (amount `0`) A CIT payment with `order_amount: "0"` and `vault_token_uid: 'new'` may be used to **save a new card without charging**. This zero-amount save applies only when the request has `vault_token_uid: 'new'`. ### Site-owned renewals — do not rely on HolestPay `subscription_uid` This recommended implementation **does not** use the HolestPay subscription object (`subscription_uid`). HolestPay will **not** create renewals by itself. The site only uses `vault_token_uid` to perform charges. Your site must implement: 1. Logic that checks whether it is time to renew. 2. Creation of a renewal order (or equivalent renewal object with a unique identifier). 3. Building a `charge_request` from that renewal and calling the backend charge endpoint (server-side, with Secret Key). ### Fiscalization with subscription renewals If HolestPay fiscalization is used with subscriptions, successful responses will simply include fiscal receipt data when fiscal receipts are configured to be created automatically. The system should still listen to webhook `orderupdate` for renewals, because fiscal receipts may be created later (fiscal temporarily unavailable, or the merchant configured fiscal printing on click instead of automatic). --- ## Admin Operations (Requires Secret Key — Backend or Admin Page Only) For admin pages, use the **normal HPay script URL**, not the FrontCore handler script: ```html ``` Do not use `.../frontend-script-core.js` for admin tooling. ```javascript // Use the 4th parameter of HPayInit to enable admin mode HPayInit( merchant_site_uid, language, environment, secret_key // 4th param — enables admin/backend operations ).then(client => client.loadHPayUI()) .then(() => { HPay.getOrder(order_uid).then(ord => { // `ord` format sample: https://apps.holest.com/holest-pay/hpay_order_sample.json const toolbox = document.getElementById('admin_toolbox'); toolbox.innerHTML = ''; [HPay.POS.payment, HPay.POS.fiscal, HPay.POS.shipping].forEach(methods => { (methods || []).forEach(pm => { if (pm.initActions) { if (typeof pm.initActions === 'string') eval('pm.initActions = ' + pm.initActions); pm.initActions(); } if (pm.orderActions) { if (typeof pm.orderActions === 'string') eval('pm.orderActions = ' + pm.orderActions); const actions = pm.orderActions(ord); if (actions && actions.length) { const h6 = document.createElement('h6'); h6.innerHTML = pm.SystemTitle; toolbox.appendChild(h6); actions.forEach(action => { if (action.Run) { const btn = document.createElement('button'); btn.innerHTML = action.Caption; btn.addEventListener('click', e => { e.preventDefault(); action.Run(ord); }); toolbox.appendChild(btn); } else if (action.actions) { const p = document.createElement('p'); p.innerHTML = action.Caption; toolbox.appendChild(p); action.actions.forEach(sub => { const sbtn = document.createElement('button'); sbtn.innerHTML = sub.Caption; sbtn.addEventListener('click', e => { e.preventDefault(); sub.Run(ord); }); p.appendChild(sbtn); }); } }); } } }); }); }); }); ``` --- ## Admin Order REST API (Site Admin Backend) Use this for the **administrative part of the merchant site** (order list, order detail, editing order fields on your own admin pages). These endpoints are **server-side only** — they require the POS Secret Key and must never be called from the browser. FrontCore checkout does not replace this; admin REST still needs the Secret Key on your server. Reference sample: `hpay_orders_api_sample_nodejs.js`. For payment sync / void / refund / capture see **Payment / Module Backend Handlers** and `hpay_payment_queryRequest_sample_nodejs.js`. **Interactive handbook (try live calls):** https://apps.holest.com/holest-pay/hpay_common_backend_api_calls.html — enter POS credentials, try `store` / Pay-By-Link preview / `queryRequest` / `getOrders` / `getOrder` / `updateOrder` / void / refund / capture and inspect METHOD, URL, headers, body, and response (PHP / Node.js / C# code tabs). Browser Try may hit CORS; the logged request still matches what your server should send. HolestPay can also render a full admin UI via `orderActions` (normal `hpay.js` + secret); manual REST/handler calls are still commonly needed for custom backends and automation. Order object format (response / update target) is the HolestPay order model — same as `HPay.getOrder(order_uid)` / webhook `orderupdate.order`, sample: `https://apps.holest.com/holest-pay/hpay_order_sample.json`. Do **not** confuse it with `pay_request` / `hpay_response` field names (`order_uid` vs `Uid`, `order_amount` vs `Amount`, `order_billing` vs `Data.billing`, etc.). ### Auth Every call is signed with the same algorithm as `generatePOSRequestSignature` / charge requests. Send signature in **HTTP headers** (not in the body for GET): - `rand` — unique random string per request - `verificationhash` — signature over `transaction_uid|status|order_uid|order_amount(8 decimals)|order_currency|vault_token_uid|subscription_uid` + `rand` then `sha512( md5(cstr + merchant_site_uid) + secret_key )` For `getOrder` / `updateOrder`, include `order_uid` in the signed fields. For list (`getOrders`), signed fields may be empty except `rand`. ### Endpoints Base host: `https://sandbox.pay.holest.com` or `https://pay.holest.com` **1) List orders — `getOrders`** ``` GET /clientpay/orders/{merchant_site_uid}?offset=0&limit=50&filter={...}&sort_order={...} Headers: rand, verificationhash ``` `filter` and `sort_order` are JSON strings, same format as HolestPay admin UI / API, e.g.: ```javascript // filter={"CreatedAt":{"op":"between","value":["2026-05-03T00:00:00","2026-08-10T23:59:59"]}} // sort_order=[["id","DESC"]] const filter = { CreatedAt: { op: 'between', value: [fromLocalDateTime, toLocalDateTime], }, }; const sort_order = [['id', 'DESC']]; ``` Response may be a bare array or an object wrapping an array under keys such as `items`, `Orders`, `orders`, `data`, `result`, `Results`. Prefer reading `Uid` (or `order_uid`) from each item. **2) Get one order — `getOrder`** ``` GET /clientpay/orders/{merchant_site_uid}/{order_uid} Headers: rand, verificationhash Sign fields: { order_uid } ``` **3) Update order — `updateOrder`** ``` POST /clientpay/orders/{merchant_site_uid}/{order_uid}/update Headers: rand, verificationhash, Content-Type: application/json Sign fields: { order_uid } Body: { "Order": { /* partial HolestPay order fields to merge */ } } ``` Example — set billing note: ```javascript const updateBody = { Order: { Data: { billing: { note: 'Updated via API', }, }, }, }; ``` Send only fields you want to change under `Order`. Nested paths follow the order model (`Data.billing`, `Data.shipping`, `Data.items`, `FiscalData[...]`, `ShippingData[...]`, etc.). ### Minimal Node.js pattern ```javascript function hpaySign(fields, merchantSiteUid, secretToken) { const amount = Number(fields.order_amount != null ? fields.order_amount : 0).toFixed(8); const src = String(fields.transaction_uid || '').trim() + '|' + String(fields.status || '').trim() + '|' + String(fields.order_uid || '').trim() + '|' + amount + '|' + String(fields.order_currency || '').trim() + '|' + String(fields.vault_token_uid || '').trim() + '|' + String(fields.subscription_uid || '').trim() + String(fields.rand || '').trim(); const md5 = crypto.createHash('md5').update(src + merchantSiteUid, 'utf8').digest('hex'); return crypto.createHash('sha512').update(md5 + secretToken, 'utf8').digest('hex').toLowerCase(); } // GET list / get one: // headers = { rand, verificationhash: hpaySign({ ...signFields, rand }, uid, secret) } // POST update: // headers = { rand, verificationhash, 'Content-Type': 'application/json; charset=utf-8' } ``` ### When to use vs `HPay.getOrder` / `orderActions` HolestPay can **easily render a full admin interface** for order detail or order list (toolbox from `pm.orderActions` after normal `hpay.js` + `HPayInit(..., secret_key)` + `HPay.getOrder`). That is often the fastest path for refunds, fiscal, shipping, etc. Integrators **frequently still need manual server-to-server calls** — custom admin pages, cron jobs, sync-after-bank, or automation without loading `hpay.js`. Use: | Need | Approach | |------|----------| | Ready-made admin UI (order detail / list actions) | Normal `hpay.js` (not FrontCore) + `HPayInit(..., secret_key)` + `HPay.getOrder` + `pm.orderActions` | | Own server-side order list / detail / edit | Admin Order REST API above (`getOrders` / `getOrder` / `updateOrder`) | | Order not paid via HPay checkout, but need fiscal / shipping / integration / Pay-By-Link | **`POST /clientpay/store`** below | | Payment sync / void / refund / capture from your backend | **Payment module handlers** below (`queryRequest`, `voidRequest`, …) | | Same pattern for fiscal / integration / shipping modules | `POST /clientpay/handlers/{type}/...` (see below) | --- ## Store Order API (`POST /clientpay/store`) + Pay-By-Link Use **`/clientpay/store`** when orders are **not** created through an HPay payment method on the frontend (`presentHPayPayForm`), but you still need HolestPay **fiscal / integration / shipping** on those orders (and optionally a **Pay-By-Link**). FrontCore checkout does not create these orders for you — this is a **server-side** call with the Secret Key. After create/update via `/store`, the order exists in HolestPay like any other — you get full **admin rendering** (`orderActions` on normal `hpay.js`) and can call module handlers such as **`defaultAction`** directly. Reference sample: `hpay_store_order_sample_nodejs.js` Interactive try: https://apps.holest.com/holest-pay/hpay_common_backend_api_calls.html (`store` panel + Pay-By-Link checkbox / URL preview) ### Endpoint and upsert behavior ``` POST /clientpay/store Body: { "request_data": { /* full order fields + rand + verificationhash */ } } ``` - **Creates** when `order_uid` is new; **updates** when that `order_uid` already exists. - Payload shape is a full order `request_data` object (same idea as plugin `store_order`): `merchant_site_uid`, `order_uid`, `order_name`, `order_amount`, `order_currency`, `payment_method`, `shipping_method`, `order_items`, `order_billing`, `order_shipping`, `order_sitedata`, optional `order_data`, plus `rand` + `verificationhash` **inside** `request_data` (same signature algorithm as charge / pay). - For externally paid / unpaid-via-HPay orders, samples often use `payment_method: '0'` and `shipping_method: '0'` until HPay modules are applied. - **Note:** checkout `pay_request` uses `order_sitedata` → `order.Data.sitedata`. On `/store`, optional **`order_data`** merges into **`order.Data`** (used for Pay-By-Link flags and other top-level Data keys). Do not confuse the two. Minimal Node.js shape: ```javascript const requestData = { merchant_site_uid: MERCHANT_SITE_UID, order_uid: 'API-STORE-...', order_name: '#API-STORE-...', order_amount: '1500.00', order_currency: 'RSD', payment_method: '0', shipping_method: '0', order_items: [ /* ... */ ], order_billing: { /* ... */ }, order_shipping: { /* ... */ }, order_sitedata: { id: '...', payment_method_id: 'external', /* ... */ }, // optional Pay-By-Link — see below: // order_data: { is_pbl: 1, pbl_offer_days: 7, allow_pm: [...], allow_sm: [...], pbl_sms: 0 }, }; requestData.verificationhash = generatePOSRequestSignature(MERCHANT_SITE_UID, SECRET_KEY, requestData); await fetch(BASE_URL + '/clientpay/store', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ request_data: requestData }), }); ``` ### Pay-By-Link (optional) You can enable Pay-By-Link for an order created via `/store`, or for an **already existing** HolestPay order (by storing/updating with PBL fields). Set these under **`order_data`** so they land on HolestPay **`order.Data`**: ```javascript order_data: { is_pbl: 1, pbl_offer_days: 7, // required for P-B-L — link validity in days allow_pm: [ // optional — allowed payment method UIDs; omit = all allowed 'nbsipspt_rs', 'upc_redirect', 'nestpay_redirect', 'nestpay_3d_mit', 'nestpay_redirect_halk', 'cpay_redirect', 'method_udev', 'cardbinrouter', 'manual', 'raiaccept_redirect', 'ipgtas_redirect', 'sia_redirect', 'nbs_ips_skeniraj_pt', 'quipu_redirect', 'banktransferrs-2', 'sia_api', ], allow_sm: [ // optional — allowed shipping method UIDs; omit = all allowed 'manual', ], pbl_sms: 0, // optional 1|0 — SMS with link (Serbian mobile numbers only) } ``` On the stored order these appear as `order.Data.is_pbl`, `order.Data.pbl_offer_days`, `order.Data.allow_pm`, `order.Data.allow_sm`, `order.Data.pbl_sms`. ### Building the Pay-By-Link URL Same logic as `HPay.getPayByLinkForOrderUid(order_uid)`: ```javascript function addChecksum27(str) { str = String(str); let sum = 0; for (let i = 0; i < str.length; i++) sum += str.charCodeAt(i); return str + String(sum % 27).padStart(2, '0'); } function getPayByLinkForOrderUid(baseUrl, hpaySiteId, orderUid) { const uid = String(orderUid || '').replace(/^PBL/, ''); return `${String(baseUrl).replace(/\/$/, '')}/pbl/${hpaySiteId}-${addChecksum27(uid)}`; } // Example: https://sandbox.pay.holest.com/pbl/{HPaySiteId}-{order_uid}{checksum27} // HPaySiteId comes from POS config (often POS.HPaySiteId / site id used in panel) ``` Try store + PBL preview in the interactive handbook: https://apps.holest.com/holest-pay/hpay_common_backend_api_calls.html --- ## Payment / Module Backend Handlers (Server-to-Server) Besides order list/get/update, HolestPay exposes **module handler** endpoints for payment, integration, fiscal, and shipping. Merchants often call these manually from their admin backend the same way they call `queryRequest`. FrontCore checkout does not replace this — handlers still need the **Secret Key on your server**. ### URL pattern (all module types) ``` POST /clientpay/handlers/{type}/{merchant_site_uid}/{module_uid}/{order_uid}/{method} ``` `{type}` is one of: `payment` | `integration` | `fiscal` | `shipping`. Examples: - Payment: `.../handlers/payment/{merchant}/{payment_method_uid}/{order_uid}/queryRequest` - Fiscal: `.../handlers/fiscal/{merchant}/{fiscal_module_uid}/{order_uid}/defaultAction` (method names vary by module) `module_uid` / `payment_method_uid` is the method identifier from POS (e.g. `pm.Uid` or site method id as used by handlers). ### Auth for handlers (signature in JSON body) Unlike Orders Admin API (where `rand` + `verificationhash` go in **HTTP headers**), payment/module handlers put signature fields in the **JSON body**: - `order_uid` — required - `rand` — unique per request - `verificationhash` — same algorithm as `generatePOSRequestSignature` / charge (sign over `order_uid` + `rand`, etc.) Reference sample: `hpay_payment_queryRequest_sample_nodejs.js` **Interactive handbook:** https://apps.holest.com/holest-pay/hpay_common_backend_api_calls.html — try `store` / Pay-By-Link, payment handlers, and Orders Admin API calls live (sandbox/production), with request log and PHP / Node.js / C# samples. ```javascript // SERVER ONLY const body = { order_uid: orderUid }; body.rand = `rnd${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`; body.verificationhash = generatePOSRequestSignature(merchantSiteUid, SECRET_KEY, body); const url = `${BASE_URL}/clientpay/handlers/payment/` + `${encodeURIComponent(merchantSiteUid)}/` + `${encodeURIComponent(paymentMethodUid)}/` + `${encodeURIComponent(orderUid)}/queryRequest`; const result = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }).then(r => r.json()); ``` Bank/gateway raw responses differ by acquirer. After `queryRequest`, call **`getOrder`** to read the current HolestPay order status. Status changes typically fire an **`orderupdate`** webhook to the site `I(P|S|F|I)N` notify URL when configured. ### Standard payment module methods **1) `queryRequest`** — ask the bank gateway for the latest payment state and sync it into HolestPay (may trigger `orderupdate`). Body: `order_uid`, `rand`, `verificationhash`. **2) `syncOnPOS`** — explicitly triggers an `orderupdate` webhook (without necessarily re-querying the bank the same way as `queryRequest`). Body: `order_uid`, `rand`, `verificationhash`. **Requires** the panel setting `I(P|S|F|I)N - Instant payment/shipping/fiscal/integation notification url` (SR: `I(P|S|F|I)N - Link za instant notifikacije - plaćanje/isporuka/fiskal/integracije`) to be configured on the POS/site. Without that URL, HolestPay does not know where to send the webhook and `syncOnPOS` cannot deliver the notification. **3) `voidRequest`** — full payment cancel **before** bank clearing. Body: `order_uid`, `rand`, `verificationhash`. **4) `refundRequest`** — refund money to the buyer. Body (required): `order_uid`, `rand`, `verificationhash`. Optional for **partial** refund: - `total` — amount in **order currency** - If you track line items: `order_items` array; per item set `refunded_qty` and `refunded` (amounts in order currency) so HolestPay knows which items the refund applies to **5) `captureRequest`** — post-authorize a previously **RESERVED** amount (capture after pre-auth). Body (required): `order_uid`, `rand`, `verificationhash`. Optional for **partial** capture (not the full reserved amount): - `total` — amount in **order currency** - If you track line items: `order_items` with `captured_qty` and `captured` (order currency) per item ### Fiscal and integration modules — `defaultAction` Fiscal and integration modules expose a **`defaultAction`** handler that runs whatever that module is expected to do by default for the order: ``` POST /clientpay/handlers/fiscal/{merchant_site_uid}/{fiscal_module_uid}/{order_uid}/defaultAction POST /clientpay/handlers/integration/{merchant_site_uid}/{integr_module_uid}/{order_uid}/defaultAction ``` Example: for the **efiscal** module, `defaultAction` is an alias for **`createSale`** (issue the fiscal sale receipt). **Unlike payment modules**, fiscal and integration modules do **not** share a fixed set of standard method names across all modules of that type — each module is different (`createSale`, module-specific ops, etc.). If you must call specific non-default methods from your own server code, **consult Holest support** for that module’s available endpoints. **Simplest recommendation for almost all admin backends:** do **not** hand-wire every fiscal/integration/shipping action. Use the **built-in HPay admin UI** (normal `hpay.js` + `HPayInit(..., secret_key)` + `HPay.getOrder` + `pm.orderActions`) which renders **all available operations** for the current order depending on payment / fiscalization / integration / shipping status — the same toolbox pattern as on the HPay panel. Call raw handlers (`defaultAction`, `queryRequest`, …) only when you need automation without that UI (cron, batch, custom API). Shipping modules follow the same handler URL pattern; available method names also vary by carrier module — prefer `orderActions` unless Holest support documents a specific method for your case. If the merchant uses a **WAF / firewall**, whitelist **`pay.holest.com`** (and `sandbox.pay.holest.com` for tests) so browser scripts, server API calls, and webhook delivery are not blocked — see CSP / WAF section. --- ## Difference Summary: FrontCore vs Standard | Feature | FrontCore | Standard | |---------|-----------|----------| | Script source | Auto-served from HPay server (whitelisted origin) | Manual `