Posted on

Hvatanje Magecart Maleware-a

Detecting a Magecart Attack: How to Find the Invisible Skimmer

Magecart is one of the most dangerous threats facing modern e-commerce. Unlike classic attacks that take down a site or steal a database, Magecart operates silently. It doesn’t steal from you – it steals directly from your customers at the moment they trust your brand the most: on the payment page.

What is a digital skimmer? It is malicious JavaScript code that “lurks” on your checkout page. It intercepts payment card data (card number, CVV, expiry date) and sends it to the attacker before the bank even receives the transaction request.

Before You Begin: Assessing the Severity of the Situation

  • Isolated case (single customer): If HOLEST DYNAMIC DOM GUARDIAN has reported a warning for only one customer, there is a high probability that the malicious code was injected on that customer’s own device (e.g. local malware, compromised browser). In this case your store is most likely fine – advise the customer to scan their own device.
  • Multiple cases – immediate action required: If HOLEST DYNAMIC DOM GUARDIAN is reporting warnings for multiple customers, or the problem occurs when you personally attempt a transaction – this is an alarm that demands immediate response. Every minute of delay means your customers’ card data is actively being stolen.
  • Warning – the threat may be hiding: In some cases, threats of this type have built-in evasion mechanisms: the malicious code automatically hides or deactivates when it detects that an administrator is accessing the site (based on session cookies, URL parameters, or specific request headers). It is therefore strongly recommended to test the site from a private/incognito window without being logged into the admin account.

The following describes how, in a real-world case, the exact script responsible for the malicious code was located.

Before the Investigation: Required Site Preparation

Do this before anything else. Before starting the investigation, you must protect visitors and ensure you have unobstructed access to the site.
  1. Enable maintenance mode: While you investigate, new visitors must not be able to reach checkout and enter card details. Enable maintenance mode through your WP maintenance plugin – visitors will see a “Site is currently under maintenance” page, while you as administrator continue to see the site normally.
  2. Deactivate the HolestPay or other payment module: To freely inspect the checkout page without interference from the HOLEST DYNAMIC DOM GUARDIAN system, you must temporarily deactivate the HolestPay plugin in the WordPress admin panel (Plugins → Holest Pay → Deactivate). Deactivating the plugin automatically deactivates the D-DOM Guardian protection as well, allowing you to observe the malicious form and trace its source without interference.
Note: Once the investigation is complete and the malicious code has been removed, be sure to reactivate HolestPay and disable maintenance mode.

Phase 0: Spotting the Anomaly

Most store owners learn about an attack only when their bank or customers report suspicious transactions. However, there are visual indicators you can spot yourself:

  • Language mismatch: If your site is in your local language but the card input fields suddenly appear in English (e.g. “Card Number” instead of the local equivalent), that is a red flag.
  • Delayed rendering: Skimmers often wait for all legitimate scripts to load before injecting their fake form. If you notice that the payment form changes or flickers a few seconds after page load – you are likely under attack.
STEP 1

Identifying and Analysing the Fake Form (Element Forensics)

The attack begins when you notice a card entry form on the checkout page that is not part of your original design. The most common sign is that the labels and fields are in English while the rest of the site is in a different language.

Phantom card form on the checkout page

Image 1 – The attacker’s fake card form has appeared on the checkout page alongside the legitimate payment processor form.

Right-click directly on the suspicious field and choose Inspect. In the Elements panel, locate the characteristic attributes of that element: id, name, or placeholder. These are the attacker’s “signature” and will serve as the trigger for our trap.

Inspecting the element – identifying characteristic code fragments

Image 2 – Using Inspect we identify the characteristic strings (sq-pf-unq and card_number) whose appearance will trigger the breakpoint.

The malware constructed a complete fake form (imitating the Square payment system) containing fields for card number, expiry date, CVV, and a save-to-account option. Importantly, this HTML is not present statically in the page source – the malware builds it dynamically through JavaScript at page load time. We confirm this by searching the DOM (e.g. searching for the prefix sq-pf-unq):

DOM search finds nothing

Image 2-2 – Searching the page source for the key strings (sq-pf-unq, card_number) returns no results – the form is constructed dynamically by JavaScript.

Precisely because the form is dynamic, a standard code search is useless. We need to catch the JavaScript in the act of building that form.

STEP 2

Setting the Digital Trap (Trap Script)

Since the malicious code executes dynamically and briefly, we need to force the browser to automatically pause execution the moment the suspicious HTML appears in the DOM. We achieve this using a MutationObserver combined with native debugger statements.

Prepare a trap script like the one below so that it triggers a breakpoint (debugger) when it detects the creation of characteristic parts of the malicious form:

(function magecartGlobalSniffer() {
    console.log("Sniffer active. Monitoring all DOM changes...");

    // 1. MutationObserver - catches absolutely everything added to the HTML
    const observer = new MutationObserver(function observerCallback(mutations) {
        mutations.forEach(function mutationHandler(mutation) {
            mutation.addedNodes.forEach(function nodeHandler(node) {
                if (node.nodeType === 1) {
                    const htmlContent = node.outerHTML || "";
                    if (htmlContent.includes('sq-pf-unq') || htmlContent.includes('card_number')) {
                        console.warn("INJECTION DETECTED!");
                        console.log("Element:", node);
                        debugger; // Check the Call Stack here!
                    }
                }
            });
        });
    });

    observer.observe(document.documentElement, {
        childList: true,
        subtree: true
    });

    // 2. Intercept document.write (common method used by older skimmers)
    const originalWrite = document.write;
    document.write = function interceptedWrite(content) {
        if (content && (content.includes('sq-pf-unq') || content.includes('card_number'))) {
            console.warn("document.write INJECTION DETECTED!");
            debugger;
        }
        return originalWrite.apply(this, arguments);
    };

    // 3. Helper function for manual stack inspection in the console
    window.getMalwareStack = function() {
        try {
            throw new Error("Manual Stack Trace");
        } catch (e) {
            return e.stack;
        }
    };

    console.log("Trap is ready. If the form appears, the debugger will fire.");
})();
Note: Replace the strings 'sq-pf-unq' and 'card_number' with the identifiers you found in Step 1 – these are the triggers specific to your attack.

Before pasting the trap, open the Sources tab in DevTools and set a breakpoint as early as possible in the page load sequence. This forces the browser to pause before the malware has any chance to inject anything, giving you the opportunity to paste the trap via the console. Once the browser pauses at that breakpoint, switch to the Console tab, paste the trap script above, and run it. From that point on, the trap is active and waiting for the malware:

Injecting the trap script via the console

Image 4 – The trap has been injected through the console while the browser is paused. As soon as the malware attempts to build the fake form, the breakpoint will fire automatically.

STEP 3

Catching the Source (Call Stack Analysis)

Continue page loading. When the malware attempts to construct the fake form, our trap will catch it and the browser will automatically pause execution. The screen dims and DevTools opens the Sources tab.

Breakpoint fired

Image 5 – The trap has worked: the breakpoint fired at the exact moment the malware tried to inject the fake form into the DOM.

The key is the Call Stack panel (right side of the Sources tab). It shows the chronological chain of function calls that led to the creation of the fake form. Walk it backwards – skip the first entry (that is our trap) and focus on the second or third entry.

Walking the Call Stack backwards

Image 6 – Walking the Call Stack backwards, we locate the exact JavaScript file that triggered the fake form injection. In our case it was a script from the external domain cms-manager.net.

Note: If the file originates from a domain that is not yours (e.g. cms-manager.net) or has a random name such as ae83f2.js – you have found the attacker.

STEP 4

Finding the “Entry Point” – How the Malware Loads onto Your Site

Now that we know the URL of the malicious script, we need to find where and how it was injected into your site. Use View Page Source (Ctrl + U) and search (Ctrl + F) for the discovered domain or URL.

Magecart is typically loaded via a seemingly harmless script tag in the site’s header or footer. Our investigation found that the following external script URL had been injected into the HTML: https://cms-manager.net/script.js

Malicious script tag found in page source

Image 7 – In the page source we find the script tag loading the malicious script from an external domain. This is the “entry point” the attacker left behind on the server.

This line is not part of your original code – the attacker injected it directly into files on the server, most commonly through a compromised FTP/SSH account or a vulnerability in a CMS plugin.

What to Do After Detection?

This is only half the battle. We now know what is being loaded and where it comes from in the browser – but that does not mean we can simply search the server and find the problem. Attackers almost always encode or obfuscate the malicious code inside PHP files on the server. A direct search such as grep -r "cms-manager.net" . or find . -name "*.php" | xargs grep "cms-manager" will almost certainly return no results – the infected PHP file exists on the server, but the string we are looking for does not appear in readable form. The malicious URL is hidden behind encoding such as base64, gzip compression, or multiple layers of obfuscation, making it undetectable by a plain text search.

How to Locate the Infected File on the Server

Since we know which script in the browser is causing the problem, we can use a process of elimination to identify which module or file on the server is injecting it:

  1. Disable modules one by one: Through the admin panel, gradually deactivate modules/plugins and after each one refresh the checkout page. When the malicious script stops loading – the last module you disabled is the culprit.
  2. Inspect that module’s files: Once we know which module is responsible, we examine its files. What we are looking for are PHP files that appear suspicious – containing massive blocks of encoded content: long strings of random-looking characters that make no sense at first glance. Typical patterns include eval(base64_decode(...)), eval(gzinflate(...)), eval(str_rot13(...)), or multiple layers of nested decoding. Legitimate modules should never contain such constructs – if you find a PHP file with hundreds or thousands of characters of encoded string, it is almost certainly malicious code.
  3. Trace the network request: In DevTools, open the Network tab, filter by type JS, and watch which file from your server initiates the request to cms-manager.net. This directly reveals which file on your site is calling the malicious external script.

Once You Have Found the Infected File

  1. Change all passwords: FTP, SSH, WP-Admin, and the database – the attacker had to gain access somehow.
  2. Review other files: Check wp-config.php and .htaccess for additional suspicious modifications – there is often more than one infected location.
  3. Server logs: Review access logs to see who modified files and when, during the period the skimmer was active.
  4. Notify your payment processor: If the skimmer was active, you are obligated to notify your bank and payment processor to protect customers who may have been affected.

This guide was created for educational purposes based on a real-world forensic analysis of a Magecart variant.
Stay vigilant – the security of your customers is your top priority.

Posted on

HolestPay FrontCore Guide for Lovable and Similar AI Site Builder Systems

Tell Lovable (or similar system…) that you want to integrate HolestPay payments/fiscal/shipping and give it this MD markup: https://apps.holest.com/holest-pay/Lovable_or_similar_ai_system.MD.txt
Lovable will know then what to ask you and what to instruct you to do…

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 Guide for Lovable and Similar AI Site Builder Systems – MD file…

# 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 `<code>` 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:*
> `<script src="https://sandbox.pay.holest.com/clientpay/handlers/pos/YOUR-UID/frontend-script-core.js"></script>`
> *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 `<script>` tag that HolestPay generates specifically for your POS/site. It contains your Merchant Site UID in the URL and acts as the authorization key for the JavaScript-only checkout — no backend signature is needed because of it.*
>
> *Here is how to get it:*
> 1. Go to https://sandbox.pay.holest.com (if you are testing) or https://pay.holest.com (if you are going live). Create an account if you don't have one.
> 2. Create a **company** on the HPay panel.
> 3. Create a **POS / site** for that company.
> 4. Add the **payment methods** you want to offer. You can test them right away using the built-in **Pay-By-Link** feature (no integration needed for that).
> 5. *(Optional)* Add **fiscal**, **integration**, and/or **shipping** modules if you need them.
> 6. In the top bar, select your company and site, then go to **Site Settings**.
> 7. Find the field **"Frontend Script-Core Origins"** and enter your site's domain (one per line, wildcards allowed — e.g. `*.mysite.com`). Save.
> 8. The panel will immediately display the full `<script>` tag. Copy it and paste it here."*

**Do not proceed with implementation until the user provides this script tag.**  
Once you have it, extract the `src` URL — that is all you need to continue.

> ### ?? IMPORTANT — Tell the user this before any code is written:
>
> *"Before we start the implementation, please make sure you have done the following on the HolestPay panel:*
>
> - ? **All payment methods** you want to offer are added to your POS and working
> - ? **Shipping methods** are added and configured (if you need shipping)
> - ? **Fiscal / integration modules** are added and configured (if you need fiscalization)
> - ? **Every method has been tested using the HPay panel's Pay-By-Link feature** — this lets you create a real test payment link without any code and confirm each method actually processes correctly
>
> *This is critical. If a payment method is missing, misconfigured, or untested on the HPay panel, the implementation here will point in the wrong direction and you will waste time debugging something that is actually a panel configuration problem, not a code problem. Fix the HPay panel first, then come back here."*

Then ask the user the following **before starting to build**:

