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.
What happens after a customer pays¶
- The customer clicks Pay in the widget.
- The widget creates a payment session against the configured gateway — CyberSource for card payments, Fonepay for QR payments.
- The top-level browser window navigates to the gateway's hosted payment page.
- The customer completes the card payment or scans the QR code.
- The gateway redirects the customer to your configured return URL, or to the HGN hosted fallback page.
- That result page receives
statusandpaymentRefquery parameters.
| Parameter | Values |
|---|---|
status |
success, failed, cancelled, or expired |
paymentRef |
The order reference, used for verification |
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¶
Call this from your server, not from browser JavaScript, and use its response to decide what the customer sees.
Examples¶
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(
`https://book.himalayanguardian.com/api/public/book/payment-status/${encodeURIComponent(paymentRef)}`
);
if (!verify.ok) {
// Could not confirm — show a neutral "we're checking" page,
// never a confirmation.
return res.render("pending", { paymentRef });
}
const result = await verify.json();
switch (result.status) {
case "success":
await fulfilBooking(paymentRef); // emails, records, inventory
return res.render("success", { paymentRef });
case "failed":
return res.render("failed", { paymentRef });
case "cancelled":
return res.render("cancelled", { paymentRef });
case "expired":
return res.render("expired", { paymentRef });
default:
return res.render("pending", { paymentRef });
}
});
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(
`https://book.himalayanguardian.com/api/public/book/payment-status/${encodeURIComponent(paymentRef)}`,
{ cache: "no-store" }
);
if (!res.ok) return <PendingState paymentRef={paymentRef} />;
const { status } = await res.json();
if (status === "success") return <SuccessState paymentRef={paymentRef} />;
if (status === "failed") return <FailedState paymentRef={paymentRef} />;
if (status === "cancelled") return <CancelledState paymentRef={paymentRef} />;
if (status === "expired") return <ExpiredState paymentRef={paymentRef} />;
return <PendingState paymentRef={paymentRef} />;
}
<?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://book.himalayanguardian.com/api/public/book/payment-status/'
. rawurlencode($paymentRef);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$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;
}
$result = json_decode($body, true);
switch ($result['status'] ?? '') {
case 'success':
fulfil_booking($paymentRef);
include 'views/success.php';
break;
case 'failed': include 'views/failed.php'; break;
case 'cancelled': include 'views/cancelled.php'; break;
case 'expired': include 'views/expired.php'; break;
default: include 'views/pending.php';
}
import requests
from flask import request, render_template
VERIFY_URL = "https://book.himalayanguardian.com/api/public/book/payment-status/{}"
@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(VERIFY_URL.format(payment_ref), timeout=15)
res.raise_for_status()
except requests.RequestException:
return render_template("pending.html", payment_ref=payment_ref)
status = res.json().get("status")
if status == "success":
fulfil_booking(payment_ref)
return render_template("success.html", payment_ref=payment_ref)
return render_template(
{
"failed": "failed.html",
"cancelled": "cancelled.html",
"expired": "expired.html",
}.get(status, "pending.html"),
payment_ref=payment_ref,
)
Designing the result page¶
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¶
- Integration — configuring the
return_urlon the iframe - Troubleshooting — customers not returning to your site