drop-in-ui

Installation
SKILL.md

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 onClick={handleCheckout}>
      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

  1. Always verify via webhook — The onSuccess callback is client-side and can be spoofed. Never fulfill orders based solely on the callback. See the webhook-handler skill.
  2. HitPay.js is loaded globally — It attaches to window.HitPay. Only call init() after the script has loaded.
  3. One modal at a time — Calling init() again replaces the previous payment session.
  4. Mobile behavior — The Drop-In opens as a full-screen overlay on mobile devices.
  5. CSP headers — If using Content Security Policy, allow frame-src for hit-pay.com or sandbox.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.

Related skills
Installs
1
First Seen
Apr 21, 2026