> *"A few things to know before we start:*
>
> - **Existing payment and shipping methods on your site do not need to be removed.** HolestPay payment and shipping methods are added **alongside** whatever you already have. You can keep PayPal, Stripe, manual bank transfer, COD — all of it — and HolestPay methods appear as additional options in the same selectors.
>
> - **Tip:** HolestPay also supports defining **generic payment methods** directly in the HPay panel — cash on delivery (COD), manual bank transfer, and other custom manual methods. If you want everything managed in one place through HolestPay (instead of mixing HolestPay with your platform's built-in methods), that is possible. Just add those methods on the HPay panel as well. But this is entirely optional — you decide what goes through HolestPay and what doesn't.
>
> *Do you also need:*
> - ?? **Shipping modules** — HolestPay can provide shipping method selection with carrier integrations, address autocomplete, and shipping labels/receipts. Do you have shipping methods configured on your HPay POS?
> - ?? **Fiscalization** — HolestPay can issue fiscal receipts automatically after payment (required by law in some countries). Do you have a fiscal module configured on your HPay POS?"*

- ?? **Subscriptions / card saving** — HolestPay can save a card token after the first payment so the buyer doesn't need to re-enter card details next time, or for automatic recurring charges. **Note: not all payment methods support this** — only methods where `pm.SubsciptionsType` contains `"cof"` or `"mit"` do. Do you plan to offer subscriptions or saved-card checkout?

- ?? **Backend admin commands (refunds, captures, cancellations, etc.)** — HolestPay provides admin operations that can be triggered from your order detail / order list pages (e.g. capture a reservation, issue a refund, resend a fiscal receipt). These require the **Merchant Site Secret Key** on the server side.
  - **If the site runs on Shopify, BigCommerce, or a similar hosted platform** — skip this, backend is not needed.
  - **If you are building a custom backend or the user's platform allows custom server code** — ask: *"Do you want admin payment actions (refund, capture, cancel, etc.) available on your order detail or order list pages?"*

- ?? **Shopify payment method name parity (Shopify only)** — If the user is on Shopify, ask them to create a custom/manual Shopify payment method with the **exact same name** as configured in HolestPay admin. If the name is changed later, it must be updated in **both** Shopify and HolestPay admin to stay aligned.

- ?? **Shopify shipping method/price parity (Shopify only)** — If the user is on Shopify, ask them to configure Shopify shipping rates using the **same shipping method names** as in HolestPay POS, and keep names synchronized in both systems if they are renamed.

- ?? **Footer bank/card/3DS logotypes** — Do you want us to implement the footer branding strip with card logos, bank logos, and 3DS logos sourced from HolestPay POS parameters (as required by banks for production approval)? These logotypes should be visible in the site footer on **all site pages**, not only on checkout.

- ?? **Terms of Service source strategy** — Do you want to use the HolestPay-provided Terms of Service page directly, compare it and merge equivalent clauses into your existing Terms page, or handle Terms in another way?

- ?? **Checkout acceptance checkbox** — Do you need an `I accept Terms of Service` checkbox on checkout, where `Terms of Service` is clickable (opens your TOS page or modal)?

Implement shipping, fiscalization, subscription/card-saving, and backend admin commands **only if the user confirms they need them.** If unsure, tell them they can add it later — the POS configuration drives what is available (`client.POS.shipping`, `client.POS.fiscal`, `pm.SubsciptionsType`, `pm.POps`) and nothing breaks if a feature is not configured.

---

## What FrontCore Is

FrontCore is a **JavaScript-only** HolestPay integration. No backend signature is required for checkout — the POS-specific script URL acts as the authorization token. A secret key is only needed on the server for backend charges and admin operations.

## 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.

### ?? FrontCore Security Trade-off — Amount Manipulation

Because everything runs on the frontend without a server-side signature, **FrontCore does not protect against amount manipulation**. A technically skilled buyer could in theory alter the `order_amount` in the JavaScript before the payment is submitted (e.g. change 3000 RSD to 30 RSD) and the payment would succeed at the tampered amount.

**When this is acceptable (most cases):**
- Sites selling **physical/tangible goods** — the merchant sees 30 RSD was paid instead of 3000 RSD and simply does not ship the order. No automated fulfillment happens before a human reviews it.
- Low-risk digital goods or services where order values are small or fixed.
- Situations where the webhook (`notify_url`) result is verified server-side before fulfillment is triggered.

**When you should use the Standard integration instead (server-side signature):**
- Automatic digital fulfillment (license keys, downloads, subscriptions activated instantly)
- High-value orders where manual review of every payment is not practical
- Any case where the server automatically fulfills the order based solely on the payment result without a human check

> **Tell the user this before implementing FrontCore:**  
> *"FrontCore is the simplest integration but it does not prevent a determined buyer from manipulating the payment amount on the frontend. For most shops selling physical goods this is fine — you simply don't ship if the paid amount doesn't match the order. If your site automatically delivers digital goods or activates services immediately after payment, you need the **Standard integration with server-side signature** instead — see https://apps.holest.com/holest-pay/AI_INTEGRATION_GUIDE_STANDARD.md.txt — which cryptographically signs each request on your server so the amount cannot be tampered with on the frontend."*

---

## What You Need from the User Before You Start

**The user must provide one thing to start: their FrontCore script `<script>` tag.**

It looks like this (sandbox example):

```html
<script src="https://sandbox.pay.holest.com/clientpay/handlers/pos/07f689c5-5bc8-44b6-a563-8facc6870fab/frontend-script-core.js"></script>
```

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 `<script>` tag | Different URL for sandbox vs production, or when switching POS |
| Merchant Site UID | Backend charge requests, admin ops | Changes with sandbox ? production swap or different POS |
| Merchant Site Secret Key | Backend only (charges, admin, webhook verification) | Changes with sandbox ? production swap or different POS |

The most common user action after initial setup is **swapping sandbox ? production** — this should require changing only these three values and nothing else.

---

## Integration Steps

### Step 1 — Add the FrontCore Script

Add the user's script tag **once**, in a global location so it is available on all pages/views. This is important for payment methods like Apple Pay / Google Pay that can be triggered from a product listing page.

```html
<!-- Place in global layout, e.g. <head> or before </body> -->
<!-- USER PROVIDES THIS TAG — store src as a config value so sandbox?production swap is one edit -->
<script src="FRONTCORE_SCRIPT_URL_FROM_USER"></script>
```

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 `<html lang="...">` 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
<!-- Place dock container below payment method description in your checkout UI -->
<div id="paymentMethodDock" style="max-width:460px;"></div>
```

```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.

#### 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 `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 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
<form method='POST' id='frmRedirect' action='${order_user_url}'>
  <input type='hidden' name='hpay_forwarded_payment_response' id='hpay_response' />
</form>
<script nonce='${csp_nonce}'>
  document.getElementById('hpay_response').value = JSON.stringify(hpay_result);
  document.getElementById('frmRedirect').submit();
</script>
```

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
<div id="hpay-transaction-user-info"></div> <!-- translated keys, original values -->
<div id="hpay-receipt-payment"></div>  <!-- payment receipt -->
<div id="hpay-receipt-fiscal"></div>   <!-- fiscal receipt (white background recommended for legibility) -->
<div id="hpay-receipt-integr"></div>   <!-- integration module output -->
<div id="hpay-receipt-shipping"></div> <!-- shipping label / receipt -->
```

```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 = '<p class="hpay-placeholder">Transaction details are not available.</p>';
    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 = '<p class="hpay-placeholder">Not available for this payment.</p>';
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 `<script>` tag added globally (all pages/views)
- [ ] Script URL stored as a single config value (easy sandbox ? production swap)
- [ ] `HPayInit()` called on checkout page load; `client.MerchantsiteUid` saved
- [ ] Payment method selector populated from `client.POS.payment` using `pm.Name` and `pm.HPaySiteMethodId`
- [ ] Shipping method selector populated from `client.POS.shipping` (if applicable)
- [ ] `onHPayResult` event handler implemented
- [ ] POST-back handler implemented on `order_user_url` page (for redirect-type methods)
- [ ] Receipt containers (`payment_html`, `fiscal_html`, `integr_html`, `shipping_html`) present on result page
- [ ] If card saving used: `vault_token_uid` saved to database on successful payment
- [ ] Dock container present if any payment method supports docking (`pm.PayInputUrl`)
- [ ] Footer logotypes rendered from `HPay.POS.pos_parameters` (cards, banks, 3DS)
- [ ] TOS checkbox present near Pay button; TOS content from `/clientpay/tos/<uid>`

---

## Required for Bank Production Approval — Logotypes and Terms of Service

> ?? **This section has nothing to do with payment functionality** — payments work without it. However, **almost all banks in the region will require these elements to be present on the site before approving the POS for production use.** Implement these alongside the payment integration.

---

### Footer Logotypes (Card logos, Bank logos, 3DS logos)

Banks require card brand logos, acquiring bank logos, and 3DS security logos to appear in the site footer — in a single horizontal line, **no taller than approximately 1 cm visually**. Order: card logos first, bank logos in the middle (with some spacing on both sides), 3DS logos on the right. Bank and 3DS logos must be linked. The footer strip must be visible across **all pages of the website**, not just the checkout page.

HolestPay provides all logo image URLs and link targets via `HPay.POS.pos_parameters` after `HPayInit()`. Use them — do not source logos elsewhere.

**CSS for the footer branding strip:**

```css
.hpay_footer_branding_wrapper {
  width: 100%;
  padding: 8px 0;
  border-top: 1px solid #e0e0e0;
}
.hpay_footer_branding {
  display: flex;
  align-items: center;
  gap: 0;
  flex-wrap: nowrap;
}
.hpay-footer-branding-cards,
.hpay-footer-branding-bank,
.hpay-footer-branding-3ds {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
}
.hpay-footer-branding-bank  { margin: 0 12px; }
.hpay_footer_branding img   { height: 1cm; width: auto; display: inline-block; }
```

**JavaScript — call this after `HPayInit()` resolves:**

```javascript
function renderHPayFooterLogotypes() {
  if (!(typeof HPay !== 'undefined' && HPay && HPay.POS)) return;
  if (!HPay.POS.pos_parameters) return;

  let card_images_html = '';
  let banks_html       = '';
  let threes_html      = '';

  if (HPay.POS.pos_parameters['Logotypes Card Images']) {
    HPay.POS.pos_parameters['Logotypes Card Images'].split('\n').forEach(src => {
      if (src.trim()) card_images_html += `<img src="${src.trim()}" alt="Card" />`;
    });
  }

  if (HPay.POS.pos_parameters['Logotypes Banks']) {
    HPay.POS.pos_parameters['Logotypes Banks'].split('\n').forEach(line => {
      if (!line.trim()) return;
      // format: "imageUrl:linkUrl" — colons in URLs are escaped before splitting
      const t = line.replace(/https:/gi, '-PS-').replace(/http:/gi, '-P-')
        .split(':').map(r => r.replace(/-P-/g, 'http:').replace(/-PS-/g, 'https:'));
      banks_html += t.length > 1
        ? `<a href="${t[1]}" target="_blank"><img src="${t[0]}" alt="Bank" /></a>`
        : `<img src="${t[0]}" alt="Bank" />`;
    });
  }

  if (HPay.POS.pos_parameters['Logotypes 3DS']) {
    HPay.POS.pos_parameters['Logotypes 3DS'].split('\n').forEach(line => {
      if (!line.trim()) return;
      const t = line.replace(/https:/gi, '-PS-').replace(/http:/gi, '-P-')
        .split(':').map(r => r.replace(/-P-/g, 'http:').replace(/-PS-/g, 'https:'));
      threes_html += t.length > 1
        ? `<a href="${t[1]}" target="_blank"><img src="${t[0]}" alt="3DS" /></a>`
        : `<img src="${t[0]}" alt="3DS" />`;
    });
  }

  const logotypesDiv = document.createElement('div');
  logotypesDiv.className = 'hpay_footer_branding';
  logotypesDiv.innerHTML =
    `<div class="hpay-footer-branding-cards">${card_images_html}</div>` +
    `<div class="hpay-footer-branding-bank">${banks_html}</div>` +
    `<div class="hpay-footer-branding-3ds">${threes_html}</div>`;

  const wrapper = document.createElement('div');
  wrapper.className = 'hpay_footer_branding_wrapper';
  wrapper.appendChild(logotypesDiv);

  (document.querySelector('footer') || document.querySelector('main') || document.body)
    .appendChild(wrapper);
}

// Call after HPayInit resolves:
// HPayInit().then(client => { renderHPayFooterLogotypes(); });
```

---

### Terms of Service Acceptance Checkbox

Almost all banks in the region require the customer to explicitly accept the site Terms of Service before payment — typically a checkbox with a link to the TOS content.

**HolestPay provides a ready-to-use Terms of Service / Purchase Conditions template page for each POS/site:**

```
https://sandbox.pay.holest.com/clientpay/tos/<merchant_site_uid>?lang=rs
https://pay.holest.com/clientpay/tos/<merchant_site_uid>?lang=en
```

Supported `lang` values: `en`, `rs`, `bs`, `hr`, `me`, `de`, `es`, `gr`, `tr`, `mk`, and others.

This URL returns HTML content (no `<html>` wrapper) and can be:
- Loaded in an `<iframe>`
- Fetched with `fetch()` / AJAX and injected into a modal
- Opened directly in the browser (use `target="_blank"`)

> ?? **Important:** This is **not** a separate "second HolestPay Terms" that replaces your site Terms. The `/clientpay/tos/<merchant_site_uid>` content is a merchant-specific **template/proposal of your own purchase Terms** (with your POS/merchant context), meant to be used as the same Terms of Service page your checkout checkbox points to. Keep one coherent TOS page for customers (your site TOS), and ensure it includes the required payment clauses.

**Implementation — TOS modal using HolestPay built-in dialog** (`hpay_dialog_open` becomes available after `client.loadHPayUI()` resolves):

```javascript
// Call client.loadHPayUI() first to make hpay_dialog_open available:
// HPayInit().then(client => client.loadHPayUI()).then(() => { /* hpay_dialog_open is ready */ });

function openTOS(merchant_site_uid, lang) {
  const tos_url = `https://sandbox.pay.holest.com/clientpay/tos/${merchant_site_uid}?lang=${lang || 'en'}`;
  // Replace sandbox host with pay.holest.com for production
  hpay_dialog_open('tos', 'Terms of Service', tos_url, 'large', {
    ['I understand and accept these Terms of Service']: {
      Run: function(dlg) {
        dlg.close();
        if (dlg.parentNode) dlg.parentNode.removeChild(dlg);
        // Mark TOS as accepted — enable the Pay button or proceed with payment
      },
      action_position: 'center',
      style: { 'min-width': '120px' }
    }
  });
}
```

**Minimum required UI — add this near the Pay button:**

```html
<label style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
  <input type="checkbox" id="tos-accept" required />
  <span>
    I accept the
    <a href="#" onclick="openTOS(MERCHANT_SITE_UID, 'en'); return false;">Terms of Service</a>
  </span>
</label>
<!-- Disable Pay button until checkbox is checked -->
```

```javascript
document.getElementById('tos-accept').addEventListener('change', function() {
  document.getElementById('do-pay').disabled = !this.checked;
});
```

**Checklist for bank approval:**
- [ ] Card brand logos visible in footer (from `HPay.POS.pos_parameters['Logotypes Card Images']`)
- [ ] Bank logos visible in footer, linked (from `HPay.POS.pos_parameters['Logotypes Banks']`)
- [ ] 3DS logos visible in footer, linked (from `HPay.POS.pos_parameters['Logotypes 3DS']`)
- [ ] All logos in a single horizontal line, max ~1 cm height
- [ ] TOS checkbox present near the Pay button, Pay button disabled until checked
- [ ] TOS content matches or includes HolestPay-provided TOS (`/clientpay/tos/<uid>`)

---

## Test Cards for Sandbox Testing

Test card numbers for almost all supported banks and payment methods can be found directly on the **dashboard page** of the HolestPay sandbox panel at https://sandbox.pay.holest.com — no need to search elsewhere. Log in and check the dashboard.

---

## Terms of Service Page — Required Content

The "Terms of Service" page that the customer checks before payment must be **comprehensive and self-contained**. Because the customer ticks a single "I accept the Terms of Service" checkbox that links to this one page, **everything required by the bank and by law must be covered on that exact page** — not spread across separate pages.

The following must be present in the Terms of Service content (either written directly or clearly summarized with a reference to the full document inline):

- **Purchase conditions** — what is being sold, pricing, order confirmation process
- **Delivery policy** — delivery methods, timeframes, costs, geographic coverage
- **Refund and return policy** — conditions, process, and timeframes for refunds and returns
- **PCI DSS statement** — explicit confirmation that card data transfer/processing is secured according to PCI DSS standards and that the merchant does not store raw card data (HolestPay handles card processing on the merchant's behalf)
- **Merchant contact information** — legal business name, address, email, and phone number for customer support

> The merchant may have separate dedicated pages for privacy policy, delivery conditions, refund policy, etc. — that is fine. But **all of the above topics must also appear on the single Terms of Service page** that the payment checkbox links to, because that is the only page the customer is asked to review and accept before paying.

> HolestPay provides a base TOS/purchase-conditions text via `/clientpay/tos/<merchant_site_uid>` that already includes payment-security wording (including PCI DSS transfer handling). Treat it as baseline content for the merchant's own single Terms page, then extend it with delivery, refund, and contact details — not as a separate second TOS document.

---

## Before Requesting Bank Production Approval — Site Readiness

Before the merchant contacts their bank to activate the POS for production, the site must be genuinely complete. A bank technician will review the site manually and will reject it if it does not look like a real, finished online store.

**The following will cause immediate rejection:**

- **Placeholder content** — any "Lorem ipsum", "Coming soon", "Test product", dummy images, or unfilled template text anywhere on the site
- **Mixed languages** — site content, product names, descriptions, and all pages must be consistently in one language (or properly localized if multilingual); a mix of languages on the same page signals an unfinished site
- **Missing or incomplete pages** — the site must have real products/services with actual descriptions and prices, a working checkout flow, and all required legal pages (Terms of Service, Privacy Policy, Contact/About)
- **Non-functional links or broken pages** — every linked page must load and contain real content
- **Test or sandbox data visible** — no test order UIDs, sandbox URLs, or debug output visible to visitors

> Tell the user: *"Before you ask your bank to approve the POS for production, make sure the site looks like a finished, real store. Bank technicians visit the site in person before approving. If they see Lorem ipsum text, empty product pages, mixed languages, or anything that looks unfinished, they will ask you to complete the site first and resubmit — which adds weeks to the process."*

---

## Bank Validation Testing Before Production Approval

Most banks require a **formal validation test** to be performed before activating the POS for production. This is separate from the developer's own sandbox testing.

What this typically involves:

- The bank provides a set of **specific test scenarios** that must be completed successfully (e.g. approved payment, declined payment, 3DS challenge flow, cancellation)
- Tests are performed using **test cards provided by the bank** (which may differ from the general sandbox test cards on the HPay dashboard)
- Results are reported back to the bank (sometimes automatically via their test monitoring system, sometimes manually by the merchant)
- **Whether validation testing is required, and exactly how it is done, depends on the bank and the specific payment method** — some banks require it for every new integration, others only for certain card brands or 3DS configurations

> Tell the user: *"Before your bank activates the production POS, they will likely ask you to run a set of validation test payments using specific test cards they provide. This is a standard bank requirement and is separate from the testing you did during development. Contact your bank's technical integration team to find out exactly what tests are required for your payment method — HolestPay sandbox will be used for these tests as well."*

---

## Shipping Method UI — Required `hpay-sm-options` Placeholder

In the UI element that displays a shipping method's description (the area shown to the buyer when they select a shipping method), you **must include an empty `<span>` with the class `hpay-sm-options` and attribute `hpay_shipping_method_id="<hpay_shipping_method.HPaySiteMethodId>"`**:

```html
<span class="hpay-sm-options" hpay_shipping_method_id="<hpay_shipping_method.HPaySiteMethodId>"></span>
```

HolestPay uses this element to inject additional shipping options and information (e.g. parcel locker selector, delivery time slots, carrier tracking info) for the selected shipping method. Without this placeholder, those features will silently fail to render.

Place it inside the shipping method description container, after any static description text:

```html
<div class="shipping-method-description">
  <!-- your static description text here -->
  <span class="hpay-sm-options" hpay_shipping_method_id="45"></span>
</div>
```

---

## Content Security Policy (CSP) and Site Configuration Requirements

For HolestPay to function correctly, the site must permit the following — ensure these are not blocked by Content Security Policy headers, server configuration, or any firewall/proxy:

### Allowed origins (iframes, scripts, XHR/fetch, content)

The site must allow iframes, scripts, and content loaded from:

```
https://pay.holest.com
https://sandbox.pay.holest.com
```

If the site sets a `Content-Security-Policy` header, include these in all relevant directives:

```
Content-Security-Policy:
  script-src  'self' https://pay.holest.com https://sandbox.pay.holest.com ...;
  frame-src   'self' https://pay.holest.com https://sandbox.pay.holest.com ...;
  connect-src 'self' https://pay.holest.com https://sandbox.pay.holest.com ...;
  img-src     'self' https://pay.holest.com https://sandbox.pay.holest.com data: ...;
```

### `eval()` must be permitted

HolestPay internally uses `eval()` to deserialize and execute certain POS method action functions returned from the server (`initActions`, `orderActions`). The site must **not** block `eval()`.

If a CSP header is set, `'unsafe-eval'` must be present in `script-src`:

```
Content-Security-Policy:
  script-src 'self' 'unsafe-eval' https://pay.holest.com https://sandbox.pay.holest.com ...;
```

> ?? If the site uses a strict CSP that blocks `eval()` or restricts iframes/scripts to specific origins, HolestPay will fail silently or partially. This is one of the most common causes of "nothing happens when I click Pay" during integration.

---

## Dock Container Placement

For each payment method row, use this visual order:
1) method **name/label**
2) method **description**
3) method **dock container** (if that method supports docking)

The dock container must be below the description of the same method, and before the next method name is rendered. It must **not** be placed after the full list of all payment methods.

Each payment method in the list that supports docking (`pm.PayInputUrl` is set) needs its own dock container adjacent to it. Best practice is to wrap **description + dock** in one method-details wrapper that is shown only when that method is selected. When the buyer selects that method, `HPay.setPaymentMethodDock()` renders the embedded payment input directly inside that container.

**Correct structure:**
```html
<label>
  <input type="radio" name="payment_method" value="179" />
  Credit Card
</label>
<div class="method-details is-selected" data-method="179">
  <div class="method-description">Card payment description...</div>
  <div id="paymentMethodDock-179" class="hpay-dock-container"></div>
</div>

<label>
  <input type="radio" name="payment_method" value="180" />
  Bank Transfer
</label>
<div class="method-details" data-method="180">
  <div class="method-description">Bank transfer description...</div>
  <!-- no dock here — this method does not support docking -->
</div>
```

---

## Always Render Method Description When Selected

Whenever a payment method or shipping method is selected by the buyer, **always display its description**. Do not skip or hide descriptions.

Method descriptions sometimes contain **functionally important content** — instructions the buyer must read (e.g. bank account number for manual transfer, pickup location details, special conditions). Hiding or omitting descriptions can cause buyer confusion and failed orders.

For shipping methods, the description container must also include the `<span class="hpay-sm-options" hpay_shipping_method_id="<hpay_shipping_method.HPaySiteMethodId>"></span>` placeholder (see earlier section) so HolestPay can inject carrier-specific UI into it.

---

## Shopify-Specific Notes (if building a Lovable app for Shopify)

### Order creation timing

If this integration is for a Shopify storefront, **all communication with the Shopify API (order creation, order update, fulfillment, etc.) must happen only after receiving the HolestPay payment result** — i.e. inside the `onHPayResult` handler or the POST-back handler on `order_user_url`. Do not create or confirm the Shopify order before payment is confirmed. The HolestPay result contains the definitive payment status (`payment_status: PAID|RESERVED|...`) that should trigger the Shopify order write.

### Shopify API credentials with `write_orders` permission

Shopify access tokens that merchants create themselves in the Shopify admin expire after **24 hours** and cannot be used for permanent integrations.

If a permanent Shopify API access token with `write_orders` permission is needed, the merchant can **borrow the credentials from the HolestPay Shopify app**:

1. In the HolestPay panel, go to site/POS settings for the Shopify site.
2. Find the **SHOPIFY CLIENT SECRET** parameter.
3. Use the **Copy All Shopify APP Credentials...** next to it to copy the credentials.

> ?? **Security warning — tell the user this explicitly:**  
> *"These are shared API credentials tied to the HolestPay Shopify app. Do not share them with anyone, do not commit them to a public repository, and do not expose them in frontend code. Store them as private environment variables on your server only."*

---

## HolestPay Language Codes

HolestPay uses its own language code convention — **do not use standard ISO/BCP-47 codes** for Serbian:

| Language | HolestPay code | ? Do NOT use |
|---|---|---|
| Serbian Latin | `rs` | `sr`, `sr-Latn`, `sr-YU`, `sr-lat` |
| Serbian Cyrillic | `rs-cyr` | `sr-Cyrl`, `sr-cyr`, `cyr` |
| Bosnian | `bs` | |
| Croatian | `hr` | |
| Slovenian | `si` | |
| Macedonian | `mk` | |
| English | `en` | |
| German | `de` | |
| Italian | `it` | |
| Spanish | `es` | |
| French | `fr` | |
| Portuguese | `pt` | |
| Dutch | `nl` | |
| Greek | `el` | `gr` |
| Turkish | `tr` | |

Pass this value as the `language` parameter to `HPayInit()` and as `hpaylang` in `pay_request`.
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:<payment_status>`
  - optional fiscal/integration segments: `<module_uid>_FISCAL:<status>` or `<module_uid>_INTEGR:<status>`
  - optional shipping segments: `<module_uid>_SHIPPING:<packet_no>@<shipping_status>`
- 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 `<script>` tag** that can be copied/pasted directly below the FrontCore Origins field.

FrontCore script URL format:

```
https://sandbox.pay.holest.com/clientpay/handlers/pos/{merchant_site_uid}/frontend-script-core.js
```

Production format:

```
https://pay.holest.com/clientpay/handlers/pos/{merchant_site_uid}/frontend-script-core.js
```

Example (sandbox):

```html
<script src="https://sandbox.pay.holest.com/clientpay/handlers/pos/07f689c5-5bc8-44b6-a563-8facc6870fab/frontend-script-core.js"></script>
```

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 <html lang="..."> 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 = '<p class="hpay-placeholder">Transaction details are not available.</p>';
    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 = '<p class="hpay-placeholder">Not available for this payment.</p>';
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
<form method='POST' id='frmRedirect' action='${order_user_url}'>
  <input type='hidden' name='hpay_forwarded_payment_response' id='hpay_response' />
</form>
<script nonce='${csp_nonce}'>
  document.getElementById('hpay_response').value = JSON.stringify(hpay_result);
  document.getElementById('frmRedirect').submit();
</script>
```

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
<!-- Sandbox admin page -->
<script src="https://sandbox.pay.holest.com/clientpay/cscripts/hpay.js"></script>

<!-- Production admin page -->
<script src="https://pay.holest.com/clientpay/cscripts/hpay.js"></script>
```

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 `<script>` tag |
| `HPayInit()` signature | `HPayInit(language)` — no UID or env needed | `HPayInit(merchant_site_uid, language, environment)` |
| `merchant_site_uid` in `pay_request` | **Not required** — script authenticates automatically | Required |
| `merchant_site_uid` for charges/admin | Required (read from `client.MerchantsiteUid`) | Required |
| Admin script URL | Use normal `.../clientpay/cscripts/hpay.js` | Use normal `.../clientpay/cscripts/hpay.js` |
| `verificationhash` in pay_request | Not required (FrontCore initial checkout) | Required (server-side) |
| Secret Key on frontend | Never needed | Never needed (generate signature server-side) |
| Secret Key for backend charges | Required (server-side) | Required (server-side) |
| Secret Key for admin ops | Required (server-side or admin page) | Required (server-side or admin page) |
| Origin whitelist required | Yes — configure in HPay panel | No |
| Result event handling | Identical | Identical |

---

## Implementation Precision Addendum

Use this section as an execution checklist to reduce integration mistakes and speed up debugging.

### FrontCore Safety Rules (Most Important)

- FrontCore initial checkout request does not require `verificationhash`; trusted origin performs signing.
- FrontCore script must be loaded from `.../handlers/pos/{merchant_site_uid}/frontend-script-core.js`.
- Current page origin must match `Frontend Script-Core Origins` rule list; otherwise `HolestPayCheckout` will not initialize.
- Keep one origin/pattern per line in HPay panel and verify wildcard scope carefully before production.
- Use normal `.../clientpay/cscripts/hpay.js` for admin tooling pages; do not use FrontCore handler script for admin.

### Request Validation Rules (Before Calling HPay)

- `order_uid` must be unique per order attempt in your system; do not reuse successful IDs for new purchases.
- `merchant_site_uid` must belong to the same environment where script/API is used (`sandbox` with sandbox, `production` with production).
- `order_amount` must be stable across checkout form, dock config, and presented request.
- `order_currency` must be a valid ISO 4217 code configured on your POS methods.
- `payment_method` must be `pm.HPaySiteMethodId` (not `pm.Uid`).
- `shipping_method` should be omitted if shipping is not used; send it only when user selected a supported shipping option.
- `notify_url` should be a stable server endpoint; avoid temporary dev tunnels for production behavior testing.
- `order_user_url` should point to your own order/thank-you page and include enough context to restore order state.
- `order_data` is ideal for stable metadata like source channel, cart fingerprint, CRM ID, campaign tags, or internal correlation keys.
- Remove empty fields before sending; avoid null/empty noise in logs and state snapshots.

### Signature and Verification Precision (Server Side)

- Even with FrontCore, backend charge and result verification still require Secret Key and signature validation.
- Use exactly the documented signature field order and amount formatting (`toFixed(8)` / `number_format(..., 8)`).
- Compare signatures using exact lowercase hex strings.
- `verificationhash` is used for requests to HPay; `vhash` is used in responses from HPay.
- Verify signatures in both places: browser event result handling path and webhook path.
- Treat unsigned or signature-invalid payload as non-authoritative and do not fulfill order.

### Webhook Reliability Pattern

- Implement webhook idempotency keyed by transaction/order identifiers from result payload.
- Accept webhook retry behavior; return success response only after durable persistence.
- Process business actions once, even if webhook arrives multiple times or after browser callback.
- Record webhook receive timestamp and raw payload snapshot for auditability.
- Never rely on callback ordering between browser event and webhook; either can arrive first.
- Use webhook as final server authority for fulfillment transitions.

### Status Handling Strategy

- Handle `PAID`, `SUCCESS`, `PAYING`, `RESERVED`, `OBLIGATED`, `AWAITING`, `FAILED`, `REFUSED`, `CANCELED`, `VOID`, `REFUNDED`, `PARTIALLY-REFUNDED`, `EXPIRED`, `OVERDUE`.
- Map payment status to business actions explicitly:
- `PAID`/`SUCCESS`: fulfill when signature is valid on server.
- `PAYING`: partially paid; keep order open according to your policy.
- `RESERVED`: authorization only; wait for capture before final fulfillment.
- `AWAITING`/`OBLIGATED`: pending/offline or legally committed flows; these are **not failed** statuses. Do not treat them as card-capture success, but show `payment_html` on thank-you page (often contains account/invoice payment instructions).
- Cart handling rule: clear cart for `PAID`, `RESERVED`, `PAYING`, `AWAITING`, and `OBLIGATED` (same cart behavior for all these statuses).
- `FAILED`/`REFUSED`/`CANCELED`/`EXPIRED`/`OVERDUE`: stop fulfillment and show retry/payment-link path.
- Parse composed HolestPay status (`PAYMENT ... _FISCAL/_INTEGR ... _SHIPPING ...`) as operational context, not payment result only.

### Shipping and Dispenser Precision

- `dispenser`, `dispenser_desc`, and `dispenser_method_id` are meaningful only for shipping methods that support locker/paket-shop mode.
- `dispenser_method_id` must match top-level `shipping_method` to be considered in downstream processing.
- Keep shipping address and shipping method logically aligned (country/state/postcode compatibility).

### Payload Size and Data Modeling

- Keep total serialized order payload below `64 KB`.
- Prefer short stable keys in `order_data`; avoid embedding huge documents.
- Do not place sensitive secrets in `order_data`, `order_items`, or any client-visible payload field.
- Put only metadata needed for reconciliation/debugging into request payload.

### Frontend and Backend Responsibility Split

- Frontend: collect user choices, call initialized HPay APIs, listen to events, render status.
- Backend: verify result signatures, process webhook, enforce idempotency, persist state.
- Backend charge/admin: generate signatures and run protected operations with Secret Key.

### Recommended Test Matrix (Minimal but Complete)

- Test FrontCore script load on an allowed origin and verify failure on a non-allowed origin.
- Test success payment and failed payment (validation/card failure).
- Test close panel before completion and verify no accidental fulfillment.
- Test retry after `onHPayResult` error flow.
- Test with and without shipping section.
- Test one request containing `order_data` and confirm data appears in resulting order `Data`.
- Test one backend charge request with existing `vault_token_uid`.
- Test webhook replay (send same payload twice) and confirm idempotent behavior.
- Test signature mismatch scenario and verify order is not fulfilled.
- Test one `RESERVED`/authorization-capable method if available and confirm capture-dependent flow.

### Logging and Observability

- Log correlation tuple for each request: `order_uid`, `merchant_site_uid`, environment, method IDs, and internal user/session key.
- Store raw HPay response payloads for troubleshooting (with sensitive-data policy applied).
- Keep separate logs for browser callback processing and webhook processing.
- Add clear audit events: `PAYMENT_INIT`, `PAYMENT_RESULT_RECEIVED`, `WEBHOOK_RECEIVED`, `SIGNATURE_VALIDATED`, `FULFILLMENT_TRIGGERED`.

### Common Integration Mistakes to Avoid

- Forgetting to whitelist the exact test/prod origin for FrontCore.
- Using `pm.Uid` instead of `pm.HPaySiteMethodId`.
- Mixing sandbox credentials with production script/API domain.
- Trusting browser callback without server-side verification.
- Reusing old `order_uid` across retries/orders.
- Treating all non-error statuses as final success.
- Sending huge custom payloads over `64 KB`.

### Field Constraints Quick Reference

| Field | Required | Typical format | Precision note |
|---|---|---|---|
| `merchant_site_uid` | Yes | UUID-like string | Must match FrontCore script path and environment POS |
| `hpaylang` | No | `en`, `rs`, `de`, ... | Keep consistent across UI and requests |
| `order_uid` | Yes | App-specific unique string | Unique per purchase attempt |
| `order_name` | No | Human-readable label | Useful for support and logs |
| `order_amount` | Yes | Numeric string | Use same value across client/server |
| `order_currency` | Yes | ISO 4217 | Must be supported by selected method |
| `payment_method` | Yes | `HPaySiteMethodId` string | Never use `pm.Uid` here |
| `shipping_method` | No | `HPaySiteMethodId` string | Send only when shipping is used |
| `order_user_url` | No | HTTPS URL | Browser flow only |
| `notify_url` | No | Public HTTPS URL | Webhook authority endpoint |
| `order_data` | No | Object/map | Stored in resulting order `Data` |
| `cof` | No | `optional|required|none` | Checkout card-save behavior |
| `vault_token_uid` | No | Token UUID or `1|true|new` | Saved card or save-request hint |
| `verificationhash` | No (initial FrontCore pay) | SHA-512 lowercase hex | Required for backend charge/verification |
| `order_billing.email` | No | Email | Validate syntax before send |
| `order_billing.phone` | No | E.164-like | Keep normalized |
| `order_billing.country` | No | ISO-2 | Use uppercase (`RS`, `US`) |
| `order_shipping.country` | No | ISO-2 | Align with shipping method support |
| `order_shipping.dispenser` | No | Provider ID | Locker/paket-shop only |
| `order_shipping.dispenser_method_id` | No | Method ID string | Must match `shipping_method` |
| `order_items[].posuid` | Yes | String/number | Merchant-defined internal item identifier (not a fixed HolestPay enum) |
| `order_items[].name` | Yes | String | Human-readable item name (not `title`) |
| `order_items[].qty` | Yes | Number/string | Positive quantity expected (not `quantity`) |
| `order_items[].price` | No | Number/string | Keep pricing model consistent |
| `order_items[].subtotal` | Yes | Number/string | Mandatory line subtotal |
| `order_items[].virtual` | No | `0|1` | Shipping relevance hint |

### FrontCore Origin and Script Diagnostics

- Validate script URL format exactly: `/clientpay/handlers/pos/{merchant_site_uid}/frontend-script-core.js`.
- Confirm script host matches environment (`sandbox.pay.holest.com` vs `pay.holest.com`).
- Confirm browser page origin is included in `Frontend Script-Core Origins`.
- Use explicit startup guard for missing global:

```javascript
setTimeout(() => {
  if (typeof HolestPayCheckout === 'undefined') {
    console.error('FrontCore script not loaded: verify origin whitelist and script URL.');
  }
}, 5000);
```

- Keep whitelist entries minimal and deliberate; avoid broad wildcards on production unless absolutely necessary.
- Re-check whitelist when moving from local/dev domain to staging/prod domain.

### Server Workflow Blueprint (FrontCore)

```text
1) Frontend loads FrontCore script from handlers/pos/{merchant_site_uid} URL.
2) Frontend initializes HPay and builds pay_request (without verificationhash).
3) Frontend calls HPay.presentHPayPayForm(pay_request).
4) Frontend receives onHPayResult and forwards payload to backend for logging.
5) Backend verifies result signature for authoritative processing.
6) Webhook arrives (possibly before/after browser callback).
7) Backend verifies webhook signature and applies idempotent state transition.
8) Fulfillment runs only once after verified terminal/success policy conditions.
9) For MIT/COF backend charge, backend builds charge_request + verificationhash and calls charge endpoint.
```

### Idempotent Webhook Handling Example (Node.js Pseudocode)

```javascript
// Pseudocode only: adapt to your DB and framework.
app.post('/webhooks/hpay', async (req, res) => {
  const payload = req.body || {};
  const orderUid = String(payload.order_uid || '');
  const txUid = String(payload.transaction_uid || '');
  const idempotencyKey = `${orderUid}:${txUid}:${payload.status || ''}`;

  if (!orderUid) return res.status(400).json({ ok: false, error: 'missing order_uid' });

  // Signature validation is still mandatory for backend authority
  const calculated = generatePOSRequestSignature(MERCHANT_SITE_UID, SECRET_KEY, payload);
  const valid = calculated === String(payload.vhash || '').toLowerCase();
  if (!valid) return res.status(400).json({ ok: false, error: 'invalid signature' });

  const alreadyProcessed = await db.idempotency.exists(idempotencyKey);
  if (alreadyProcessed) return res.status(200).json({ ok: true, duplicate: true });

  await db.tx(async trx => {
    await trx.idempotency.insert({ key: idempotencyKey, createdAt: new Date() });
    await trx.hpayEvents.insert({
      orderUid,
      transactionUid: txUid,
      paymentStatus: payload.payment_status || null,
      orderStatus: payload.status || null,
      raw: JSON.stringify(payload),
      source: 'webhook'
    });

    const paymentStatus = String(payload.payment_status || '').toUpperCase();
    if (/PAID|SUCCESS|PAYING|RESERVED|OBLIGATED/.test(paymentStatus)) {
      await trx.orders.markAsPaidOrReserved(orderUid, paymentStatus);
      await trx.jobs.enqueue({ type: 'FULFILLMENT_DECISION', orderUid });
    } else if (/AWAITING/.test(paymentStatus)) {
      // Not failed: keep order pending and show payment_html instructions on return/thank-you page
      await trx.orders.markAsPendingPaymentInstructions(orderUid, paymentStatus);
    } else {
      await trx.orders.markAsPaymentNotCompleted(orderUid, paymentStatus);
    }
  });

  return res.status(200).json({ ok: true });
});
```

### Browser Callback + Webhook Reconciliation Rules

- If browser callback succeeds first, store it as provisional until webhook check completes.
- If webhook succeeds first, mark server truth immediately; browser callback only enriches logs/UI.
- If callback says success but webhook never arrives, run delayed status query before fulfillment.
- If callback and webhook disagree, trust verified server-side signature + latest authoritative status policy.
- Never ship goods/services exclusively from client-side event data.

### Operational State Model (Recommended)

- `ORDER_CREATED`: order exists locally before payment started.
- `PAYMENT_INITIATED`: FrontCore request sent to payment UI.
- `PAYMENT_CALLBACK_RECEIVED`: browser event stored.
- `WEBHOOK_RECEIVED`: webhook accepted and verified.
- `PAYMENT_CONFIRMED`: business rule says financial completion met.
- `FULFILLMENT_PENDING`: queued for shipping/fiscal/integration actions.
- `FULFILLED`: goods/services delivered or process completed.
- `PAYMENT_FAILED`: terminal non-success status.
- `CLOSED`: final immutable state with audit trail.

### Production Readiness Checklist

- Keep `Frontend Script-Core Origins` strict and reviewed per deployment.
- Protect admin/backend code paths that collect/use Secret Key.
- Add request/response size limits and payload schema validation at backend edge.
- Enable TLS-only endpoints for checkout and webhook.
- Add monitoring alarms for missing FrontCore script load events and webhook signature failures.
- Capture dashboard metrics: conversion by method, callback-to-webhook lag, duplicate webhook rate.
- Create runbook for manual recovery: status query, refund/void/capture decisions, replay-safe reprocessing.
- Test environment cutover with explicit checklist (POS UID, methods, notify URLs, allowed origins, secrets).

### Troubleshooting Cookbook

- Symptom: `HolestPayCheckout` is undefined.
- Check: origin whitelist entry, script URL path includes correct `merchant_site_uid`, environment host.
- Fix: add exact HTTPS test origin to `Frontend Script-Core Origins` and reload.

- Symptom: FrontCore works on one domain but not another.
- Check: wildcard pattern coverage and path-level matching assumptions.
- Fix: add explicit origin/pattern entry for each domain stage (dev/stage/prod) and retest.

- Symptom: payment UI opens but methods list is empty.
- Check: POS method activation in panel, country/amount filters, currency compatibility.
- Fix: verify `availablePaymentMethods(...)` inputs and POS configuration in same environment.

- Symptom: result arrives in browser but order not updated on server.
- Check: callback forwarding endpoint, webhook delivery logs, signature verification result.
- Fix: persist callback as provisional and complete state transition on verified webhook.

- Symptom: signature mismatch on webhook/backend verification.
- Check: amount normalization to 8 decimals, field order in signature string, correct secret key/environment.
- Fix: use one shared signature utility across all server handlers and add test vectors.

- Symptom: webhook endpoint receives duplicates.
- Check: idempotency key existence and transaction wrapping around insert/process logic.
- Fix: move idempotency write before business action and enforce unique DB constraint on key.

- Symptom: charge request fails with saved token.
- Check: token validity/scope, method supports `charge`, signature correctness, environment match.
- Fix: use fresh token tied to same merchant scope and verify method `POps` includes `charge`.

- Symptom: user is not redirected to thank-you URL.
- Check: method iframe/redirect behavior and callback handling.
- Fix: redirect manually in success branch using `r.order_user_url` when appropriate.

### Suggested CI/QA Validation Cases

- Validate JSON schema for `pay_request` and `charge_request` in unit tests.
- Validate signature generation against fixed fixtures in PHP and Node.js implementations.
- Validate idempotency behavior by replaying same webhook payload at least 3 times.
- Validate parser for composed status strings with and without fiscal/shipping segments.
- Validate route-level access control for admin/backend operations requiring Secret Key.
- Validate payload size guard rejects requests above `64 KB`.
- Validate environment guard rejects sandbox credentials on production host and vice versa.
- Validate origin guard behavior: whitelisted origin passes, non-whitelisted origin fails predictably.
- Validate correlation logs always include `order_uid` and `merchant_site_uid`.

### Security No-Go List

- Never hardcode `secret_key` in browser JavaScript, HTML templates, or mobile bundle assets.
- Never process fulfillment on unverified callback payload.
- Never expose raw webhook endpoint without rate limiting and request body size limits.
- Never trust query params from `order_user_url` alone as proof of payment.
- Never log secret values or full PAN/cardholder data in application logs.
- Never reuse one `order_uid` for multiple independent purchases.

### Terminology Quick Map

- `POS` in HolestPay = your app/site sales endpoint, not a physical cashier terminal.
- `FrontCore` = JS-first flow where initial checkout signing is origin-trust based.
- `status` = composed platform state across payment/fiscal/shipping modules.
- `payment_status` = payment module status focus.
- `verificationhash` = request signature hash used for backend charge/operations.
- `vhash` = response signature hash received from HPay result/webhook.
- `order_data` = custom merchant metadata persisted to order `Data`.

---

## Key Object Reference

All fields supported by the template are listed in `Step 2` under `Full Template Field Catalog (Standalone)`.

---

## Important Notes

- The page must be served over **HTTPS**. HPay will not work on `file://` or plain HTTP.
- `payment_method` value is `pm.HPaySiteMethodId` — **not** `pm.Uid` (these are different).
- `availablePaymentMethods(country, amount, currency)` returns methods available for the given buyer country and order. Use billing country for payment, shipping country for shipping.
- If `pm['Use IFRAME'] === false`, the method uses redirect — your page cannot receive the postback inline. Proper server-side integration (webhook) is required.
- After a successful payment, generate a new `order_uid` for the next order — never reuse UIDs.
- `order_data` supports custom metadata and is stored as HPay order `Data`.
- `order_billing` and `order_shipping` can include additional custom fields, but keep total serialized order payload under **64 KB**.
- If your FrontCore page also needs to display the Secret Key prompt (e.g. for admin ops), collect it via a prompt/dialog at runtime — never hardcode it in the page source.
- The Secret Key may be collected once per session (e.g. via `prompt()` or an admin login flow) and stored in a local variable for use during the current session only.

---

## Required for Bank Production Approval — Logotypes and Terms of Service

> ?? **This section has nothing to do with payment functionality** — payments work without it. However, **almost all banks in the region will require these elements to be present on the site before approving the POS for production use.** Implement these alongside the payment integration.

---

### Footer Logotypes (Card logos, Bank logos, 3DS logos)

Banks require card brand logos, acquiring bank logos, and 3DS security logos to appear in the site footer — in a single horizontal line, **no taller than approximately 1 cm visually**. Order: card logos first, bank logos in the middle (with some spacing on both sides), 3DS logos on the right. Bank and 3DS logos must be linked. The footer strip must be visible on **all pages of the website**, not only on checkout.

HolestPay provides all logo image URLs and link targets via `HPay.POS.pos_parameters` after `HPayInit()`. Use them — do not source logos elsewhere.

**CSS for the footer branding strip:**

```css
.hpay_footer_branding_wrapper {
  width: 100%;
  padding: 8px 0;
  border-top: 1px solid #e0e0e0;
}
.hpay_footer_branding {
  display: flex;
  align-items: center;
  gap: 0;
  flex-wrap: nowrap;
}
.hpay-footer-branding-cards,
.hpay-footer-branding-bank,
.hpay-footer-branding-3ds {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
}
.hpay-footer-branding-bank  { margin: 0 12px; }
.hpay_footer_branding img   { height: 1cm; width: auto; display: inline-block; }
```

**JavaScript — call this after `HPayInit()` resolves:**

```javascript
function renderHPayFooterLogotypes() {
  if (!(typeof HPay !== 'undefined' && HPay && HPay.POS)) return;
  if (!HPay.POS.pos_parameters) return;

  let card_images_html = '';
  let banks_html       = '';
  let threes_html      = '';

  if (HPay.POS.pos_parameters['Logotypes Card Images']) {
    HPay.POS.pos_parameters['Logotypes Card Images'].split('\n').forEach(src => {
      if (src.trim()) card_images_html += `<img src="${src.trim()}" alt="Card" />`;
    });
  }

  if (HPay.POS.pos_parameters['Logotypes Banks']) {
    HPay.POS.pos_parameters['Logotypes Banks'].split('\n').forEach(line => {
      if (!line.trim()) return;
      // format: "imageUrl:linkUrl" — colons in URLs are escaped before splitting
      const t = line.replace(/https:/gi, '-PS-').replace(/http:/gi, '-P-')
        .split(':').map(r => r.replace(/-P-/g, 'http:').replace(/-PS-/g, 'https:'));
      banks_html += t.length > 1
        ? `<a href="${t[1]}" target="_blank"><img src="${t[0]}" alt="Bank" /></a>`
        : `<img src="${t[0]}" alt="Bank" />`;
    });
  }

  if (HPay.POS.pos_parameters['Logotypes 3DS']) {
    HPay.POS.pos_parameters['Logotypes 3DS'].split('\n').forEach(line => {
      if (!line.trim()) return;
      const t = line.replace(/https:/gi, '-PS-').replace(/http:/gi, '-P-')
        .split(':').map(r => r.replace(/-P-/g, 'http:').replace(/-PS-/g, 'https:'));
      threes_html += t.length > 1
        ? `<a href="${t[1]}" target="_blank"><img src="${t[0]}" alt="3DS" /></a>`
        : `<img src="${t[0]}" alt="3DS" />`;
    });
  }

  const logotypesDiv = document.createElement('div');
  logotypesDiv.className = 'hpay_footer_branding';
  logotypesDiv.innerHTML =
    `<div class="hpay-footer-branding-cards">${card_images_html}</div>` +
    `<div class="hpay-footer-branding-bank">${banks_html}</div>` +
    `<div class="hpay-footer-branding-3ds">${threes_html}</div>`;

  const wrapper = document.createElement('div');
  wrapper.className = 'hpay_footer_branding_wrapper';
  wrapper.appendChild(logotypesDiv);

  (document.querySelector('footer') || document.querySelector('main') || document.body)
    .appendChild(wrapper);
}

// Call after HPayInit resolves:
// HPayInit().then(client => { renderHPayFooterLogotypes(); });
```

---

### Terms of Service Acceptance Checkbox

Almost all banks in the region require the customer to explicitly accept the site Terms of Service before payment — typically a checkbox with a link to the TOS content.

**HolestPay provides a ready-to-use Terms of Service / Purchase Conditions template page for each POS/site:**

```
https://sandbox.pay.holest.com/clientpay/tos/<merchant_site_uid>?lang=rs
https://pay.holest.com/clientpay/tos/<merchant_site_uid>?lang=en
```

Supported `lang` values: `en`, `rs`, `bs`, `hr`, `me`, `de`, `es`, `gr`, `tr`, `mk`, and others.

This URL returns HTML content (no `<html>` wrapper) and can be:
- Loaded in an `<iframe>`
- Fetched with `fetch()` / AJAX and injected into a modal
- Opened directly in the browser (use `target="_blank"`)

> ?? **Important:** This is **not** a separate "second HolestPay Terms" that replaces your site Terms. The `/clientpay/tos/<merchant_site_uid>` content is a merchant-specific **template/proposal of your own purchase Terms** (with your POS/merchant context), meant to be used as the same Terms of Service page your checkout checkbox points to. Keep one coherent TOS page for customers (your site TOS), and ensure it includes the required payment clauses.

**Implementation — TOS modal using HolestPay built-in dialog** (`hpay_dialog_open` becomes available after `client.loadHPayUI()` resolves):

```javascript
// Call client.loadHPayUI() first to make hpay_dialog_open available:
// HPayInit().then(client => client.loadHPayUI()).then(() => { /* hpay_dialog_open is ready */ });

function openTOS(merchant_site_uid, lang) {
  const tos_url = `https://sandbox.pay.holest.com/clientpay/tos/${merchant_site_uid}?lang=${lang || 'en'}`;
  // Replace sandbox host with pay.holest.com for production
  hpay_dialog_open('tos', 'Terms of Service', tos_url, 'large', {
    ['I understand and accept these Terms of Service']: {
      Run: function(dlg) {
        dlg.close();
        if (dlg.parentNode) dlg.parentNode.removeChild(dlg);
        // Mark TOS as accepted — enable the Pay button or proceed with payment
      },
      action_position: 'center',
      style: { 'min-width': '120px' }
    }
  });
}
```

**Minimum required UI — add this near the Pay button:**

```html
<label style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
  <input type="checkbox" id="tos-accept" required />
  <span>
    I accept the
    <a href="#" onclick="openTOS(MERCHANT_SITE_UID, 'en'); return false;">Terms of Service</a>
  </span>
