HitPay Drop-In UI
Embed HitPay's hosted checkout as a modal overlay on your site using HitPay.js. The Drop-In is best suited for QR-based payment methods (PayNow, GrabPay, ShopeePay, etc.) — no PCI scope required. For card payments, Apple Pay, or Google Pay, use the redirect checkout flow instead.
When to Apply
- Developer wants an embedded checkout without building custom UI
- Developer asks about HitPay.js, payment modal, or checkout popup
- Need a simpler alternative to building redirect or QR flows from scratch
Building custom flows? See the payment-integration skill for redirect and embedded QR patterns. Payment method lookup? See the payment-methods skill.
When to Use Drop-In vs Other Approaches
| Approach | Best For | PCI Scope | Customization |
|---|---|---|---|
| Drop-In UI | QR-based methods (PayNow, GrabPay, ShopeePay, etc.) | None | Limited (colors, logo) |
| Redirect | Card payments, Apple Pay, Google Pay | None | None (HitPay page) |
| Embedded QR | QR-only payments, custom UI | None | Full control |
Important: Drop-In UI is recommended for QR-based payment methods only. For card payments, use the redirect flow — 3DS authentication pop-ups do not work reliably in the Drop-In modal.
Drop-In Limitations
- Cards not recommended: 3DS authentication pop-ups cause issues inside the Drop-In modal — use redirect flow for card payments
- Apple Pay / Google Pay: Not supported — use redirect flow instead
- Custom styling: Limited to color and logo customization
- Mobile: Opens as full-screen overlay on mobile devices
Step 1: Include HitPay.js
Add the script tag to your HTML or layout:
<!-- Production -->
<script src="https://hit-pay.com/hitpay.js"></script>
<!-- Sandbox -->
<script src="https://sandbox.hit-pay.com/hitpay.js"></script>
For Next.js, add via next/script:
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Script
src={process.env.NEXT_PUBLIC_HITPAY_ENV === 'production'
? 'https://hit-pay.com/hitpay.js'
: 'https://sandbox.hit-pay.com/hitpay.js'}
strategy="afterInteractive"
/>
</body>
</html>
);
}
Step 2: Create Payment Request (Server)
Create a payment request on your server, then pass the URL to the client:
// app/api/payments/create/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { amount, currency, orderId, email } = await request.json();
const baseUrl = process.env.HITPAY_ENV === 'production'
? 'https://api.hit-pay.com'
: 'https://api.sandbox.hit-pay.com';
const response = await fetch(`${baseUrl}/v1/payment-requests`, {
method: 'POST',
headers: {
'X-BUSINESS-API-KEY': process.env.HITPAY_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount,
currency,
email,
reference_number: orderId,
redirect_url: `${process.env.NEXT_PUBLIC_APP_URL}/payment/complete`,
webhook: `${process.env.NEXT_PUBLIC_APP_URL}/api/webhooks/hitpay`,
}),
});
const data = await response.json();
return NextResponse.json({
paymentRequestId: data.id,
paymentUrl: data.url,
});
}
Step 3: Initialize Drop-In (Client)
// components/DropInCheckout.tsx
'use client';
import { useCallback } from 'react';
declare global {
interface Window {
HitPay: {
init(url: string, options: HitPayInitOptions): void;
toggle(options?: { open: boolean }): void;
};
}
}
interface HitPayInitOptions {
domain?: string;
apiDomain?: string;
onSuccess?: (data: { payment_request_id: string; status: string; reference_number?: string }) => void;
onError?: (error: { message: string }) => void;
onClose?: () => void;
}
interface Props {
amount: number;
currency: string;
orderId: string;
email?: string;
}
export function DropInCheckout({ amount, currency, orderId, email }: Props) {
const handleCheckout = useCallback(async () => {
// 1. Create payment request on server
const response = await fetch('/api/payments/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount, currency, orderId, email }),
});
const { paymentUrl } = await response.json();
// 2. Initialize Drop-In with the payment URL
window.HitPay.init(paymentUrl, {
domain: process.env.NEXT_PUBLIC_HITPAY_ENV === 'production'
? 'hit-pay.com'
: 'sandbox.hit-pay.com',
apiDomain: process.env.NEXT_PUBLIC_HITPAY_ENV === 'production'
? 'api.hit-pay.com'
: 'api.sandbox.hit-pay.com',
onSuccess: (data) => {
console.log('Payment successful:', data.payment_request_id);
// Redirect to success page or update UI
// IMPORTANT: Always verify via webhook before fulfilling
window.location.href = `/payment/complete?ref=${data.reference_number}`;
},
onError: (error) => {
console.error('Payment error:', error.message);
// Show error to user
},
onClose: () => {
console.log('Payment modal closed');
// User closed without completing — no action needed
},
});
// 3. Open the Drop-In modal
window.HitPay.toggle({ open: true });
}, [amount, currency, orderId, email]);
return (
<button
Pay {currency} {amount.toFixed(2)}
</button>
);
}
HitPay.init() Options
| Option | Type | Required | Description |
|---|---|---|---|
domain |
string | Yes | 'hit-pay.com' (prod) or 'sandbox.hit-pay.com' (sandbox) |
apiDomain |
string | Yes | 'api.hit-pay.com' (prod) or 'api.sandbox.hit-pay.com' (sandbox) |
onSuccess |
function | No | Called when payment completes successfully |
onError |
function | No | Called when payment fails |
onClose |
function | No | Called when user closes the modal |
onSuccess Callback Data
{
payment_request_id: string; // HitPay payment request ID
status: string; // "completed"
reference_number?: string; // Your order reference
}
onError Callback Data
{
message: string; // Human-readable error message
}
Full Next.js Example
Server Component (Page)
// app/checkout/page.tsx
import { DropInCheckout } from '@/components/DropInCheckout';
export default function CheckoutPage({
searchParams,
}: {
searchParams: { orderId: string; amount: string };
}) {
return (
<main>
<h1>Complete Your Purchase</h1>
<DropInCheckout
amount={parseFloat(searchParams.amount)}
currency="SGD"
orderId={searchParams.orderId}
/>
</main>
);
}
Payment Complete Page
// app/payment/complete/page.tsx
export default function PaymentCompletePage({
searchParams,
}: {
searchParams: { ref?: string };
}) {
return (
<main>
<h1>Payment Received</h1>
<p>Thank you! Your order {searchParams.ref} is being processed.</p>
<p>You will receive a confirmation email shortly.</p>
</main>
);
}
Important Notes
- Always verify via webhook — The
onSuccesscallback is client-side and can be spoofed. Never fulfill orders based solely on the callback. See the webhook-handler skill. - HitPay.js is loaded globally — It attaches to
window.HitPay. Only callinit()after the script has loaded. - One modal at a time — Calling
init()again replaces the previous payment session. - Mobile behavior — The Drop-In opens as a full-screen overlay on mobile devices.
- CSP headers — If using Content Security Policy, allow
frame-srcforhit-pay.comorsandbox.hit-pay.com.
Environment Variables
# .env.local
HITPAY_API_KEY=your_api_key
HITPAY_SALT=your_webhook_salt
HITPAY_ENV=sandbox # or "production"
NEXT_PUBLIC_HITPAY_ENV=sandbox # client-side env detection
NEXT_PUBLIC_APP_URL=http://localhost:3000
See references/dropin-ui-api.md for the full HitPay.js API reference including CSS customization.