Skip to content

Payment Verification

Only needed if you built your own result page. If you use the HGN hosted result page, verification is handled for you and you can skip this page.

Corrected in widget 1.1 — this page previously documented an endpoint that does not exist

Earlier versions of this guide told you to verify against:

GET https://book.hgn.com.np/api/public/book/payment-status/{paymentRef}

That URL has never existed and returns 404. If you built a result page against it, your verification call has been failing every time. Following the rest of this guide correctly — never confirm without a verified response — means your customers have been seeing a "we are confirming your payment" page instead of a confirmation.

The working procedure is below. If you are unsure whether you are affected, check your server logs for 404 responses to book.hgn.com.np.


What happens after a customer pays

  1. The customer clicks Pay in the widget.
  2. The widget creates a payment session against the configured gateway — CyberSource for card payments, Fonepay for QR payments.
  3. The top-level browser window navigates to the gateway's hosted payment page.
  4. The customer completes the card payment or scans the QR code.
  5. The gateway redirects the customer to your configured return URL, or to the HGN hosted fallback page.
  6. That result page receives status and paymentRef query parameters.
https://your-agency.com/booking-complete?status=success&paymentRef=ORD-2026-000247-1786524461302
Parameter Values
status success, failed, cancelled, or expired
paymentRef The gateway payment reference, used for verification

paymentRef is not an order reference

Its shape is <orderRef>-<epochMillis>. It looks like an order reference with a suffix. Treat it as an opaque lookup key: do not store it as an order reference, and do not try to recover the order reference by splitting on the last hyphen.

The booking exists before payment

The booking is created server-side before payment is attempted. A successful payment transitions the booking status. If payment fails, the booking stays pending payment and the customer can retry — a failed payment does not lose their work.


Never trust the query string

This is a security requirement, not a suggestion

Anyone can type ?status=success into their address bar and load your result page. If your page shows a confirmation based on that parameter alone, you will confirm bookings that were never paid for.

Always verify server-side using paymentRef before showing a final confirmation.

Use the verified response — never the query string — to drive anything that matters: confirmation screens, emails, releasing inventory, or updating your own records.


The verification endpoint

Verification uses the Partner API, called from your server:

GET https://apitest.himalayanguardian.com/api/external/v1/orders/{paymentRef}/payment-status
Authorization: Bearer <your access token>

This is the sandbox host — do not ship it

Every example on this page uses apitest.himalayanguardian.com, HGN's test server. Build and test against it freely.

Your production host and live credentials are issued separately, over WhatsApp or another secure channel agreed with your account manager. They are deliberately not published here. Point your live result page at the host you were given — verifying a real payment against the sandbox will not find the order, and your page will sit in the "confirming your payment" state forever.

Keep the host in an environment variable rather than inline, so moving from test to live is a config change and not a code edit. See Environments for every sandbox host.

Pass the paymentRef from the return URL through verbatim — the endpoint accepts the gateway payment reference and resolves it back to the order it paid for.

This endpoint requires Partner API credentials

Unlike the publishable key, these are secret and must never reach a browser. Ask your HGN account manager to issue Partner API client credentials, then follow Authentication to exchange them for an access token.

There is no unauthenticated public equivalent. If you do not want to hold API credentials, use the HGN hosted result page instead — it needs no verification work from you.

Response

{
  "success": true,
  "data": {
    "orderRef": "ORD-2026-000247",
    "orderStatus": "PAID",
    "paymentRef": "PAY-2026-000001",
    "paymentStatus": "SUCCESS",
    "paymentMethod": "CYBERSOURCE",
    "amount": 590.00,
    "currency": "NPR",
    "completedAt": "2026-07-07T03:30:00Z"
  }
}

Read data.paymentStatus. It is UPPERCASE — it does not match the lowercase status query parameter, so compare against the right vocabulary:

paymentStatus Meaning
SUCCESS Paid. Safe to confirm and fulfil
NOT_INITIATED No payment attempted yet. This is a 200, not an error
PENDING, INITIATED In flight. Do not confirm
FAILED, CANCELLED Not paid

Guard on the field, not the shape

The "no payment yet" response omits paymentRef, amount, currency, and completedAt entirely. Read paymentStatus first and only reach for the other fields once it is something other than NOT_INITIATED.

The multi-traveller limitation

A widget booking creates one order per traveller, grouped together. When a customer pays for more than one traveller in a single checkout, the resulting paymentRef covers an order group and cannot resolve to a single order — the endpoint returns 400.

Plan for this before you build a custom result page

Multi-traveller bookings are common. If you cannot verify them, your result page must fall back to the neutral "we are confirming your payment" state for exactly the bookings that are worth the most.

Two ways forward:

  • Use the HGN hosted result page. It handles every case, including groups. This is the recommended option.
  • Talk to your account manager about group-level verification before committing to a custom page.

Examples

Each example assumes you already hold a Partner API access token. See Authentication for obtaining and caching one.

const HGN_BASE = "https://apitest.himalayanguardian.com/api/external/v1";