</label>
<!-- Disable Pay button until checkbox is checked -->
```

```javascript
document.getElementById('tos-accept').addEventListener('change', function() {
  document.getElementById('do-pay').disabled = !this.checked;
});
```

**Checklist for bank approval:**
- [ ] Card brand logos visible in footer (from `HPay.POS.pos_parameters['Logotypes Card Images']`)
- [ ] Bank logos visible in footer, linked (from `HPay.POS.pos_parameters['Logotypes Banks']`)
- [ ] 3DS logos visible in footer, linked (from `HPay.POS.pos_parameters['Logotypes 3DS']`)
- [ ] All logos in a single horizontal line, max ~1 cm height
- [ ] TOS checkbox present near the Pay button, Pay button disabled until checked
- [ ] TOS content matches or includes HolestPay-provided TOS (`/clientpay/tos/<uid>`)

---

## Test Cards for Sandbox Testing

Test card numbers for almost all supported banks and payment methods can be found directly on the **dashboard page** of the HolestPay sandbox panel at https://sandbox.pay.holest.com — no need to search elsewhere. Log in and check the dashboard.

---

## Terms of Service Page — Required Content

The "Terms of Service" page that the customer checks before payment must be **comprehensive and self-contained**. Because the customer ticks a single "I accept the Terms of Service" checkbox that links to this one page, **everything required by the bank and by law must be covered on that exact page** — not spread across separate pages.

The following must be present in the Terms of Service content (either written directly or clearly summarized with a reference to the full document inline):

- **Purchase conditions** — what is being sold, pricing, order confirmation process
- **Delivery policy** — delivery methods, timeframes, costs, geographic coverage
- **Refund and return policy** — conditions, process, and timeframes for refunds and returns
- **PCI DSS statement** — explicit confirmation that card data transfer/processing is secured according to PCI DSS standards and that the merchant does not store raw card data (HolestPay handles card processing on the merchant's behalf)
- **Merchant contact information** — legal business name, address, email, and phone number for customer support

> The merchant may have separate dedicated pages for privacy policy, delivery conditions, refund policy, etc. — that is fine. But **all of the above topics must also appear on the single Terms of Service page** that the payment checkbox links to, because that is the only page the customer is asked to review and accept before paying.

> HolestPay provides a base TOS/purchase-conditions text via `/clientpay/tos/<merchant_site_uid>` that already includes payment-security wording (including PCI DSS transfer handling). Treat it as baseline content for the merchant's own single Terms page, then extend it with delivery, refund, and contact details — not as a separate second TOS document.

---

## Before Requesting Bank Production Approval — Site Readiness

Before the merchant contacts their bank to activate the POS for production, the site must be genuinely complete. A bank technician will review the site manually and will reject it if it does not look like a real, finished online store.

**The following will cause immediate rejection:**

- **Placeholder content** — any "Lorem ipsum", "Coming soon", "Test product", dummy images, or unfilled template text anywhere on the site
- **Mixed languages** — site content, product names, descriptions, and all pages must be consistently in one language (or properly localized if multilingual); a mix of languages on the same page signals an unfinished site
- **Missing or incomplete pages** — the site must have real products/services with actual descriptions and prices, a working checkout flow, and all required legal pages (Terms of Service, Privacy Policy, Contact/About)
- **Non-functional links or broken pages** — every linked page must load and contain real content
- **Test or sandbox data visible** — no test order UIDs, sandbox URLs, or debug output visible to visitors

> Tell the user: *"Before you ask your bank to approve the POS for production, make sure the site looks like a finished, real store. Bank technicians visit the site in person before approving. If they see Lorem ipsum text, empty product pages, mixed languages, or anything that looks unfinished, they will ask you to complete the site first and resubmit — which adds weeks to the process."*

---

## Bank Validation Testing Before Production Approval

Most banks require a **formal validation test** to be performed before activating the POS for production. This is separate from the developer's own sandbox testing.

What this typically involves:

- The bank provides a set of **specific test scenarios** that must be completed successfully (e.g. approved payment, declined payment, 3DS challenge flow, cancellation)
- Tests are performed using **test cards provided by the bank** (which may differ from the general sandbox test cards on the HPay dashboard)
- Results are reported back to the bank (sometimes automatically via their test monitoring system, sometimes manually by the merchant)
- **Whether validation testing is required, and exactly how it is done, depends on the bank and the specific payment method** — some banks require it for every new integration, others only for certain card brands or 3DS configurations

> Tell the user: *"Before your bank activates the production POS, they will likely ask you to run a set of validation test payments using specific test cards they provide. This is a standard bank requirement and is separate from the testing you did during development. Contact your bank's technical integration team to find out exactly what tests are required for your payment method — HolestPay sandbox will be used for these tests as well."*

---

## Shipping Method UI — Required `hpay-sm-options` Placeholder

In the UI element that displays a shipping method's description (the area shown to the buyer when they select a shipping method), you **must include an empty `<span>` with the class `hpay-sm-options` and attribute `hpay_shipping_method_id="<hpay_shipping_method.HPaySiteMethodId>"`**:

```html
<span class="hpay-sm-options" hpay_shipping_method_id="<hpay_shipping_method.HPaySiteMethodId>"></span>
```

HolestPay uses this element to inject additional shipping options and information (e.g. parcel locker selector, delivery time slots, carrier tracking info) for the selected shipping method. Without this placeholder, those features will silently fail to render.

Place it inside the shipping method description container, after any static description text:

```html
<div class="shipping-method-description">
  <!-- your static description text here -->
  <span class="hpay-sm-options" hpay_shipping_method_id="45"></span>
