# 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 — unique per order order_name: '#Order 204', // optional 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) cof: 'optional', // optional — 'optional'|'required'|'none' — enables card saving vault_token_uid: '', // optional — saved token UUID, or 1|true|new to force new token order_billing: { // optional but recommended 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 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 for reconciliation { 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) 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. --- ### 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, // e.g. can be Terminal ID fro payment method config or soemthinglike that vault_onlyforuser: r.vault_onlyforuser, //subscription falsy | fast checkout 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. --- ### 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-Side, Subscriptions / MIT) > **Skip this section** unless the user explicitly asked for subscriptions or automatic recurring charges. This is used by a small fraction of integrations. The standard checkout flow (Steps 1–6—6) is completely independent of this. ### What it is A backend charge lets your **server** silently charge a customer's saved card without any buyer interaction — no payment form, no redirect. This is used for: - Recurring subscription billing (charge every month automatically) - MIT (Merchant Initiated Transactions) — charging after service delivery ### Prerequisites for backend charge - A payment method with `pm.SubsciptionsType` containing `"cof"` or `"mit"` must be active on the POS - `pm.POps` must contain `"charge"` for that method - A `vault_token_uid` saved from a previous successful checkout (from `r.vault_token_uid` in `onHPayResult`) must exist in your database for that customer - **Merchant Site UID** and **Merchant Site Secret Key** (server-side only) ### How the token gets saved During a normal checkout (Step 5), if card saving was enabled (`cof: 'optional'|'required'`), the `onHPayResult` response will contain a `vault_token_uid`. You **must save this to your database** linked to the user's account. The backend charge uses this token later. ### Backend charge request (Node.js) ```javascript // SERVER-SIDE ONLY — never run this on the frontend const crypto = require('crypto'); const md5 = require('md5'); // Signature function — required to authenticate the charge request 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(); } // charge_request has same structure as pay_request but: // - merchant_site_uid IS required (backend operation) // - vault_token_uid IS required (the saved token from your DB) // - order_user_url is NOT needed (no buyer to redirect) // - cof is NOT needed const charge_request = { merchant_site_uid: MERCHANT_SITE_UID, // from config — never hardcode order_uid: 'ORDER-20260425-001', // unique per charge attempt order_amount: '15000', order_currency: 'RSD', payment_method: '179', // pm.HPaySiteMethodId vault_token_uid: 'saved-token-from-db', // from your database for this customer }; charge_request.verificationhash = generatePOSRequestSignature( charge_request.merchant_site_uid, MERCHANT_SITE_SECRET, charge_request ); const BASE_URL = 'https://sandbox.pay.holest.com'; // or https://pay.holest.com for production const response = await fetch(BASE_URL + '/clientpay/charge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(charge_request) }); const result = await response.json(); // result has same structure as e.hpay_response from onHPayResult // check result.payment_status for PAID|RESERVED|SUCCESS|PAYING|OBLIGATED|AWAITING if (/PAID|PAYING|RESERVED|OBLIGATED|AWAITING/i.test(result.payment_status)) { // charge succeeded } ``` --- ## 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—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.) - **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). --- ## Checklist - [ ] FrontCore `