app.get("/booking-complete", async (req, res) => {
  const { paymentRef } = req.query;

  if (!paymentRef) {
    return res.status(400).render("error");
  }

  // Verify server-side. The `status` query param is NOT trusted.
  const verify = await fetch(
    `${HGN_BASE}/orders/${encodeURIComponent(paymentRef)}/payment-status`,
    { headers: { Authorization: `Bearer ${await getAccessToken()}` } }
  );

  if (!verify.ok) {
    // Includes the 400 returned for multi-traveller group payments.
    // Show a neutral "we're checking" page, never a confirmation.
    return res.render("pending", { paymentRef });
  }

  const { data } = await verify.json();

  switch (data.paymentStatus) {
    case "SUCCESS":
      await fulfilBooking(data.orderRef);   // emails, records, inventory
      return res.render("success", { paymentRef, orderRef: data.orderRef });
    case "FAILED":
      return res.render("failed", { paymentRef });
    case "CANCELLED":
      return res.render("cancelled", { paymentRef });
    default:
      // NOT_INITIATED, PENDING, INITIATED — not paid yet.
      return res.render("pending", { paymentRef });
  }
});
app/booking-complete/page.tsx
const HGN_BASE = "https://apitest.himalayanguardian.com/api/external/v1";

export default async function BookingComplete({
  searchParams,
}: {
  searchParams: Promise<{ paymentRef?: string }>;
}) {
  const { paymentRef } = await searchParams;
  if (!paymentRef) return <ErrorState />;

  // Runs on the server. Never trust searchParams.status.
  const res = await fetch(
    `${HGN_BASE}/orders/${encodeURIComponent(paymentRef)}/payment-status`,
    {
      headers: { Authorization: `Bearer ${await getAccessToken()}` },
      cache: "no-store",
    }
  );

  if (!res.ok) return <PendingState paymentRef={paymentRef} />;

  const { data } = await res.json();

  if (data.paymentStatus === "SUCCESS")
    return <SuccessState orderRef={data.orderRef} />;
  if (data.paymentStatus === "FAILED")
    return <FailedState paymentRef={paymentRef} />;
  if (data.paymentStatus === "CANCELLED")
    return <CancelledState paymentRef={paymentRef} />;

  return <PendingState paymentRef={paymentRef} />;
}
booking-complete.php
<?php
$paymentRef = $_GET['paymentRef'] ?? null;

if (!$paymentRef) {
    http_response_code(400);
    exit('Missing payment reference');
}

// Verify server-side. $_GET['status'] is NOT trusted.
$url = 'https://apitest.himalayanguardian.com/api/external/v1/orders/'
     . rawurlencode($paymentRef) . '/payment-status';

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . hgn_access_token(),
]);
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($code !== 200) {
    include 'views/pending.php';   // never a confirmation
    exit;
}

$data = json_decode($body, true)['data'] ?? [];

switch ($data['paymentStatus'] ?? '') {
    case 'SUCCESS':
        fulfil_booking($data['orderRef']);
        include 'views/success.php';
        break;
    case 'FAILED':    include 'views/failed.php';    break;
    case 'CANCELLED': include 'views/cancelled.php'; break;
    default:          include 'views/pending.php';
}
import requests
from flask import request, render_template

HGN_BASE = "https://apitest.himalayanguardian.com/api/external/v1"


@app.route("/booking-complete")
def booking_complete():
    payment_ref = request.args.get("paymentRef")
    if not payment_ref:
        return render_template("error.html"), 400

    # Verify server-side. request.args["status"] is NOT trusted.
    try:
        res = requests.get(
            f"{HGN_BASE}/orders/{payment_ref}/payment-status",
            headers={"Authorization": f"Bearer {get_access_token()}"},
            timeout=15,
        )
        res.raise_for_status()
    except requests.RequestException:
        return render_template("pending.html", payment_ref=payment_ref)

    data = res.json().get("data", {})
    status = data.get("paymentStatus")

    if status == "SUCCESS":
        fulfil_booking(data["orderRef"])
        return render_template("success.html", order_ref=data["orderRef"])

    return render_template(
        {
            "FAILED": "failed.html",
            "CANCELLED": "cancelled.html",
        }.get(status, "pending.html"),
        payment_ref=payment_ref,
    )

Designing the result page

An agency's own booking-complete page reading "Payment Confirmed", with the order reference ORD-2026-000227 and a Back to Home link, styled entirely in the agency's own branding

The page above is on the agency's own domain, in their own design, and it reached the confirmed state only after the server-side check came back SUCCESS. Note that it shows the order reference returned by verification rather than the paymentRef from the query string — that is the value worth storing and quoting to support.

Handle all four states. Customers reach failed and cancelled far more often than teams expect.

State What to show
success Confirmation, order reference, what happens next
failed Plain explanation and a clear way to retry. The booking is still pending, so retrying works
cancelled Acknowledge the customer backed out, offer a link back to the booking page
expired Explain the session timed out and offer to start again
Verification unavailable A neutral "we are confirming your payment" message. Never a confirmation

Always show the payment reference

Whatever the state, display paymentRef on the page. It is the first thing HGN support will ask for.


Next steps