</div>
```

---

## Content Security Policy (CSP) and Site Configuration Requirements

For HolestPay to function correctly, the site must permit the following — ensure these are not blocked by Content Security Policy headers, server configuration, or any firewall/proxy:

### Allowed origins (iframes, scripts, XHR/fetch, content)

The site must allow iframes, scripts, and content loaded from:

```
https://pay.holest.com
https://sandbox.pay.holest.com
```

If the site sets a `Content-Security-Policy` header, include these in all relevant directives:

```
Content-Security-Policy:
  script-src  'self' https://pay.holest.com https://sandbox.pay.holest.com ...;
  frame-src   'self' https://pay.holest.com https://sandbox.pay.holest.com ...;
  connect-src 'self' https://pay.holest.com https://sandbox.pay.holest.com ...;
  img-src     'self' https://pay.holest.com https://sandbox.pay.holest.com data: ...;
```

### `eval()` must be permitted

HolestPay internally uses `eval()` to deserialize and execute certain POS method action functions returned from the server (`initActions`, `orderActions`). The site must **not** block `eval()`.

If a CSP header is set, `'unsafe-eval'` must be present in `script-src`:

```
Content-Security-Policy:
  script-src 'self' 'unsafe-eval' https://pay.holest.com https://sandbox.pay.holest.com ...;
