Posted on

HolestPay FrontCore (JavaScript-Only) Payment Integration Guide for AI Coding Assistants

Tell your AI coding assistant (such as Cursor, for example) that you want to integrate HolestPay for payments, fiscalization, and shipping, and provide it with this MD markup: https://apps.holest.com/holest-pay/AI_INTEGRATION_GUIDE_FRONTCORE.md.txt
Your AI assistant will then know exactly how to guide you through the process.

Make sure to SET UP FIRST ALL methods you plan to use on https://sandbox.pay.holest.com/ (payment | fiscal | shipping) and test them using “pay-by-link” (entirely off-site). If everything is configured properly, the integration will go smoothly with just a few queries.

HolestPay FrontCore (JavaScript-Only) Payment Integration Guide for AI Coding Assistants – MD file…


# 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)?

---

## 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 unique order ID
  order_name: "#Order 204",                      // optional order label
  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_data: {                                  // optional custom payload -> stored as order.Data
    customer_segment: "B2C",
    source: "checkout-web"
  },
  cof: "optional",                               // optional: optional|required|none
  vault_token_uid: "saved-token-uuid",           // optional: saved token, or 1|true|new

  // Optional billing object (template fields):
  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 object (template fields):
  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 items array (template fields):
  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)

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.

### 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_data`, `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_data` may contain any custom key/value pairs; it is persisted to `order.Data` in HPay order.
  - `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,
        vault_onlyforuser:  r.vault_onlyforuser,
        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 `order_uid`, `order_amount`, `order_currency` -> order object `Uid`, `Amount`, `Currency` - request/response `order_items`, `order_billing`, `order_shipping` -> order object `Data.items`, `Data.billing`, `Data.shipping` - 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 Side — Still Requires Secret Key) FrontCore does not expose the Secret Key on the frontend, but backend charges still need it on your server. `charge_request` uses the same structure as `pay_request`. The practical difference is that `order_user_url` has no effect for backend charge (no buyer to redirect), so you should omit it. `cof` is also not needed for backend charge requests. ```javascript // Node.js backend 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 charge_request = { merchant_site_uid: "YOUR-MERCHANT-SITE-UID", order_uid: "20260315-999999", order_amount: "15000", order_currency: "RSD", payment_method: "179", vault_token_uid: "saved-token-uuid", // required order_data: { source: "subscription-renewal" }, // optional custom data -> order.Data // 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( charge_request.merchant_site_uid, SECRET_KEY, charge_request ); const baseUrl = 'https://sandbox.pay.holest.com'; // or pay.holest.com const response = await fetch(`${baseUrl}/s-blue/v1/clientpay/charge`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(charge_request) }); const result = await response.json(); 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($charge_request); $ch = curl_init($base_url . '/s-blue/v1/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); ``` --- ## 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); }); } }); } } }); }); }); }); ``` --- ## Difference Summary: FrontCore vs Standard | Feature | FrontCore | Standard | |---------|-----------|----------| | Script source | Auto-served from HPay server (whitelisted origin) | Manual `