# HolestPay FrontCore Integration — Guide for Lovable / AI Code Generators > **Full technical reference:** https://apps.holest.com/holest-pay/AI_INTEGRATION_GUIDE_FRONTCORE.md.txt > **Working sample (sandbox):** https://apps.holest.com/holest-pay/test/sandbox-164-hpay-frontcore-sample.html > (View page source and inspect `` tags for real implementation examples.) > > **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.). **Read this to know what payment/shipping methods look like before building any selector UI.** > - `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. **Read this to know what data is available when displaying the thank-you / order confirmation page.** --- ## ?? FIRST ACTION — Ask the User for Their FrontCore Script URL **Before writing any code, ask the user:** > *"Please give me your HolestPay FrontCore script URL. It looks like this:* > `` > *Do you have it?"* **If the user says they don't have it, or doesn't know what it is, explain:** > *"This is a unique ` ``` The UID segment in the URL (`07f689c5-...`) is the Merchant Site UID — it is embedded in the URL itself. **If the user confirms they need backend implementation** (admin actions, backend charges — see pre-build questions above), they must also provide: - **Merchant Site UID** — visible in HPay panel ? Site Settings (also in the FrontCore script URL) - **Merchant Site Secret Key** — visible in HPay panel ? Site Settings (keep this server-side only, never expose on frontend) ### ?? Configuration values must be easily changeable by the user Store all three of these as **user-editable configuration** (environment variables, a settings screen, or a clearly marked config file) — **never hardcode them**: | Value | Where used | Why it changes | |---|---|---| | FrontCore script `src` URL | Frontend, global ` ``` When the HPay script is loaded, it also exposes these globals on `window`: - `window.presentHPayPayForm` — function - `window.HPayIsSandbox` — environment flag variable Add a startup check to catch misconfiguration early: ```javascript // After page load — warn developer if FrontCore failed to initialize setTimeout(function() { if (typeof HolestPayCheckout === 'undefined') { console.error( 'HolestPay FrontCore script did not load. ' + 'Check that the current origin is listed in "Frontend Script-Core Origins" in the HPay panel.' ); } }, 5000); ``` --- ### Step 2 — Initialize HPay and Load Payment/Shipping Methods Call `HPayInit()` once when the checkout page loads (or when the user reaches it). It returns a Promise. - `merchant_site_uid` and environment (`sandbox`/`production`) are **not** parameters — they come from the script URL automatically. - Language is optional — if omitted, HPay uses the `` attribute or the language set in HPay panel POS settings. ```javascript // Save this after HPayInit so it can be used in charge_request and admin ops later let hpayMerchantSiteUid = null; HPayInit(/* language optional, e.g. 'en' */).then(async client => { hpayMerchantSiteUid = client.MerchantsiteUid; // Filter available methods by buyer's country (recommended) const country = 'RS'; // use actual buyer country, ISO 3166-1 alpha-2 let availablePayment = [], availableShipping = []; try { availablePayment = await HPay.availablePaymentMethods(country, orderAmount, orderCurrency); } catch(e) {} try { availableShipping = await HPay.availableShippingMethods(country, orderAmount, orderCurrency); } catch(e) {} // Populate payment method selector client.POS.payment.forEach(pm => { if (!pm.Hidden && (!availablePayment.length || availablePayment.find(m => m.Uid == pm.Uid))) { // Add pm to your UI selector // pm.HPaySiteMethodId ? use as pay_request.payment_method // pm.Name ? display name (use this — do NOT invent a name) // pm.SubsciptionsType ? contains "cof" or "mit" ? card saving is supported for this method // pm.POps ? e.g. "charge,refund" ? backend operations available // pm.PayInputUrl ? if set, this method supports docking (embedded payment input) // pm['Use IFRAME'] ? if false, method uses redirect flow (bank page POST redirect) } }); // Populate shipping method selector (if POS has shipping configured) 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 // sm.Name ? display name } }); } }); ``` > **Make sure existing payment methods in your UI have a name and description.** Use `pm.Name` from the POS config — do not hardcode or invent names. --- ### Step 3 — Build the `pay_request` and Initiate Payment ```javascript const pay_request = { // NOTE: merchant_site_uid is NOT included here for FrontCore — the script handles auth automatically hpaylang: 'en', // optional order_uid: 'ORDER-20260425-001', // 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.00', // required — string, in smallest currency unit or decimal order_currency: 'RSD', // required — ISO 4217 payment_method: '179', // required — pm.HPaySiteMethodId from Step 2 shipping_method: '45', // optional — sm.HPaySiteMethodId order_user_url: 'https://yoursite.com/thanks', // recommended — redirect/thank-you page URL notify_url: 'https://yoursite.com/webhook', // optional — server-to-server webhook (must be publicly reachable) order_sitedata: { // optional — custom site metadata -> order.Data.sitedata (no order_data on pay_request; store API uses order_data for PBL) id: 204, customer_id: 1, }, cof: 'optional', // optional — 'optional'|'required'|'none' — enables card saving vault_token_uid: '', // 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 order_billing: { // optional — maps to order.Data.billing on HPay order 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 }, order_shipping: { // optional — maps to order.Data.shipping on HPay order 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: '' }, order_items: [ // strongly recommended — maps to order.Data.items on HPay order { 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, tax_label: '', tax_amount: 0, length: '', // always in centimeters (cm) width: '', // always in centimeters (cm) height: '', // always in centimeters (cm) weight: '', // always in grams (g) split_pay_uid: '', virtual: true, tax_percent: 0 } ] }; // Remove empty fields before sending Object.keys(pay_request).forEach(k => { if (pay_request[k] === '') delete pay_request[k]; }); // Initiate payment — shows modal or triggers redirect depending on payment method HPay.presentHPayPayForm(pay_request); ``` **Important — `order_items` field naming must match HolestPay template 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`. If source platform fields are Shopify-style, map them explicitly before sending: - `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`) — **not** product name; products use `order_items[].name` → `Data.items[].name` | | `order_amount` | `Amount` | | | `order_currency` | `Currency` | | | `order_sitedata` | `Data.sitedata` | Custom site metadata (e.g. platform 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`. **There is no `order_data` field on checkout `pay_request`.** (For server **`POST /clientpay/store`**, optional `order_data` → top-level `order.Data` 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. --- ### Step 4 — Docking (Embedded Payment Input) Some payment methods support an embedded payment input directly in the page (e.g. card form inline). Check `pm.PayInputUrl` — if it is set, docking is supported for that method. Call `HPay.setPaymentMethodDock()` **when the user selects a payment method or when any checkout field changes** (amount, currency, etc.). ```html
``` ```css #paymentMethodDock { background: #ffffff9e; } ``` ```javascript function updateDock() { const selectedPm = /* pm object for selected payment method */; if (selectedPm && selectedPm.PayInputUrl) { 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 }, document.getElementById('paymentMethodDock') ); } else { document.getElementById('paymentMethodDock').innerHTML = ''; } } ``` --- ### Step 5 — Handle Results #### A — Event handler (iframe / modal methods) Most payment methods return the result via this event, fired on the current page: ```javascript document.addEventListener('onHPayResult', function(e) { const r = e.hpay_response; if (!r) return; if (r.error && r.error.code) { // Payment error — retry HPay.presentHPayPayForm(pay_request); return; } if (/PAID|RESERVED|SUCCESS|PAYING|OBLIGATED|AWAITING/i.test(r.payment_status)) { // Payment completed or pending payment instructions (AWAITING/OBLIGATED are NOT failed) // Clear cart for PAID/RESERVED and also for PAYING/AWAITING/OBLIGATED. // Display HTML receipts in dedicated containers on your thank-you page: // r.payment_html ? payment receipt HTML // r.fiscal_html ? fiscal receipt HTML (if fiscal module active) // r.integr_html ? integration module HTML // r.shipping_html ? shipping label/receipt HTML (if shipping module active) // r.transaction_user_info ? object with card brand, masked number, etc. if (r.vault_token_uid) {//only if card save is on for some reason subscription or simply token save for faster checkout // IMPORTANT: save this to your database linked to the user account — do NOT only store in browser const saveCardData = { vault_token_uid: r.vault_token_uid, //e.g. hJ0khUi67856765rtyrytrytry 12 up to 128 characters vault_card_brand: r.vault_card_brand, // e.g. "MASTERCARD" vault_card_umask: r.vault_card_umask, // e.g. "544358******4639" vault_exp: r.vault_exp, // e.g. "12/26" 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 — subscriptions need 0 pay_method_uid: r.pay_method_uid //Uid of payment method from HPay.POS.payment[N].Uid }; // POST saveCardData to your backend } // Redirect to thank-you page if needed: // window.location.href = r.order_user_url; } // r.status ? order status (always present) }); document.addEventListener('onHPayPanelClose', function(e) { const r = e.hpay_response; // null if user 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; } if (r && r.reason === '' && /* user wants retry */ false) { setTimeout(() => HPay.presentHPayPayForm(pay_request), 300); } }); ``` #### B — POST-back redirect (redirect methods) Some payment methods (e.g. certain bank integrations) require a **full page redirect to the bank**. `HPay.presentHPayPayForm()` handles this automatically with a POST redirect. After the bank interaction, the user is returned to `order_user_url` via a POST-back. On `order_user_url` (your thank-you/return page), read the result from the POST body: ```javascript // Server-side (Node.js example) // The POST body contains: hpay_forwarded_payment_response = JSON string const result = JSON.parse(req.body.hpay_forwarded_payment_response); // result is the same structure as e.hpay_response from onHPayResult ``` > Implement **both** the event handler (Step 5A) and the POST-back handler (Step 5B). Which one fires depends on the payment method the buyer chooses — you cannot predict it at build time. #### C — Webhook topics, panel URL, and `notify_url` override If server-to-server notifications are needed, configure 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` If request payload includes `notify_url`, it overrides panel-level `I(P|S|F|I)N` URL for that request. HPay appends query string parameter `topic` and sends POST JSON. `topic` can be: - `payresult` — same shape as `onHPayResult` / `https://apps.holest.com/holest-pay/response_sample.json` - `orderupdate` — root includes `order_uid`, `status`, `vhash`; payload fields are the same as `payresult`; and has `order` property with HolestPay 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); validate `vhash` the same way as `payresult` - `posconfig-updated` — contains POS object (`HPay.POS` format plus 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 result may arrive from browser callback and webhook; use `vhash` (plus order/transaction IDs) to prevent duplicate processing. #### D — Redirect POST-back parse robustness For redirect methods, HPay can return result via: ```html
``` If `JSON.parse` fails on first try, normalize escaped payload and retry: ```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 6 — Display HTML Receipts on Thank-You Page On the thank-you page, always render data in this order: 1) `transaction_user_info` block first 2) `payment_html`, `fiscal_html`, `shipping_html`, `integr_html` blocks For bank production approval, thank-you page must also show complete order summary 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. Create containers for transaction details and each receipt type: ```html
``` ```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; // keys can be translated dd.textContent = String(v ?? ''); // values must stay original 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 `transaction_user_info` 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" } } ``` Rule: keys may be translated for UI labels, but values should be displayed exactly as received. If any section is missing, render a visible placeholder instead of hiding the section. For `AWAITING` and `OBLIGATED`, always render `payment_html` on the thank-you page. These are not failed statuses; they are commonly used for account/invoice payment instructions (bank transfer details). Cart handling rule: clear cart for `PAID`, `RESERVED`, `PAYING`, `AWAITING`, and `OBLIGATED` (same cart behavior for all these statuses). --- ### Step 7 — Verify Response `vhash` on Server (Node.js) Important naming: - request to HPay (`pay_request` / `charge_request`) -> field `verificationhash` -> generate with `generatePOSRequestSignature(...)` - response from HPay (browser callback / webhook) -> field `vhash` -> validate with `verifyHPayResponse(...)` Always verify response `vhash` server-side before marking order as paid/fulfilled. ```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(); } ``` --- ## Optional — Advanced: Backend Charges (Server-Server MIT) — `/clientpay/charge` > **Skip this section** unless the user explicitly asked for subscriptions, automatic recurring charges, or merchant-initiated billing. The standard checkout flow (Steps 1–6) is completely independent of this. ### Critical: `presentHPayPayForm(pay_request)` vs `/clientpay/charge` — do not confuse Both can use a saved `vault_token_uid`, but they are **different flows**: | | **`presentHPayPayForm(pay_request)`** (CIT / buyer checkout) | **`/clientpay/charge`** (MIT / server-server) | |---|--------------------------------------------------------------|-----------------------------------------------| | **Who initiates** | Buyer at checkout | Your server — cron renewal, merchant admin | | **Buyer present** | Yes — HPay UI opens from `presentHPayPayForm` | No — silent, no card form | | **`vault_token_uid`** | Optional on `pay_request` | **Required** on `charge_request` | | **Use existing token when** | Logged-in buyer **selects** a saved `user_vault_token` at checkout for faster payment | Server charges token from DB without buyer | | **Do not use for MIT** | — | This is the correct path for MIT | **Wrong pattern (common mistake):** cron / admin renewal calling **`presentHPayPayForm(pay_request)`** with `vault_token_uid`. **Correct pattern:** server POST to **`/clientpay/charge`** with body `{ request_data: charge_request }`. ### What server-server charge is Lets your **server** charge a saved card with **no buyer interaction** — used for: - Recurring subscription billing (cron) - MIT — merchant-initiated charge from admin ### Prerequisites - Payment method with `pm.SubsciptionsType` containing `"cof"` or `"mit"` - `pm.POps` contains `"charge"` - `vault_token_uid` in your DB (`user_vault_token`) from a prior successful payment - **Merchant Site UID** + **Secret Key** on server only - MIT renewals need a bank that supports MIT; manual merchant charges can still use `/clientpay/charge` where supported ### How the token gets saved (CIT via `presentHPayPayForm` — not `/clientpay/charge`) First payment / card save uses **`presentHPayPayForm(pay_request)`** (buyer checkout), not `/clientpay/charge`: ```javascript vault_token_uid: 'new' // save new card // OR logged-in buyer picked saved card at checkout: vault_token_uid: 'existing-token-uuid' // still presentHPayPayForm (CIT), not MIT ``` After success, persist `vault_*` fields (example): > **`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": "Ij0BZTRhgkuhDG2nqSg8IQ", // 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 } ``` ### Server-server charge request (Node.js) Same signature function as `pay_request`. **Wrap body as `{ request_data: charge_request }`.** ```javascript // SERVER-SIDE 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 = [request.transaction_uid, request.status, request.order_uid, amt, request.order_currency, request.vault_token_uid, request.subscription_uid].map(v => String(v || '').trim()).join('|'); cstr += String(request.rand || '').trim(); return crypto.createHash('sha512') .update(md5(cstr + merchant_site_uid) + 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 // 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 (verifyHPayResponse) if (/PAID|PAYING|RESERVED|OBLIGATED|AWAITING/i.test(result.payment_status)) { // charge succeeded } ``` --- ## 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 Charges section above). ### Successful CIT response — vault fields to persist After a successful CIT payment, the response typically includes the `vault_*` fields shown above. Persist them immediately. The same **`vault_token_uid`** is passed again in **`pay_request`** for **`presentHPayPayForm`** when the logged-in user picks that saved card, or in **`charge_request`** for **`/clientpay/charge`** (server MIT — only field required there). ### 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). --- ## Optional: Shipping Address Autocomplete (AdaptCheckout) If the user uses HolestPay shipping modules, some methods provide address autocomplete/suggestion UI. Call `adaptCurrentShipping()` whenever the selected shipping method changes. Pass `null` to destroy the adapter when no shipping method is selected. The selector strings map your existing checkout input fields to HolestPay's address model — **update them to match your actual input selectors**. ```javascript let adapted_checkout_destroy = null; let adapted_shipping_method_uid = null; function adaptCurrentShipping(shipping_method_uid) { try { if (shipping_method_uid) { if (shipping_method_uid === adapted_shipping_method_uid) return; const smethod = HPay.POS.shipping.find(s => s.Uid == shipping_method_uid); if (smethod && smethod.AdaptCheckout) { adapted_checkout_destroy = smethod.AdaptCheckout({ billing: { address: "#addressInput[name='address1']", // update selectors to match your inputs address_num: "#addressLine2Input[name='address2']", postcode: "#postCodeInput[name='postalCode']", city: "#cityInput[name='city']", municipality: "#provinceInput[name='stateOrProvince']", phone: "#phoneInput[name='phone']", country: "#countryCodeInput[name='countryCode']", is_company: "#companyInput[name='company']", company: "#companyInput[name='company']", company_tax_id: "", company_reg_id: "" }, shipping: { address: "#addressInput[name='shippingAddress.address1']", address_num: "#addressLine2Input[name='shippingAddress.address2']", postcode: "#postCodeInput[name='shippingAddress.postalCode']", city: "#cityInput[name='shippingAddress.city']", municipality: "#provinceInput[name='shippingAddress.stateOrProvince']", phone: "#phoneInput[name='shippingAddress.phone']", country: "#countryCodeInput[name='shippingAddress.countryCode']", is_company: "#companyInput[name='shippingAddress.company']", company: "#companyInput[name='shippingAddress.company']", company_tax_id: "", company_reg_id: "" } }) || null; adapted_shipping_method_uid = shipping_method_uid; } } else { if (adapted_checkout_destroy) { adapted_checkout_destroy(); adapted_checkout_destroy = null; adapted_shipping_method_uid = null; } } } catch(ex) { console.error(ex); } } ``` --- ## No Backend Needed for Basic Checkout If the site runs on Shopify, BigCommerce, or any platform where you cannot run custom server code, the FrontCore flow (Steps 1–6) is fully self-contained — no backend is required for standard checkout. Backend code is only needed if you implement: - **Backend charges** (subscription / MIT auto-charge) - **Admin operations** (refunds, captures, etc.) - **Admin Order REST API** (list / get / update HolestPay orders from your admin backend) - **Payment / module handlers** (`queryRequest`, void, refund, capture, fiscal/shipping/integration methods) - **Webhook verification** (verify response `vhash` from `notify_url` payload) For webhooks and backend operations you also need the **Merchant Site Secret Key** (from HPay panel ? Site Settings). --- ## Optional — Advanced: Admin Order REST API (Site Admin Backend) > **Skip this section** unless the user has a custom backend / admin area and needs to list, load, or update HolestPay orders from the server. Not needed for basic FrontCore checkout. Never call these endpoints from the browser. Reference sample: `hpay_orders_api_sample_nodejs.js` Use this for the **administrative part of the merchant site** (order list, order detail, editing order fields). Requires **Merchant Site UID** + **POS Secret Key** on the server. Signature algorithm is the same as backend charge / `generatePOSRequestSignature`. Auth goes in HTTP headers: `rand` + `verificationhash`. HolestPay can also render a full admin UI via `orderActions`; manual REST and **Payment / Module Backend Handlers** (`queryRequest`, void, refund, capture, …) are still commonly needed for custom admin backends. See `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 / orders + payment handler calls and inspect METHOD, URL, headers, body, and response (PHP / Node.js / C# tabs). Order object format is the HolestPay order model (same as webhook `orderupdate.order` / `HPay.getOrder`), sample: `https://apps.holest.com/holest-pay/hpay_order_sample.json` — not the `pay_request` / `hpay_response` field names. ### Endpoints Base: `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` / `sort_order` match HolestPay admin UI format, 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 wrapped under `items` / `Orders` / `orders` / `data` / `result` / `Results`. Prefer `Uid` on each order. **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 order fields */ } } ``` Example: ```javascript const updateBody = { Order: { Data: { billing: { note: 'Updated via API' }, }, }, }; ``` ### Signature headers (same as charge) ```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(); } ``` For list calls, signed fields may be empty except `rand`. For get/update, include `order_uid` in signed fields. ### vs browser admin toolbox / manual handlers HolestPay can **easily render a full admin interface** for order detail or order list (`orderActions` after normal `hpay.js` + secret). Integrators **often still need manual server-to-server calls** (custom admin, cron, bank sync). - REST API above → your server order list / detail / field edits - `HPayInit(..., secret_key)` + `orderActions` on normal `hpay.js` (not FrontCore) → ready-made refund / fiscal / shipping UI - **`POST /clientpay/store`** below → orders not paid via HPay checkout that still need fiscal / shipping / integration / Pay-By-Link - **Payment / module handlers** below → `queryRequest`, void, refund, capture, and the same pattern for fiscal/integration/shipping --- ## Optional — Advanced: Store Order API (`POST /clientpay/store`) + Pay-By-Link > **Skip unless** the user needs HolestPay fiscal / integration / shipping (or a Pay-By-Link) on orders that were **not** created through an HPay payment method on the frontend. Server-side only (Secret Key). Never call from the browser. After create/update via `/store`, the order exists in HolestPay like any other — full **admin rendering** (`orderActions`) and module handlers such as **`defaultAction`**. 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) ``` POST /clientpay/store Body: { "request_data": { /* full order fields + rand + verificationhash */ } } ``` - **Creates** when `order_uid` is new; **updates** when it already exists. - Signature lives **inside** `request_data` (same algorithm as charge). Include `order_uid`, `order_amount`, `order_currency`, `rand`. - Typical fields: `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`. - Externally paid / unpaid-via-HPay 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`** (Pay-By-Link flags). Do not confuse the two. ```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: // 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) Works for a newly stored order or an **existing** one (store/update with PBL fields). Set under **`order_data`** → 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 payment method UIDs; omit = all */ ], allow_sm: [ /* optional shipping method UIDs; omit = all */ ], pbl_sms: 0, // optional 1|0 — SMS for Serbian mobiles only } ``` Example `allow_pm` UIDs (omit the list to allow all): `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`. ### Pay-By-Link URL Same 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)}`; } // https://sandbox.pay.holest.com/pbl/{HPaySiteId}-{order_uid}{checksum27} ``` --- ## Optional — Advanced: Payment / Module Backend Handlers > **Skip unless** the user needs server-side payment sync, void, refund, capture, or other module actions without the HPay admin UI. Never call from the browser. ### URL pattern ``` POST /clientpay/handlers/{type}/{merchant_site_uid}/{module_uid}/{order_uid}/{method} ``` `{type}`: `payment` | `integration` | `fiscal` | `shipping`. Payment example: `.../handlers/payment/{merchant}/{payment_method_uid}/{order_uid}/queryRequest` Same pattern applies to other modules. Fiscal/integration use **`defaultAction`** for the default expected op (e.g. efiscal → `createSale`). They do not all share payment-like standard method names — prefer `orderActions` UI, or ask Holest support for module-specific endpoints. ### Auth (JSON body — not headers) Handlers put `order_uid`, `rand`, `verificationhash` in the **JSON body** (Orders Admin API uses headers instead). Signature algorithm is the same as charge / `generatePOSRequestSignature`. 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 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); await fetch( `${BASE_URL}/clientpay/handlers/payment/${encodeURIComponent(merchantSiteUid)}/` + `${encodeURIComponent(paymentMethodUid)}/${encodeURIComponent(orderUid)}/queryRequest`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } ); ``` After `queryRequest`, call **`getOrder`** for current HolestPay status. Status changes may fire **`orderupdate`** webhook (`I(P|S|F|I)N` URL). ### Standard payment methods | Method | Purpose | Body notes | |--------|---------|------------| | `queryRequest` | Sync payment state from bank GTW into HPay | `order_uid`, `rand` (+ signature) | | `syncOnPOS` | Explicitly trigger `orderupdate` webhook | `order_uid`, `rand`. **Requires** panel `I(P|S|F|I)N` notify URL to be configured — otherwise HolestPay has nowhere to send the webhook | | `voidRequest` | Full cancel before bank clearing | `order_uid`, `rand` | | `refundRequest` | Refund | Required: `order_uid`, `rand`. Optional partial: `total` (order currency); with items: `order_items[]` + `refunded_qty` / `refunded` per item | | `captureRequest` | Capture RESERVED (pre-auth) amount | Required: `order_uid`, `rand`. Optional partial: `total`; with items: `captured_qty` / `captured` per item | ### Fiscal / integration — `defaultAction` Fiscal and integration modules have **`defaultAction`** — runs the module’s default expected operation for the order (e.g. **efiscal**: `defaultAction` ≈ **`createSale`**). ``` POST /clientpay/handlers/fiscal/.../{order_uid}/defaultAction POST /clientpay/handlers/integration/.../{order_uid}/defaultAction ``` Unlike payment (`queryRequest`, `voidRequest`, …), fiscal/integration modules do **not** share one fixed action list for every module — each is different. For custom server-side ops beyond `defaultAction`, **ask Holest support**. Prefer the built-in **`orderActions` admin UI** (same as HPay panel): it renders all available payment / fiscal / integration / shipping actions for the current order status. Use raw handlers mainly for automation. If the merchant uses a **WAF / firewall**, whitelist **`pay.holest.com`** (and `sandbox.pay.holest.com` for tests) — see CSP / WAF section. --- ## Checklist - [ ] FrontCore `