```

> ?? If the site uses a strict CSP that blocks `eval()` or restricts iframes/scripts to specific origins, HolestPay will fail silently or partially. This is one of the most common causes of "nothing happens when I click Pay" during integration.

---

## Dock Container Placement

For each payment method row, use this visual order:
1) method **name/label**
2) method **description**
3) method **dock container** (if that method supports docking)

The dock container must be below the description of the same method, and before the next method name is rendered. It must **not** be placed after the full list of all payment methods.

Each payment method in the list that supports docking (`pm.PayInputUrl` is set) needs its own dock container adjacent to it. Best practice is to wrap **description + dock** in one method-details wrapper that is shown only when that method is selected. When the buyer selects that method, `HPay.setPaymentMethodDock()` renders the embedded payment input directly inside that container.

**Correct structure:**
```html
<label>
  <input type="radio" name="payment_method" value="179" />
  Credit Card
</label>
<div class="method-details is-selected" data-method="179">
  <div class="method-description">Card payment description...</div>
  <div id="paymentMethodDock-179" class="hpay-dock-container"></div>
</div>

<label>
  <input type="radio" name="payment_method" value="180" />
  Bank Transfer
</label>
<div class="method-details" data-method="180">
  <div class="method-description">Bank transfer description...</div>
  <!-- no dock here — this method does not support docking -->
</div>
```

---

## Always Render Method Description When Selected

Whenever a payment method or shipping method is selected by the buyer, **always display its description**. Do not skip or hide descriptions.

Method descriptions sometimes contain **functionally important content** — instructions the buyer must read (e.g. bank account number for manual transfer, pickup location details, special conditions). Hiding or omitting descriptions can cause buyer confusion and failed orders.

For shipping methods, the description container must also include the `<span class="hpay-sm-options" hpay_shipping_method_id="<hpay_shipping_method.HPaySiteMethodId>"></span>` placeholder (see earlier section) so HolestPay can inject carrier-specific UI into it.

---

## HolestPay Language Codes

HolestPay uses its own language code convention — **do not use standard ISO/BCP-47 codes** for Serbian:

| Language | HolestPay code | ? Do NOT use |
|---|---|---|
| Serbian Latin | `rs` | `sr`, `sr-Latn`, `sr-YU`, `sr-lat` |
| Serbian Cyrillic | `rs-cyr` | `sr-Cyrl`, `sr-cyr`, `cyr` |
| Bosnian | `bs` | |
| Croatian | `hr` | |
| Slovenian | `si` | |
| Macedonian | `mk` | |
| English | `en` | |
| German | `de` | |
| Italian | `it` | |
| Spanish | `es` | |
| French | `fr` | |
| Portuguese | `pt` | |
| Dutch | `nl` | |
| Greek | `el` | `gr` |
| Turkish | `tr` | |

Pass this value as the `language` parameter to `HPayInit()` and as `hpaylang` in `pay_request`.
Posted on

HolestPay Payment Integration Guide for AI Coding Assistants (Standard)

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_STANDARD.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 Payment Integration Guide for AI Coding Assistants (Standard) – MD file…

# HolestPay Integration Guide — Standard (Server-Side Signature)

> This guide is written for AI coding assistants. It describes how to implement HolestPay payment integration using the **standard approach** where a server-side signature is required for every payment request.  
> Reference implementation: `hpay_html_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.

---

## 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 must be activated on the POS in the HPay panel.
3. From the HPay panel ? site/POS settings, you need:
   - **Merchant Site UID** (`merchant_site_uid`) — public identifier of the POS
   - **POS Secret Key** (`secret_key`) — used only on the **server side** for signature generation. **Never expose this on the frontend. **

---

## 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.
- 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 Standard guide, start from the standard generated sample (based on `hpay_html_sample.html`) and verify signature generation/verification flow 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:<payment_status>`
  - optional fiscal/integration segments: `<module_uid>_FISCAL:<status>` or `<module_uid>_INTEGR:<status>`
  - optional shipping segments: `<module_uid>_SHIPPING:<packet_no>@<shipping_status>`
- 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.

---

## Integration Choice Recommendation

Use **Standard integration** as the default/recommended approach for most real projects.

- Standard gives the clearest backend ownership of signing, verification, idempotency, and fulfillment control.
- Standard is usually better for long-term maintainability, audits, security reviews, and complex order workflows.
- FrontCore should be treated as a fast-connect option when you need to get checkout running quickly with minimal backend work.
- If you choose FrontCore for speed, still keep server-side verification and webhook authority as mandatory production rules.

---

## How It Works — Overview

```
Browser                          Your Server                    HPay Server
  |                                   |                               |
  |-- build pay_request ------------> |                               |
  |                                   |-- generate signature -------> |
  |<-- return signed pay_request ---- |                               |
  |                                   |                               |
  |-- HPayInit() + presentHPayPayForm() ---------------------------> |
  |                                   |                               |
  |<-- onHPayResult (payment result) --------------------------------|
  |                                   |                               |
  |          (server-to-server) <-----|-- notify_url webhook --------|
```

The key difference from FrontCore: **the signature is computed on your backend** using the Secret Key, and only the signed `pay_request` is sent to the browser.

---

## Step 0 — Load the HPay Script

HPay's JavaScript is loaded from the payment server. Add this to your HTML `<head>` or just before `</body>`:

```html
<!-- For sandbox: -->
<script src="https://sandbox.pay.holest.com/clientpay/cscripts/hpay.js"></script>

<!-- For production: -->
<script src="https://pay.holest.com/clientpay/cscripts/hpay.js"></script>
```

After loading, the global `HPay` object and the `HPayInit()` function become available.

When the HPay script is loaded, it also exposes these globals on `window`:
- `window.presentHPayPayForm` — function
- `window.HPayIsSandbox` — environment flag variable

---

## Step 1 — Initialize HPay and Fetch POS Configuration

Call `HPayInit()` once after the script loads. It returns a Promise that resolves with `client` (same reference as global `HPay`).

```javascript
HPayInit(
  merchant_site_uid,   // string — your Merchant Site UID
  language,            // string — e.g. "en", "rs", "de"
  environment          // string — "sandbox" or "production"
).then(async client => {
  // client === HPay (global)
  // client.POS.payment  — array of payment method objects
  // client.POS.shipping — array of shipping method objects
  // client.POS.fiscal   — array of fiscal method objects

  // Optionally filter by buyer country:
  let availablePayment = [];
  let availableShipping = [];
  const country = 'RS'; // ISO 2-letter code from billing or shipping address
  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 your payment method <select> from client.POS.payment:
  client.POS.payment.forEach(pm => {
    if (!pm.Hidden && (!availablePayment.length || availablePayment.find(m => m.Uid == pm.Uid))) {
      // pm.HPaySiteMethodId — value to pass as pay_request.payment_method
      // pm.Name             — display name
      // pm.SubsciptionsType — e.g. "cof" or "mit" if card saving is supported
      // pm.POps             — string like "charge,refund" — available backend ops
      // pm.PayInputUrl      — if set, docking (embedded payment form) is supported
      // pm['Use IFRAME']    — if false, method uses redirect (no iframe)
    }
  });

  // Build your shipping method <select> 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 — value to pass as pay_request.shipping_method
      }
    });
  }
});
```

`HPayInit()` is safe to call multiple times — it only re-initializes if parameters change.

---

## Step 2 — Build the `pay_request` Object

Collect the data from your order/checkout form:

```javascript
const pay_request = {
  merchant_site_uid: "YOUR-MERCHANT-SITE-UID",  // required
  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
  verificationhash: "SERVER_GENERATED_SHA512",   // required in standard flow

  // 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",   // ISO 2-letter code
    state: "Beograd",
    lang: "sr_RS", // language from merchant platform/system
    postcode: "11000"
  },

  // 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
    }
  ]
};
```

Fields with empty string values should be deleted before sending:

```javascript
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`:
  - `merchant_site_uid`, `hpaylang`, `order_uid`, `order_name`, `order_amount`, `order_currency`
  - `payment_method`, `shipping_method`, `order_user_url`, `notify_url`
  - `order_data`, `cof`, `vault_token_uid`, `verificationhash`
- `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:
  - `transaction_uid`, `status`, `order_uid`, `order_amount`, `order_currency`, `vault_token_uid`, `subscription_uid`, `rand`
  - For result validation compare computed signature with result `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 — Generate the Signature (Server Side!)

**NEVER generate the signature on the frontend in production.** The Secret Key must stay on the server.

### PHP (server side)

```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'] ?? '');
    $md5  = md5($cstr . $merchant_site_uid);
    return hash('sha512', $md5 . $secretkey);
}

// Usage — add signature hash to pay_request before returning to browser:
$pay_request['verificationhash'] = generatePOSRequestSignature(
    $merchant_site_uid, $secret_key, $pay_request
);
```

### Node.js (server side)

```javascript
const crypto = require('crypto');
const md5 = require('md5'); // npm install 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 cstrmd5 = md5(cstr + merchant_site_uid);
  return crypto.createHash('sha512').update(cstrmd5 + secretkey).digest('hex').toLowerCase();
}

pay_request.verificationhash = generatePOSRequestSignature(
  merchant_site_uid, secret_key, pay_request
);
```

Also available as `HPay.generatePOSRequestSignature(request)` if HPay was initialized with the secret key as the 4th parameter (only for admin/backend use cases).

---

## Step 4 — Present the Payment Form

After receiving the signed `pay_request` from your server:

```javascript
// Option A: Modal (default)
HPay.presentHPayPayForm(pay_request);

// Option B: Docked (embedded in page element) — 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,   // force instalment number, or null to let buyer choose
    vault_token_uid:       pay_request.vault_token_uid || null,
    hpaylang:              pay_request.hpaylang,
    cof:                   pay_request.cof
  },
  dockElement  // container element or selector
);

// Then trigger payment (e.g. on a "Pay" button click):
HPay.presentHPayPayForm(pay_request);
```

For the dock container, add this CSS so it has a visible background:

```css
#paymentMethodDock { background: #ffffff9e; }
```

---

## Step 5 — Handle the Result Events

Listen for these document-level custom events:

```javascript
// Fires when payment result arrives (success OR error)
document.addEventListener('onHPayResult', function(e) {
  const r = e.hpay_response;
  if (!r) return;

  if (r.error && r.error.code) {
    // payment failed — can retry
    HPay.presentHPayPayForm(pay_request); // re-open form
    return;
  }

  // r.payment_status — e.g. "PAID", "RESERVED", "PAYING", "OBLIGATED", "AWAITING"
  if (/PAID|RESERVED|SUCCESS|PAYING|OBLIGATED|AWAITING/i.test(r.payment_status)) {
    // SUCCESS — fulfil the order
    // window.location.href = r.order_user_url; // redirect to thank-you page
    // IMPORTANT: clear cart for PAID/RESERVED and also for PAYING/AWAITING/OBLIGATED.

    // r.payment_html   — HTML receipt / payment instructions from payment provider
    // r.fiscal_html    — HTML fiscal receipt (if fiscal module active)
    // r.integr_html    — Integration HTML
    // r.shipping_html  — Shipping HTML

    // IMPORTANT: AWAITING/OBLIGATED are NOT failed; show r.payment_html on thank-you page
    // (commonly account/invoice payment details)
    // r.transaction_user_info — object with card/transaction details
    // e.g. { card_brand: "MASTERCARD", masked_pan: "544358******4639", ... }

    // Vault token (card-on-file) — if user agreed to save card:
    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,   // e.g. "MASTERCARD"
        vault_card_umask:   r.vault_card_umask,   // masked PAN e.g. "544358******4639"
        vault_exp:          r.vault_exp,           // e.g. "12/26"
        vault_scope:        r.vault_scope,         // acquirer scope / merchant ID
        vault_onlyforuser:  r.vault_onlyforuser,   // 1 = tied to specific user
        pay_method_uid:     r.pay_method_uid       // payment method this token belongs to
      };
      // TODO: POST saveCardData to your backend and store in DB
    }
  }

  // Always: r.status — order status string
  // Verify result signature before trusting it:
  // const valid = HPay.verifyPOSResultSignature(r); // if secret key was passed to HPayInit
});

// Fires when the payment panel is closed (by user or after result)
document.addEventListener('onHPayPanelClose', function(e) {
  const r = e.hpay_response; // may be 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;
  }
});

// Fires when an admin operation (via orderActions) completes
document.addEventListener('onHPayOrderOpExecuted', function(e) {
  // e.hpay_response — result of the admin operation
});
```

### Redirect Method POST-back Note (Bank Redirect Flows)

For redirect payment methods, HPay may return the result to `order_user_url` using an auto-submitted POST form:

```html
<form method='POST' id='frmRedirect' action='${order_user_url}'>
  <input type='hidden' name='hpay_forwarded_payment_response' id='hpay_response' />
</form>
<script nonce='${csp_nonce}'>
  document.getElementById('hpay_response').value = JSON.stringify(hpay_result);
  document.getElementById('frmRedirect').submit();
</script>
```

Because some stacks may escape the JSON string, implement robust parsing with normalization fallback:

```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;
}
```

### 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 = '<p class="hpay-placeholder">Transaction details are not available.</p>';
    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 = '<p class="hpay-placeholder">Not available for this payment.</p>';
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"
  }
}
```

---

## Step 6 — Verify the Result Signature (Server Side)

Always verify result `vhash` on your server before fulfilling the order.

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
// Node.js
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, for COF/MIT)

When a payment method supports `pm.POps` containing `"charge"` and a `vault_token_uid` has been saved, you can charge the card without user interaction.

`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');

const charge_request = {
  merchant_site_uid: "YOUR-MERCHANT-SITE-UID",
  hpaylang: "en",
  order_uid: "20260315-999999",
  order_name: "#Order 205",
  order_amount: "15000",
  order_currency: "RSD",
  payment_method: "179",
  vault_token_uid: "saved-token-uuid",  // required for charge
  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();

// Verify result signature before fulfilling:
// const valid = verifyHPayResponse(result, merchant_site_uid, SECRET_KEY);

if (/PAID|PAYING|RESERVED/.test(result.payment_status)) {
  // success — fulfil the order
}
```

---

## Admin Operations (Order Management)

To access admin-level order operations (refund, fiscal print, shipping confirm, etc.):

```javascript
HPayInit(
  merchant_site_uid,
  language,
  environment,
  secret_key   // 4th parameter 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);
                 });
               }
             });
           }
         }
       });
     });
   });
 });
```

---

## Result Verification — Webhook (Notify URL)

HPay calls `notify_url` via HTTP POST with a JSON body (server-to-server). The URL must be publicly accessible from the internet.  
The structure is the same as `onHPayResult`'s `e.hpay_response`.  
Always verify response `vhash` before processing.
If `notify_url` is sent in request payload, it overrides the panel-level `I(P|S|F|I)N` instant notification URL for that request.

In HPay panel site settings, configure instant notification URL under:
- `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` to that URL and sends POST JSON.

Supported `topic` values:
- `payresult`: payload shape is the same as `onHPayResult` (`e.hpay_response`), sample: `https://apps.holest.com/holest-pay/response_sample.json`.
- `orderupdate`: root payload always includes `order_uid`, `status`, and `vhash`; payload fields are the same as `payresult`; includes `order` property with HolestPay order object in the same format as `ord` from `HPay.getOrder(order_uid).then(ord => {...})`, sample: `https://apps.holest.com/holest-pay/hpay_order_sample.json` (`"id"` is ID from HPay system). Signature verification is the same (`vhash` validation).
- `posconfig-updated`: contains POS config (same structure as `HPay.POS` 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 `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 can arrive via browser callback and via webhook. Use `vhash` (plus order/transaction identifiers) as idempotency key to prevent duplicate processing.

---

## Implementation Precision Addendum

Use this section as an execution checklist to reduce integration mistakes and speed up debugging.

### Request Validation Rules (Before Calling HPay)

- `order_uid` must be unique per order attempt in your system; do not reuse successful IDs for new purchases.
- `merchant_site_uid` must belong to the same environment where script/API is used (`sandbox` with sandbox, `production` with production).
- `order_amount` must be the same value across all stages for one request lifecycle (form -> signed payload -> audit logs).
- `order_currency` must be a valid ISO 4217 code configured on your POS methods.
- `payment_method` must be `pm.HPaySiteMethodId` (not `pm.Uid`).
- `shipping_method` should be omitted if shipping is not used; send it only when user selected a supported shipping option.
- `notify_url` should be a stable server endpoint; avoid temporary dev tunnels for production behavior testing.
- `order_user_url` should point to your own order/thank-you page and include enough context to restore order state.
- `order_data` is ideal for stable metadata like source channel, cart fingerprint, CRM ID, campaign tags, or internal correlation keys.
- Remove empty fields before sending; avoid null/empty noise in logs and signatures.

### Signature and Verification Precision

- Generate `verificationhash` server-side only for requests sent to HPay.
- For responses received from HPay, always validate `vhash`.
- Use exactly the documented signature field order and amount formatting (`toFixed(8)` / `number_format(..., 8)`).
- Compare signatures using exact lowercase hex strings.
- Verify signatures in both places: browser event result handling path and webhook path.
- Treat unsigned or signature-invalid payload as non-authoritative and do not fulfill order.
- Log both the received and calculated signature in secure server logs for incident analysis.

### Webhook Reliability Pattern

- Implement webhook idempotency keyed by transaction/order identifiers from result payload.
- Accept webhook retry behavior; return success response only after durable persistence.
- Process business actions once, even if webhook arrives multiple times or after browser callback.
- Record webhook receive timestamp and raw payload snapshot for auditability.
- Never rely on callback ordering between browser event and webhook; either can arrive first.
- Use webhook as final server authority for fulfillment transitions.

### Status Handling Strategy

- Handle `PAID`, `SUCCESS`, `PAYING`, `RESERVED`, `OBLIGATED`, `AWAITING`, `FAILED`, `REFUSED`, `CANCELED`, `VOID`, `REFUNDED`, `PARTIALLY-REFUNDED`, `EXPIRED`, `OVERDUE`.
- Map payment status to business actions explicitly:
- `PAID`/`SUCCESS`: fulfill when signature is valid.
- `PAYING`: partially paid; keep order open according to your policy.
- `RESERVED`: authorization only; wait for capture before final fulfillment.
- `AWAITING`/`OBLIGATED`: pending/offline or legally committed flows; these are **not failed** statuses. Do not treat them as card-capture success, but show `payment_html` on thank-you page (often contains account/invoice payment instructions).
- Cart handling rule: clear cart for `PAID`, `RESERVED`, `PAYING`, `AWAITING`, and `OBLIGATED` (same cart behavior for all these statuses).
- `FAILED`/`REFUSED`/`CANCELED`/`EXPIRED`/`OVERDUE`: stop fulfillment and show retry/payment-link path.
- Parse composed HolestPay status (`PAYMENT ... _FISCAL/_INTEGR ... _SHIPPING ...`) as operational context, not payment result only.

### Shipping and Dispenser Precision

- `dispenser`, `dispenser_desc`, and `dispenser_method_id` are meaningful only for shipping methods that support locker/paket-shop mode.
- `dispenser_method_id` must match top-level `shipping_method` to be considered in downstream processing.
- Keep shipping address and shipping method logically aligned (country/state/postcode compatibility).

### Payload Size and Data Modeling

- Keep total serialized order payload below `64 KB`.
- Prefer short stable keys in `order_data`; avoid embedding huge documents.
- Do not place sensitive secrets in `order_data`, `order_items`, or any client-visible payload field.
- Put only metadata needed for reconciliation/debugging into request payload.

### Frontend and Backend Responsibility Split

- Frontend: collect user choices, call initialized HPay APIs, listen to events, render status.
- Backend: create signature, verify result signatures, process webhook, enforce idempotency, persist state.
- Admin operations (`HPayInit(..., secret_key)`) belong to protected/admin contexts only.

### Recommended Test Matrix (Minimal but Complete)

- Test with `sandbox` and at least two payment methods configured on POS.
- Test success payment and failed payment (validation/card failure).
- Test close panel before completion and verify no accidental fulfillment.
- Test retry after `onHPayResult` error flow.
- Test with and without shipping section.
- Test one request containing `order_data` and confirm data appears in resulting order `Data`.
- Test one backend charge request with existing `vault_token_uid`.
- Test webhook replay (send same payload twice) and confirm idempotent behavior.
- Test signature mismatch scenario and verify order is not fulfilled.
- Test one `RESERVED`/authorization-capable method if available and confirm capture-dependent flow.

### Logging and Observability

- Log correlation tuple for each request: `order_uid`, `merchant_site_uid`, environment, method IDs, and internal user/session key.
- Store raw HPay response payloads for troubleshooting (with sensitive-data policy applied).
- Keep separate logs for browser callback processing and webhook processing.
- Add clear audit events: `PAYMENT_INIT`, `PAYMENT_RESULT_RECEIVED`, `WEBHOOK_RECEIVED`, `SIGNATURE_VALIDATED`, `FULFILLMENT_TRIGGERED`.

### Common Integration Mistakes to Avoid

- Using `pm.Uid` instead of `pm.HPaySiteMethodId`.
- Mixing sandbox credentials with production script/API domain.
- Trusting browser callback without server-side verification.
- Reusing old `order_uid` across retries/orders.
- Treating all non-error statuses as final success.
- Sending huge custom payloads over `64 KB`.

### Field Constraints Quick Reference

| Field | Required | Typical format | Precision note |
|---|---|---|---|
| `merchant_site_uid` | Yes | UUID-like string | Must match target environment POS |
| `hpaylang` | No | `en`, `rs`, `de`, ... | Keep consistent across UI and requests |
| `order_uid` | Yes | App-specific unique string | Unique per purchase attempt |
| `order_name` | No | Human-readable label | Useful for support and logs |
| `order_amount` | Yes | Numeric string | Use same value across client/server |
| `order_currency` | Yes | ISO 4217 | Must be supported by selected method |
| `payment_method` | Yes | `HPaySiteMethodId` string | Never use `pm.Uid` here |
| `shipping_method` | No | `HPaySiteMethodId` string | Send only when shipping is used |
| `order_user_url` | No | HTTPS URL | Browser flow only |
| `notify_url` | No | Public HTTPS URL | Webhook authority endpoint |
| `order_data` | No | Object/map | Stored in resulting order `Data` |
| `cof` | No | `optional|required|none` | Checkout card-save behavior |
| `vault_token_uid` | No | Token UUID or `1|true|new` | Saved card or save-request hint |
| `verificationhash` | Yes (standard pay) | SHA-512 lowercase hex | Generated server-side |
| `order_billing.email` | No | Email | Validate syntax before send |
| `order_billing.phone` | No | E.164-like | Keep normalized |
| `order_billing.country` | No | ISO-2 | Use uppercase (`RS`, `US`) |
| `order_shipping.country` | No | ISO-2 | Align with shipping method support |
| `order_shipping.dispenser` | No | Provider ID | Locker/paket-shop only |
| `order_shipping.dispenser_method_id` | No | Method ID string | Must match `shipping_method` |
| `order_items[].posuid` | Yes | String/number | Merchant-defined internal item identifier (not a fixed HolestPay enum) |
| `order_items[].name` | Yes | String | Human-readable item name (not `title`) |
| `order_items[].qty` | Yes | Number/string | Positive quantity expected (not `quantity`) |
| `order_items[].price` | No | Number/string | Keep pricing model consistent |
| `order_items[].subtotal` | Yes | Number/string | Mandatory line subtotal |
| `order_items[].virtual` | No | `0|1` | Shipping relevance hint |

### Server Workflow Blueprint (Standard)

```text
1) Frontend builds draft pay_request (without verificationhash).
2) Frontend sends draft payload to your backend /payments/init endpoint.
3) Backend validates required fields + method compatibility + environment.
4) Backend generates verificationhash using secret key.
5) Backend persists "payment initiated" row with internal correlation ID.
6) Backend returns signed pay_request to frontend.
7) Frontend calls HPay.presentHPayPayForm(signed_request).
8) Frontend receives onHPayResult and forwards payload to backend for logging.
9) Backend verifies signature on result payload.
10) Webhook arrives (possibly before/after browser callback).
11) Backend verifies webhook signature and applies idempotent state transition.
12) Fulfillment runs only once after verified terminal/success policy conditions.
13) Audit events are written for every transition.
```

### Idempotent Webhook Handling Example (Node.js Pseudocode)

```javascript
// Pseudocode only: adapt to your DB and framework.
app.post('/webhooks/hpay', async (req, res) => {
  const payload = req.body || {};
  const orderUid = String(payload.order_uid || '');
  const txUid = String(payload.transaction_uid || '');
  const idempotencyKey = `${orderUid}:${txUid}:${payload.status || ''}`;

  if (!orderUid) return res.status(400).json({ ok: false, error: 'missing order_uid' });

  // 1) Verify signature before business logic
  const calculated = generatePOSRequestSignature(MERCHANT_SITE_UID, SECRET_KEY, payload);
  const valid = calculated === String(payload.vhash || '').toLowerCase();
  if (!valid) return res.status(400).json({ ok: false, error: 'invalid signature' });

  // 2) Idempotency lock
  const alreadyProcessed = await db.idempotency.exists(idempotencyKey);
  if (alreadyProcessed) return res.status(200).json({ ok: true, duplicate: true });

  // 3) Persist first, then act
  await db.tx(async trx => {
    await trx.idempotency.insert({ key: idempotencyKey, createdAt: new Date() });
    await trx.hpayEvents.insert({
      orderUid,
      transactionUid: txUid,
      paymentStatus: payload.payment_status || null,
      orderStatus: payload.status || null,
      raw: JSON.stringify(payload),
      source: 'webhook'
    });

    const paymentStatus = String(payload.payment_status || '').toUpperCase();
    if (/PAID|SUCCESS|PAYING|RESERVED|OBLIGATED/.test(paymentStatus)) {
      await trx.orders.markAsPaidOrReserved(orderUid, paymentStatus);
      // Optional: enqueue downstream workers instead of inline execution
      await trx.jobs.enqueue({ type: 'FULFILLMENT_DECISION', orderUid });
    } else if (/AWAITING/.test(paymentStatus)) {
      // Not failed: keep order pending and show payment_html instructions on return/thank-you page
      await trx.orders.markAsPendingPaymentInstructions(orderUid, paymentStatus);
    } else {
      await trx.orders.markAsPaymentNotCompleted(orderUid, paymentStatus);
    }
  });

  return res.status(200).json({ ok: true });
});
```

### Browser Callback + Webhook Reconciliation Rules

- If browser callback succeeds first, store it as provisional until webhook check completes.
- If webhook succeeds first, mark server truth immediately; browser callback only enriches logs/UI.
- If callback says success but webhook never arrives, run delayed status query before fulfillment.
- If callback and webhook disagree, trust verified server-side signature + latest authoritative status policy.
- Never ship goods exclusively from client-side event data.

### Operational State Model (Recommended)

- `ORDER_CREATED`: order exists locally before payment started.
- `PAYMENT_INITIATED`: signed request issued to browser.
- `PAYMENT_UI_OPENED`: optional telemetry state.
- `PAYMENT_CALLBACK_RECEIVED`: browser event stored.
- `WEBHOOK_RECEIVED`: webhook accepted and verified.
- `PAYMENT_CONFIRMED`: business rule says financial completion met.
- `FULFILLMENT_PENDING`: queued for shipping/fiscal/integration actions.
- `FULFILLED`: goods/services delivered or process completed.
- `PAYMENT_FAILED`: terminal non-success status.
- `CLOSED`: final immutable state with audit trail.

### Production Readiness Checklist

- Rotate and store Secret Key in secure secret manager, not source code.
- Restrict admin pages that use `HPayInit(..., secret_key)` by authentication/authorization.
- Add request/response size limits and payload schema validation at backend edge.
- Enable TLS-only endpoints for checkout and webhook.
- Add monitoring alarms for webhook signature failures and payment failure spikes.
- Capture dashboard metrics: conversion rate by method, callback-to-webhook lag, duplicate webhook rate.
- Create runbook for manual recovery: status query, refund/void decisions, replay-safe reprocessing.
- Test environment cutover with explicit checklist (POS UID, methods, notify URLs, allowed domains, secrets).

### Troubleshooting Cookbook

- Symptom: payment modal does not open.
- Check: script URL host, browser console errors, `HPayInit` resolved, `payment_method` selected.
- Fix: ensure `hpay.js` loaded from correct environment and request has required fields.

- Symptom: result arrives in browser but order not updated on server.
- Check: callback forwarding endpoint, webhook delivery logs, signature verification result.
- Fix: persist callback as provisional and complete state transition on verified webhook.

- Symptom: signature mismatch on server.
- Check: amount normalization to 8 decimals, field order in signature string, correct secret key/environment.
- Fix: use one shared signature utility across all server handlers and add test vectors.

- Symptom: webhook endpoint receives duplicates.
- Check: idempotency key existence and transaction wrapping around insert/process logic.
- Fix: move idempotency write before business action and enforce unique DB constraint on key.

- Symptom: shipping actions are unavailable or inconsistent.
- Check: selected `shipping_method`, configured shipping module, country constraints, item/shipping flags.
- Fix: refresh POS config via `HPayInit`, re-evaluate available shipping methods per country/amount.

- Symptom: charge request fails with saved token.
- Check: token validity/scope, method supports `charge`, signature correctness, environment match.
- Fix: use fresh token tied to same merchant scope and verify method `POps` includes `charge`.

- Symptom: user is not redirected to thank-you URL.
- Check: method iframe/redirect behavior and callback handling.
- Fix: redirect manually in success branch using `r.order_user_url` when appropriate.

- Symptom: fiscal/shipping statuses are missing in composed status.
- Check: module actually enabled and configured to emit status.
- Fix: treat missing module segment as valid; do not hard-fail parsing logic.

### Suggested CI/QA Validation Cases

- Validate JSON schema for `pay_request` and `charge_request` in unit tests.
- Validate signature generation against fixed fixtures in PHP and Node.js implementations.
- Validate idempotency behavior by replaying same webhook payload at least 3 times.
- Validate parser for composed status strings with and without fiscal/shipping segments.
- Validate route-level access control for admin operations requiring Secret Key.
- Validate payload size guard rejects requests above `64 KB`.
- Validate environment guard rejects sandbox credentials on production host and vice versa.
- Validate correlation logs always include `order_uid` and `merchant_site_uid`.

### Security No-Go List

- Never hardcode `secret_key` in browser JavaScript, HTML templates, or mobile bundle assets.
- Never process fulfillment on unverified callback payload.
- Never expose raw webhook endpoint without rate limiting and request body size limits.
- Never trust query params from `order_user_url` alone as proof of payment.
- Never log secret values or full PAN/cardholder data in application logs.
- Never reuse one `order_uid` for multiple independent purchases.

### Terminology Quick Map

- `POS` in HolestPay = your app/site sales endpoint, not a physical cashier terminal.
- `payment_method` = HPay method instance ID selected by buyer.
- `status` = composed platform state across payment/fiscal/shipping modules.
- `payment_status` = payment module status focus.
- `verificationhash` = request signature hash sent to HPay.
- `vhash` = response signature hash returned by HPay.
- `order_data` = custom merchant metadata persisted to order `Data`.

---

## Key Object Reference

All fields supported by the template are listed in `Step 2` under `Full Template Field Catalog (Standalone)`.

---

## Important Notes

- **Secret Key must never appear in frontend code** in production. Use it only on your server.
- The page must be served over **HTTPS**. HPay will not work on `file://` or plain HTTP.
- `HPayInit()` can be called multiple times safely — only re-inits on parameter change.
- `payment_method` value is `pm.HPaySiteMethodId` (NOT `pm.Uid` — those are different).
- `availablePaymentMethods(country, amount, currency)` and `availableShippingMethods(...)` filter by buyer's country and order details. Pass billing country for payment, shipping country for shipping.
- If `pm['Use IFRAME'] === false`, the method uses a redirect flow — your page cannot accept the postback result directly. This method type requires proper server-side integration.
- After a successful payment, generate a new `order_uid` for the next order — never reuse order UIDs.
- `order_data` supports custom metadata and is stored as HPay order `Data`.
- `order_billing` and `order_shipping` can include additional custom fields, but keep total serialized order payload under **64 KB**.

---

## Required for Bank Production Approval — Logotypes and Terms of Service

> ?? **This section has nothing to do with payment functionality** — payments work without it. However, **almost all banks in the region will require these elements to be present on the site before approving the POS for production use.** Implement these alongside the payment integration.

---

### Footer Logotypes (Card logos, Bank logos, 3DS logos)

Banks require card brand logos, acquiring bank logos, and 3DS security logos to appear in the site footer — in a single horizontal line, **no taller than approximately 1 cm visually**. Order: card logos first, bank logos in the middle (with some spacing on both sides), 3DS logos on the right. Bank and 3DS logos must be linked. The footer strip must be visible on **all pages of the website**, not only on checkout.

HolestPay provides all logo image URLs and link targets via `HPay.POS.pos_parameters` after `HPayInit()`. Use them — do not source logos elsewhere.

**CSS for the footer branding strip:**

```css
.hpay_footer_branding_wrapper {
  width: 100%;
  padding: 8px 0;
  border-top: 1px solid #e0e0e0;
}
.hpay_footer_branding {
  display: flex;
  align-items: center;
  gap: 0;
  flex-wrap: nowrap;
}
.hpay-footer-branding-cards,
.hpay-footer-branding-bank,
.hpay-footer-branding-3ds {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
}
.hpay-footer-branding-bank  { margin: 0 12px; }
.hpay_footer_branding img   { height: 1cm; width: auto; display: inline-block; }
```

**JavaScript — call this after `HPayInit()` resolves:**

```javascript
function renderHPayFooterLogotypes() {
  if (!(typeof HPay !== 'undefined' && HPay && HPay.POS)) return;
  if (!HPay.POS.pos_parameters) return;

  let card_images_html = '';
  let banks_html       = '';
  let threes_html      = '';

  if (HPay.POS.pos_parameters['Logotypes Card Images']) {
    HPay.POS.pos_parameters['Logotypes Card Images'].split('\n').forEach(src => {
      if (src.trim()) card_images_html += `<img src="${src.trim()}" alt="Card" />`;
    });
  }

  if (HPay.POS.pos_parameters['Logotypes Banks']) {
    HPay.POS.pos_parameters['Logotypes Banks'].split('\n').forEach(line => {
      if (!line.trim()) return;
      // format: "imageUrl:linkUrl" — colons in URLs are escaped before splitting
      const t = line.replace(/https:/gi, '-PS-').replace(/http:/gi, '-P-')
        .split(':').map(r => r.replace(/-P-/g, 'http:').replace(/-PS-/g, 'https:'));
      banks_html += t.length > 1
        ? `<a href="${t[1]}" target="_blank"><img src="${t[0]}" alt="Bank" /></a>`
        : `<img src="${t[0]}" alt="Bank" />`;
    });
  }

  if (HPay.POS.pos_parameters['Logotypes 3DS']) {
    HPay.POS.pos_parameters['Logotypes 3DS'].split('\n').forEach(line => {
      if (!line.trim()) return;
      const t = line.replace(/https:/gi, '-PS-').replace(/http:/gi, '-P-')
        .split(':').map(r => r.replace(/-P-/g, 'http:').replace(/-PS-/g, 'https:'));
      threes_html += t.length > 1
        ? `<a href="${t[1]}" target="_blank"><img src="${t[0]}" alt="3DS" /></a>`
        : `<img src="${t[0]}" alt="3DS" />`;
    });
  }

  const logotypesDiv = document.createElement('div');
  logotypesDiv.className = 'hpay_footer_branding';
  logotypesDiv.innerHTML =
    `<div class="hpay-footer-branding-cards">${card_images_html}</div>` +
    `<div class="hpay-footer-branding-bank">${banks_html}</div>` +
    `<div class="hpay-footer-branding-3ds">${threes_html}</div>`;

  const wrapper = document.createElement('div');
  wrapper.className = 'hpay_footer_branding_wrapper';
  wrapper.appendChild(logotypesDiv);

  (document.querySelector('footer') || document.querySelector('main') || document.body)
    .appendChild(wrapper);
}

// Call after HPayInit resolves:
// HPayInit().then(client => { renderHPayFooterLogotypes(); });
```

---

### Terms of Service Acceptance Checkbox

Almost all banks in the region require the customer to explicitly accept the site Terms of Service before payment — typically a checkbox with a link to the TOS content.

**HolestPay provides a ready-to-use Terms of Service / Purchase Conditions template page for each POS/site:**

```
https://sandbox.pay.holest.com/clientpay/tos/<merchant_site_uid>?lang=rs
https://pay.holest.com/clientpay/tos/<merchant_site_uid>?lang=en
```

Supported `lang` values: `en`, `rs`, `bs`, `hr`, `me`, `de`, `es`, `gr`, `tr`, `mk`, and others.

This URL returns HTML content (no `<html>` wrapper) and can be:
- Loaded in an `<iframe>`
- Fetched with `fetch()` / AJAX and injected into a modal
- Opened directly in the browser (use `target="_blank"`)

> ?? **Important:** This is **not** a separate "second HolestPay Terms" that replaces your site Terms. The `/clientpay/tos/<merchant_site_uid>` content is a merchant-specific **template/proposal of your own purchase Terms** (with your POS/merchant context), meant to be used as the same Terms of Service page your checkout checkbox points to. Keep one coherent TOS page for customers (your site TOS), and ensure it includes the required payment clauses.

**Implementation — TOS modal using HolestPay built-in dialog** (`hpay_dialog_open` becomes available after `client.loadHPayUI()` resolves):

```javascript
// Call client.loadHPayUI() first to make hpay_dialog_open available:
// HPayInit().then(client => client.loadHPayUI()).then(() => { /* hpay_dialog_open is ready */ });

function openTOS(merchant_site_uid, lang) {
  const tos_url = `https://sandbox.pay.holest.com/clientpay/tos/${merchant_site_uid}?lang=${lang || 'en'}`;
  // Replace sandbox host with pay.holest.com for production
  hpay_dialog_open('tos', 'Terms of Service', tos_url, 'large', {
    ['I understand and accept these Terms of Service']: {
      Run: function(dlg) {
        dlg.close();
        if (dlg.parentNode) dlg.parentNode.removeChild(dlg);
        // Mark TOS as accepted — enable the Pay button or proceed with payment
      },
      action_position: 'center',
      style: { 'min-width': '120px' }
    }
  });
}
```

**Minimum required UI — add this near the Pay button:**

```html
<label style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
  <input type="checkbox" id="tos-accept" required />
  <span>
    I accept the
    <a href="#" onclick="openTOS(MERCHANT_SITE_UID, 'en'); return false;">Terms of Service</a>
  </span>
</label>
<!-- Disable Pay button until checkbox is checked -->
```

```javascript
document.getElementById('tos-accept').addEventListener('change', function() {
  document.getElementById('do-pay').disabled = !this.checked;
});
```

**Checklist for bank approval:**
- [ ] Card brand logos visible in footer (from `HPay.POS.pos_parameters['Logotypes Card Images']`)
- [ ] Bank logos visible in footer, linked (from `HPay.POS.pos_parameters['Logotypes Banks']`)
- [ ] 3DS logos visible in footer, linked (from `HPay.POS.pos_parameters['Logotypes 3DS']`)
- [ ] All logos in a single horizontal line, max ~1 cm height
- [ ] TOS checkbox present near the Pay button, Pay button disabled until checked
- [ ] TOS content matches or includes HolestPay-provided TOS (`/clientpay/tos/<uid>`)

---

## Test Cards for Sandbox Testing

Test card numbers for almost all supported banks and payment methods can be found directly on the **dashboard page** of the HolestPay sandbox panel at https://sandbox.pay.holest.com — no need to search elsewhere. Log in and check the dashboard.

---

## Terms of Service Page — Required Content

The "Terms of Service" page that the customer checks before payment must be **comprehensive and self-contained**. Because the customer ticks a single "I accept the Terms of Service" checkbox that links to this one page, **everything required by the bank and by law must be covered on that exact page** — not spread across separate pages.

The following must be present in the Terms of Service content (either written directly or clearly summarized with a reference to the full document inline):

- **Purchase conditions** — what is being sold, pricing, order confirmation process
- **Delivery policy** — delivery methods, timeframes, costs, geographic coverage
- **Refund and return policy** — conditions, process, and timeframes for refunds and returns
- **PCI DSS statement** — explicit confirmation that card data transfer/processing is secured according to PCI DSS standards and that the merchant does not store raw card data (HolestPay handles card processing on the merchant's behalf)
- **Merchant contact information** — legal business name, address, email, and phone number for customer support

> The merchant may have separate dedicated pages for privacy policy, delivery conditions, refund policy, etc. — that is fine. But **all of the above topics must also appear on the single Terms of Service page** that the payment checkbox links to, because that is the only page the customer is asked to review and accept before paying.

> HolestPay provides a base TOS/purchase-conditions text via `/clientpay/tos/<merchant_site_uid>` that already includes payment-security wording (including PCI DSS transfer handling). Treat it as baseline content for the merchant's own single Terms page, then extend it with delivery, refund, and contact details — not as a separate second TOS document.

---

## Before Requesting Bank Production Approval — Site Readiness

Before the merchant contacts their bank to activate the POS for production, the site must be genuinely complete. A bank technician will review the site manually and will reject it if it does not look like a real, finished online store.

**The following will cause immediate rejection:**

- **Placeholder content** — any "Lorem ipsum", "Coming soon", "Test product", dummy images, or unfilled template text anywhere on the site
- **Mixed languages** — site content, product names, descriptions, and all pages must be consistently in one language (or properly localized if multilingual); a mix of languages on the same page signals an unfinished site
- **Missing or incomplete pages** — the site must have real products/services with actual descriptions and prices, a working checkout flow, and all required legal pages (Terms of Service, Privacy Policy, Contact/About)
- **Non-functional links or broken pages** — every linked page must load and contain real content
- **Test or sandbox data visible** — no test order UIDs, sandbox URLs, or debug output visible to visitors

> Tell the user: *"Before you ask your bank to approve the POS for production, make sure the site looks like a finished, real store. Bank technicians visit the site in person before approving. If they see Lorem ipsum text, empty product pages, mixed languages, or anything that looks unfinished, they will ask you to complete the site first and resubmit — which adds weeks to the process."*

---

## Bank Validation Testing Before Production Approval

Most banks require a **formal validation test** to be performed before activating the POS for production. This is separate from the developer's own sandbox testing.

What this typically involves:

- The bank provides a set of **specific test scenarios** that must be completed successfully (e.g. approved payment, declined payment, 3DS challenge flow, cancellation)
- Tests are performed using **test cards provided by the bank** (which may differ from the general sandbox test cards on the HPay dashboard)
- Results are reported back to the bank (sometimes automatically via their test monitoring system, sometimes manually by the merchant)
- **Whether validation testing is required, and exactly how it is done, depends on the bank and the specific payment method** — some banks require it for every new integration, others only for certain card brands or 3DS configurations

> Tell the user: *"Before your bank activates the production POS, they will likely ask you to run a set of validation test payments using specific test cards they provide. This is a standard bank requirement and is separate from the testing you did during development. Contact your bank's technical integration team to find out exactly what tests are required for your payment method — HolestPay sandbox will be used for these tests as well."*

---

## Shipping Method UI — Required `hpay-sm-options` Placeholder

In the UI element that displays a shipping method's description (the area shown to the buyer when they select a shipping method), you **must include an empty `<span>` with the class `hpay-sm-options` and attribute `hpay_shipping_method_id="<hpay_shipping_method.HPaySiteMethodId>"`**:

```html
<span class="hpay-sm-options" hpay_shipping_method_id="<hpay_shipping_method.HPaySiteMethodId>"></span>
```

HolestPay uses this element to inject additional shipping options and information (e.g. parcel locker selector, delivery time slots, carrier tracking info) for the selected shipping method. Without this placeholder, those features will silently fail to render.

Place it inside the shipping method description container, after any static description text:

```html
<div class="shipping-method-description">
  <!-- your static description text here -->
  <span class="hpay-sm-options" hpay_shipping_method_id="45"></span>
</div>
```

---

## Content Security Policy (CSP) and Site Configuration Requirements

For HolestPay to function correctly, the site must permit the following — ensure these are not blocked by Content Security Policy headers, server configuration, or any firewall/proxy:

### Allowed origins (iframes, scripts, XHR/fetch, content)

The site must allow iframes, scripts, and content loaded from:

```
https://pay.holest.com
https://sandbox.pay.holest.com
```

If the site sets a `Content-Security-Policy` header, include these in all relevant directives:

```
Content-Security-Policy:
  script-src  'self' https://pay.holest.com https://sandbox.pay.holest.com ...;
  frame-src   'self' https://pay.holest.com https://sandbox.pay.holest.com ...;
  connect-src 'self' https://pay.holest.com https://sandbox.pay.holest.com ...;
  img-src     'self' https://pay.holest.com https://sandbox.pay.holest.com data: ...;
```

### `eval()` must be permitted

HolestPay internally uses `eval()` to deserialize and execute certain POS method action functions returned from the server (`initActions`, `orderActions`). The site must **not** block `eval()`.

If a CSP header is set, `'unsafe-eval'` must be present in `script-src`:

```
Content-Security-Policy:
  script-src 'self' 'unsafe-eval' https://pay.holest.com https://sandbox.pay.holest.com ...;
```

> ?? If the site uses a strict CSP that blocks `eval()` or restricts iframes/scripts to specific origins, HolestPay will fail silently or partially. This is one of the most common causes of "nothing happens when I click Pay" during integration.

---

## Dock Container Placement

For each payment method row, use this visual order:
1) method **name/label**
2) method **description**
3) method **dock container** (if that method supports docking)

The dock container must be below the description of the same method, and before the next method name is rendered. It must **not** be placed after the full list of all payment methods.

Each payment method in the list that supports docking (`pm.PayInputUrl` is set) needs its own dock container adjacent to it. Best practice is to wrap **description + dock** in one method-details wrapper that is shown only when that method is selected. When the buyer selects that method, `HPay.setPaymentMethodDock()` renders the embedded payment input directly inside that container.

**Correct structure:**
```html
<label>
  <input type="radio" name="payment_method" value="179" />
  Credit Card
</label>
<div class="method-details is-selected" data-method="179">
  <div class="method-description">Card payment description...</div>
  <div id="paymentMethodDock-179" class="hpay-dock-container"></div>
</div>

<label>
  <input type="radio" name="payment_method" value="180" />
  Bank Transfer
</label>
<div class="method-details" data-method="180">
  <div class="method-description">Bank transfer description...</div>
  <!-- no dock here — this method does not support docking -->
</div>
```

---

## Always Render Method Description When Selected

Whenever a payment method or shipping method is selected by the buyer, **always display its description**. Do not skip or hide descriptions.

Method descriptions sometimes contain **functionally important content** — instructions the buyer must read (e.g. bank account number for manual transfer, pickup location details, special conditions). Hiding or omitting descriptions can cause buyer confusion and failed orders.

For shipping methods, the description container must also include the `<span class="hpay-sm-options" hpay_shipping_method_id="<hpay_shipping_method.HPaySiteMethodId>"></span>` placeholder (see earlier section) so HolestPay can inject carrier-specific UI into it.

---

## HolestPay Language Codes

HolestPay uses its own language code convention — **do not use standard ISO/BCP-47 codes** for Serbian:

| Language | HolestPay code | ? Do NOT use |
|---|---|---|
| Serbian Latin | `rs` | `sr`, `sr-Latn`, `sr-YU`, `sr-lat` |
| Serbian Cyrillic | `rs-cyr` | `sr-Cyrl`, `sr-cyr`, `cyr` |
| Bosnian | `bs` | |
| Croatian | `hr` | |
| Slovenian | `si` | |
| Macedonian | `mk` | |
| English | `en` | |
| German | `de` | |
| Italian | `it` | |
| Spanish | `es` | |
| French | `fr` | |
| Portuguese | `pt` | |
| Dutch | `nl` | |
| Greek | `el` | `gr` |
| Turkish | `tr` | |

Pass this value as the `language` parameter to `HPayInit()` and as `hpaylang` in `pay_request`.

Posted on

HolestPay – Shopify Connector – Custom App

HolestPay – Shopify Connector – Custom App

**Explanations Regarding the HolestPay Shopify Connector as a Custom App

If you want to install this app, you can find instalaltion manual inside our panel in HELP section: https://sandbox.pay.holest.com/

This is a Custom App that we generate exclusively for your Shopify store every time. The rules that apply to custom apps on the Shopify platform allow the user to add any kind of functionality to their online store (except in extreme cases, e.g., fraud), but Shopify does not guarantee their functionality, unlike with public apps available on the Shopify App Store or payment solutions listed by **Payment Partners** that Shopify has verified before public release.

Since the application for new Payment Partners has been closed for several years (it is stated that they are working on upgrading that part of the system, so for now, they operate on an “invite-only” basis, inviting only globally renowned payment institutions so they don’t fall behind while the system is being upgraded), the HolestPay Shopify connector is implemented as a custom app (for now). We guarantee reliable and secure payment on your site as a **certified PSP (PCI DSS)**, or in solutions where we do not act as the PSP (**bank-redirect**), as an integrator that implements solutions in accordance with the **PCI DSS standard**.

Confusion and incorrect interpretation of Shopify documentation often occur because Shopify, in its documentation, focuses on explanations that apply to apps that developers plan to list on the public Shopify App Store or in the list of **Payment Partner** solutions in the Shopify **backend**. There are no limitations regarding the rules for **Custom Apps**, as long as they do not violate the basic Shopify Terms of Service, which the name **”Custom”** implies, but full responsibility for the functioning lies with the merchant. The merchant mitigates this responsibility by using our solution, where we guarantee complete payment security and reliability, regardless of the Shopify plan.


1. Default Option: Payment Button (Recommended)

The app defaults to using the **Payment Button** which it adds to the order confirmation pages (**Thank You Page / Order Status Page**).

How it works: The app adds a button to proceed to the payment page, which the Customer must explicitly click to be directed to the page where they **can** make the payment.

Advantage: It is fully compliant with all Shopify recommendations and policies on all plans and guarantees the accurate collection of marketing conversions (pixels, analytics).


2. Optional Feature: Automatic Redirection (Auto-Redirect)

If you activate the “Auto-Redirect” option, the customer is automatically redirected to our payment page, the bank’s page, or the page of another payment institution without needing to click **the payment button**.

This feature is not prohibited for custom **apps** (it would be for public apps because it interferes with the live checkout flow, i.e., the user is not required to explicitly click a button), but it encroaches on Shopify’s **efforts** to ensure a seamless checkout flow without the **need** for any special modifications.

Policy Issue (Why Shopify “Dislikes” it unless the merchant is on the “Shopify Plus” plan): Shopify explicitly recommends keeping the customer on the order status page to ensure the accurate and reliable collection of marketing conversion and analytics data (such as data for Google and Meta pixels).

Potential Problem: Although the payment is **100%** reliable and secure (which we guarantee), the Auto-Redirect feature may lead to unreliable or a complete loss of marketing conversion data resulting from the automatic interruption of the standard checkout flow.

Assumption of Responsibility: A merchant who activates the “Auto-Redirect” option assumes full and exclusive responsibility for the aforementioned loss of marketing data and analytics caused by the automatic interruption of the **Checkout** flow. When our custom app is implemented for a merchant, the merchant needs to check and perhaps add additional modifications for the collection of analytical data to work smoothly.

If you want to install this app, you can find instalaltion manual inside our panel in HELP section: https://sandbox.pay.holest.com/

Frequently Asked Questions (FAQ)

The bank told me they are not familiar with HolestPay as an integration option for Shopify?

Banks naturally want more clients and strive to get those who apply into production as quickly as possible.

Since these are large corporations, employees usually work with established instructions, all while trying to make the implementation easier for you. This sometimes leads to the need for clarification with banks we do not have constant communication with.

There are providers in the region who were listed on the Shopify Payment Partner list before Shopify closed new applications. The cost of their services annually can be 5 to 10 times higher than our solution. Employees often have them listed somewhere and recommend them to you.

The platform you use for your store is not within the scope of what the bank needs to verify. Their duty is to ensure that the regulations of card organizations and the laws in your country are respected at your point of sale before granting you production status.

The recommendation is not to mention the platform at all to avoid misunderstandings – simply ask for a terminal for a custom website.


Why do I have to wait up to 24 hours after requesting the app to receive the installation link?

This is a custom app and requires the codebase to be prepared specifically for your store so you can then install it on your shop. This preparation must be performed by an engineer.

Posted on

Free License Terms

TERMS FOR ONE YEAR FREE USE (TWO YEARS FOR FEMALE FOUNDERS)

1. LOCATION: The company and its sole founder are from Serbia, Croatia, Bosnia and Herzegovina, Montenegro or Macedonia (the founder must have the citizenship of the mentioned countries or the status of a displaced person from Russia, Ukraine, Syria or Lebanon in the mentioned countries).
2. FOR YOUR FIRST BUSINESS STEPS: The company was not established more than 6 months ago (from the date of the request).
3. YOU ARE ALONE AND DO NOT HAVE SIGNIFICANT START-UP CAPITAL: The company has one and only one founder

It is not necessary to forward to office@holest.com:

– a scan/photograph of the decision on the establishment of the competent register (the date of establishment and the name of the founder must be visible)

– you forward an email in which the bank issues you test parameters

NOTE: For the fiscal service, we are not able to offer the same type of benefits because ESIR (the modern name for cash register) is under the jurisdiction of a long specialized company. We can reduce the subscription amount for “Fiscalization Service” by 40%. The fiscal module activated within the HolestPay system already has this discount in any case, so there is no room for a discount for it.

TOTALLY FREE – TERMS OF GRANT AND USE

Payment plugin or HolestPay system for payments use will be granted free for use to collect donations/donation-subscriptions for treatment, child support and may be granted to humanitarian organsations after internal check. Decision to grant free licence is made by HOLEST E-COMMERCE DOO. Free licence shell not be granted to entites related to policits or religion or extremism.
Forward the free licence request to office@holest.com

Posted on

Fiskalizacija – kako se preuzima bezbednosni element sa e-porezi

 

IAKO MOŽDA VI NIKAD NISTE PRISTUPLI PORTALU E-POREZI JER TO ZA VAS RADI KNJIGOVOĐA, VI KAO OSNIVAČ FIRME NAJVEROVATNIJE IMATE TAKOĐE PRISTUP.

Potrebno je da imate čitač smart kartica (ili kako ga kod nas zovu čitač lične karte).
Potrebno je takođe da imate biometrisku LK sa učitanim sertifikatom i da znate svoj pin za LK.
Ako nemate čitač smart kartica, on se može kupiti u svakoj bolje snabdevenoj prodavnici računarske opreme.
Ako nemate sertifikat na ličnoj karti onda možete otići do najbliže ispostave MUP-a da vam upišu (to se obično odmah završava). Ako ne znate da li imate sertifikat na LK možete proveriti to preko aplikacije ČELIK

Ukoliko imate gore navedene elemente potrebno je da instalirate aplikaciju ePorezi sa sajta poreske uprave.


– logujete se na e-porezi


– odabere se pravni subjekat (ako ih imate više)


– u listingu opcija idite na ESF (na početku liste), dolazi se de recapch-e koju treba proci


– Kada se uđe na TAXCORE portal ide se na Administratcija->Zahtevi za izdavanje bezbednosnoh elemenata


– Klikunuti na dugme “Zahtevaj bezbednosni element”

– Na fromi skroz dole postoji dugme + pod sekcijom “Sertifikati”. Klik na ovo dugme pa uneti lozinku i PAK (ili ostaviti ponuđene vrednosti)

– Kad već ovo radite možete da zahtevate i pametnu karticu ako vam slućajno zatreba. Za to kliknuti na dugme “+” u sekciji pamete kartice.


– Poslati zahtev


Sutra ujutru će u poreskom sanducetu biti PDF sa linkom za prezimanje sertifikata (sertifikat može samo jednom da se skine – posle toga link postaje ne-upotrebljiv. Doduše nije teško pribaviti opet novi ali se čeka opet 1 dan. Uredno preuzmite fajl na svom PC-u i postavite ga na neko sigurno mesto.)
Posted on Leave a comment

Kako da započnem online naplatu

F.A.Q. ONLINE PLAĆANJE – JEDNOSTAVNA RAZJAŠNJENJA ČESTIH PITANJA U VEZI ONLINE PLAĆANJA

Kako mogu prodavati robu/usluge preko interneta?

Da bi mogli prodavati preko interneta, osim naravno sajta, potrebno je da imate registrovanu firmu.

Možete biti preduzetnik/preduzetnica ili D.O.O.

Ukoliko prodajete robu/usluge fizičkim licima preporuka je da budete u sistemu PDV-a.

I Preduzetnik/preduzetnica i D.O.O. mogu biti ali ne moraju u sistemu PDV-a (dok ne pređu priliv od 8.000.000 RSD u roku od jedne godine). Promet sa inostranstvom se u nekim slučajevima ne uzima u obzir pa ovaj limit može u nekim slučajevima biti i 120.000 EUR.

Jeste. To znači da možete imati online prodavnicu koja prodaje robu/usluge isključivo pravnim licima ili usluge inostranstvu i da nemate obavezu ulaska u PDV sistem.

Prelaskom ukupnog priliva od 8.000.000 RSD u periodu od jedne godine sa domaćim kupcima po zakonu sledeći radni dan ulazite u sistem PDV-a po automatizmu.

Vrsta robe i usluga koju prodajete se mora navesti prilikom sklapanja ugovora sa bankom/provajderom. Možete biti odbijeni. Pravila su obično u skladu sa pravilima VISA / MC / Maestro / DINA.

Troškovi firme: Preduzetnik/preduzetnica

Minimalni mesečni trošak preduzetnika/preduzetnice kod kojih se paušalno plaćaju porezi i doprinosi svodi se na uplatu poreza i doprinosa koje vam odredi poreska uprava. Za ovaj vid registracije firme knjigovodja vam nije ni potreban (potrebno je samo imate pristup portalu e’poreyi i da uplacujete ono što van odrede početkom godine). To okvirno bude u startu okvirno oko 25.000,00 RSD mesečno za unutrasnjost, a za Beograd mogu biti i znatno veci iznosi zavsno od delatnosti i opštine (za Beograd u startu u proseku oko 35.000 RSD). Uslov da ostanete paušalac je da u kalendarskoj godini ne predjete 6M prometa.

Postoje programi NSZ i privredne komore, naročito za žene, koji vam čak dozvoljavaju da prve ili čak prve 2 godine budete oslobođeni plaćanja poreza i doprinosa na svoju zaradu. Jeste, sa takvim podsticajem, nemate nikakvu novčanu obavezu prema bilo kome pa i samom sebi u startu. Mogućnost da probate, a da nemate nikakve novčane obaveze jeste olakšavajuća za nekog ko se prvi put upušta u ovakvu vrstu posla.

Troškovi firme: D.O.O.

Ako imate DOO ili ste u PDV sistemu onda knjigovođ jeste potreban.

Minimalni mesečni trošak D.O.O. su porezi i doprinosi na zaradu direktora (ukoliko je i jedini radnik). Oko 33.000,00 RSD mesečno, PDV na prodatu robu (ukoliko ste u PDV sistemu) i troškove za knjigovođu (za D.O.O svakako potreban).

Za D.O.O. sa 0 zaposlenih mora se platiti PIO za direktora ~12000 RSD.

 

Koliko me košta dodavanje i sama usluga online plaćanja na mom sajtu?

(sam sajt ne razmatramo ovde)

  • Jednokratni troškovi prilikom sklapanja ugovora sa bankom/provajderom plaćanja. Sve ukupno oko 2.000,00 RSD
  • Nabavka plugina za plaćanje 39.99EUR / godišnje
  • [OPCIONO] ukoliko ne želite sami da podešavate plugin i sajt. Usluga da vam to neko podesi i prođe test fazu/validaciju banke staje oko 200EUR.
  • Troškovi svake transakcije su oko ~2.8% zavisno od banke i ugovora koji dobijete.
  • To je to. Nema više nikakve filozofije.

 

Sve banke i provajderi plaćanja u Srbiji daju mogućnost kupovine iz inostranstva!

  • JEDNOSTAVNO: stranac može uneti broj svoje kartice i platiti.
  • Sredstva sa kartica inostranih kupaca se konvertuju u RSD.
  • Na Vaš račun može dospeti samo RSD.

 

Ok, koji su koraci redom?

  • Ukoliko nemate firmu registrujte se prvo.
  • U vašoj banci zatražite da vam omoguće online plaćanje. (Proverite da li je sistem online plaćanja banke “NestPay”.)
  • Nabavite plugin/modul.
  • Podešavate modul/plugin i sajt u testnoj fazi sa testnim podacima.
  • Banka/Provajder validira vašu prodavnicu.
  • Prelazak u produkciju.

 

Ali ja nikad ne znam da li ću imati sve proizvode na stanju?

  • Prodavci sa neizvesnim stanjem zaliha umesto obične naplate (SMS – prosta momentalna naplata) mogu koristiti naplatu u dva koraka (DMS) Pre-Autorizacija + Post-Autorizacija(capture).
  • Pri Pre-Autorizaciji sredstva se rezervišu na kartici kupca ali ostaju na njoj sve dok prodavac ne izvrši Post-Autorizaciju(capture) ili ih ne oslobodi (VOID).
  • Pri Post-Autorizaciji(capture) prodavac može zatvoriti transakciju na ceo rezervisan iznos ili jedan deo iznosa.
  • Ukoliko nema svih proizvoda na stanju klijentu se može naplatiti deo autorizovane sume za proizvode koje je moguće isporučiti.

 

Da li mogu da vršim naplatu pretplatnicima ili naplatu po upotrebi bez intervencije kupca (uz prethodnu saglasnost)?

  • Moguće je kreirati takozvani “recurring plan” i vršiti naplatu određene sume kupcu svakog meseca (ili N dana, N godina…) do otkaza usluge.
  • Moguće je ukoliko imate mogućnost COF naplate vršiti naplatu bez intervencije kupca ukoliko je on za to dao saglasnost (sve dok ta saglasnost postoji).
  • Ove obe opcije zahtevaju da nas kontaktirate radi konsultacija u vezi specifične implementacije.

 
 

Posted on 4 Comments

Fiskalizacija za web sajtove – ESIR / VPFR – MINIMUM MINIMUMA

HOLEST E-COMMERCE AUTOMATIZOVANO REŠENJE (ESIR / VPFR) ZA FISKALIZACIJU NA SAJTOVIMA JE U PRIPREMI I BIĆE SPREMNO PRE 30 APRILA. PRIBAVITE SERTIFIKAT (BEZBEDNOSNI ELEMENT) U MEĐUVREMENU AKO VEĆ NISTE. Korisnici naših plathnih modula biće obavešteni mailom kada plugin za fiskalizaciju bude dostupan ili možete posetiti ovu stanu kroz koji dan. 

JA PRODAJEM SAMO PREKO WEB-a. ŠTA JE ZA SAD POTREBNO DA URADIM?

  • na starnici uslova kupovine dodati rečenicu u kojoj se naglašava da fiskalini račun može biti dostavljen kupcu u elektronskom formatu.

KAKO DA NA NAJLAKŠI NAČIN POČNEM DA IZDAJEM RACUNE?

1. SERTIFIKAT SA E-POREZI

Preuzmi bezbednosni element u obiku serifikata sa e-porezi (ako ne znate sta je e-porezi ili se sami  nikad niste logovali na e-porezi to znaci da treba da se obratite Vašem knjigovođi da uradi to za vas).

(podrazumeva se da ste već prijavili neku lokaciju gde vršite delatnost, da bi između ostalog dobili subvenciju, ako niste možete i sad to da uradite)

Skidanje sertifikata:

– logujete se na e-porezi
– odabere se pravni subjekat (ako ih imate više)
– u listingu opcija idite na ESF (na početku liste), dolazi se de recapch-e koju treba proci
– Kada se uđe na TAXCORE portal ide se na Administratcija->Zahtevi za izdavanje bezbednosnoh elemenata
– Klikunuti na dugme “Zahtevaj bezbednosni element”
– Na fromi skroz dole postoji dugme + pod sekcijom “Sertifikati”. Klik na ovo dugme pa uneti lozinku i PAK (ili ostaviti ponuđene vrednosti)
– Kad već ovo radite možete da zahtevate i pametnu karticu ako vam slućajno zatreba. Za to kliknuti na dugme “+” u sekciji pamete kartice.
– Sacuvati zahtev
– Sutra ujutru će u poreskom sanducetu biti PDF sa linkom za prezimanje sertifikata (sertifikat može samo jednom da se skine – posle toga link postaje ne-upotrebljiv. Doduše nije teško pribaviti opet novi ali se čeka opet 1 dan. Ne unositi ovaj link direktno u aplikaciju opisanu u sledećem tekstu jer nećete možda posle lako moći da pronađete sertifikat na svom telefnu ako vam je potreban za druga rešenja. Uredno preuzmite fajl na svom PC-u i postavite ga na neko sigurno mesto.)

2. BESPLATNA ANDROID APLIKACIJA PORESKE UPRAVE

Da bi imali svo sto vam je potrebno za fiskalizaciju pri prodaji na WEB-u (ovo ne zadovaljava sve što Vam je potrebno kao pravnom subjektu) možete koristiti ovu besplatnu android aplikaciju poreske uprave. Ova aplikacija je sertifikovani ESIR i ako se ništa ne promeni u konfiguraciji vezana je na VPFR poreske urave.

Kada instalirate aplikaciju i pritupite joj prvi put traži će vam par dozvola koje je potrebno da prihvatite.

Onda ce prijaviti da nije konfigurisana i traziće da navedete link do .zip fajla sertifikata koji ste dobili u prethodnom poglavlju.

Posto ste već web prodavac sigurno imate bar jedan svoj sajt. Sertifikat iz prethodnog teksta uplodujte na svoj sajt (kao sto uploadujete slike i sl.) i zapamtite link koji fajl dobija posle uploada. Idealno link do fajla prosledite sebi na telefon da bi ka kopirali.  NE ZABORAVITE DA UKONITE SERVIFIKAT SA SVOG SAJTA KADA OVO ZAVRŠITE! 

Taj link unosite u aplikaciju i kada potvrdite tražiće vam PAK. PAK ce nalazi u PDF-u gde je bio i link za sertifikat. Kada unesete PAK i potvrdite sve će biti konfigurisano i vezano na VPFR poreske uprave. Možete da unosite artikle i pravite račune koje ćete slati klijentima.

TO JE SVE ŠTO VAM TREBA DA BI ISPUNILI MINIMUM MINIMUMA ZA WEB PRODAJU – I NIJE KOŠTALO NIŠTA.

3. JA HOĆU DA REŠIM SVE ZAKONSKE ZAHTEVE VEZANO ZA SVOJU FIRMU – ŠTA MI JOŠ TREBA

Zakon propisuje da morate da imate LPFR (lokalni procesor fiskalnih računa) po svakom prodajnom mestu. Da – i ako prodajete samo preko web-a i vama treba bar jedan LPFR.

LPFR hardverski/softverski obično traže pamentnu karticu kao bezbednosni element. Procedura pribavljanja je skoro ista  kao i za sertifikat samo u koristite “+” dugme u  sekciji “Pametene kartice”. Kartica se može preuzeti 3 dana posle pravljenja zahteva. Možete u isto vreme da napravite zahtev za oba tipa bezbednosnih elemenata , cak i za sertifikat, pametnu karticu u L.K. formatu i pametnu karticu u SIM formatu – da bi imali sve moguće opcije.

OPCIJA 1:

Kupiti neki najjeftiniji LPFR hardverski uređaj koji ne morate uopšte koristiti. Uredaji u ponudi su uglavnom ESIR+LPFR. U ovom slucaju provetite koj format pametne kartice ide u njega. Može biti SIM ili kao lična karta.

OPCIJA 2:

Pretplatiti se na neki softverski LPFR.  Mi smo pronašli više opcija . LPFR aplikacije za Windows koštaju od 50EUR/godina pa naviše.

Ako imate potrebu da koristite LPFR onda besplatnu aplikaciju poreske uprave možete vezati na LPFR umesto na VPFR kako je podrzumevano jer i VPFR i LPFR koriste isti SDC protokol. Telefon i računar moraju biti u tom slučaju na istoj lokalnoj mreži. Ima nekih rešanja za LPFR koji ne mogu da rade ovako – pazite na to i izbegavajte u širokom luku LPFR koji ne nudi port na kome može da primi SDC zahtev.

Ako nemate čitać pametnih kartica morate kupiti jedan u lokalnoj prodavnici računarske opreme – uskladite se sa formatom pamente kartice ka za L.K. ili SIM. Takodje ako nemate bilo kakav štampač morate kupiti neki. Zakon ne propisuje format papira za račune.

Kada se pokrene Windows LPFR tražiće vam ili sertifikat ili da pametnu karticu imate u čitaču. U informacijama ćete videti port na kome radi.

Potrebno je da saznate IP računara i ovaj port.  Takođe morate da otvorite ovaj port u windows firewall-u.  Za pomoć oko ovoga obratite se svom dobavljaču LPFR-a.

Ponavljamo još jednom telefon na kome vam je aplikacija i windows na kome radi ovaj LPFR moraju biti u istoj lokalnoj mrezi.

Potrebno je da se obe u opciju CONFIGURE u aplikaciji i da se iskluci prodazumevani VPFR.

Onda treba navesti IP adresu windows računara na kome radi sotverski LPFR i port na kome radi.

Probati kreiranje racuna.