Skip to main content
One-time payments are single, non-recurring transactions where customers pay once for a product or service. The customer selects a HitPay payment method, completes the payment via QR code or redirect, and the transaction is recorded in Stripe.

How It Works

This integration connects Stripe’s Payment Element with HitPay’s Payment Request API, allowing customers to pay using local payment methods while keeping all transaction records in Stripe.

Key Concepts

ComponentPurpose
Stripe PaymentIntentTracks the payment intent on Stripe’s side. Remains “incomplete” for external payments.
HitPay Payment RequestProcesses the actual payment via local methods (PayNow, ShopeePay, etc.) and generates QR codes.
Stripe Payment RecordsRecords the external HitPay payment back in Stripe for unified reporting and reconciliation.

API References

HitPay Payment Request API

Create payment requests and generate QR codes for local payment methods.

HitPay Embedded QR Payments

Embed QR codes directly in your checkout for seamless mobile payments.

Payment Flow

Step 1: Configure Custom Payment Methods

Register each HitPay payment method as a Custom Payment Method (CPM) type in your Stripe Dashboard, map those CPM Type IDs to their HitPay method identifiers in a config file, and load them into the Stripe Elements provider so they appear as options in your checkout.
1

Create Custom Payment Methods on Stripe Dashboard

Stripe DashboardCreate Custom Payment Method types in your Stripe Dashboard for each HitPay payment method you want to offer.After creating the custom payment method, the Dashboard displays the custom payment method ID (beginning with cpmt_) that you need for the next step.

Create CPM in Stripe Dashboard

Follow Stripe’s guide to create Custom Payment Method types and get your CPM Type IDs.

Download Payment Icons

Download official HitPay payment method icons (PayNow, ShopeePay, GrabPay, FPX, and more) optimized for Stripe Custom Payment Methods.
2

Create Configuration File

Server-sideThis configuration file maps the Custom Payment Methods you created in the Stripe Dashboard to their corresponding HitPay payment method identifiers. Each entry links a Stripe CPM Type ID (e.g. cpmt_xxx) to the HitPay API method name used when creating a payment request.See the HitPay Payment Methods Reference for the full list of supported payment method names and their identifiers.The chargeAutomatically flag indicates whether a payment method supports tokenized or recurring payments — these methods can be used to charge customers automatically without requiring re-authorization each time.The code below is a sample — adapt it to the payment methods you’ve configured:
// config/payment-methods.ts

interface CustomPaymentMethodConfig {
  id: string;                      // Stripe CPM Type ID (cpmt_xxx)
  hitpayMethod: string;            // HitPay one-time payment method
  hitpayRecurringMethod?: string;  // Example only: used when the recurring method name differs from the one-time name
  displayName: string;
  chargeAutomatically: boolean;    // Supports auto-charge subscriptions via HitPay tokenization
}

export const CUSTOM_PAYMENT_METHODS: CustomPaymentMethodConfig[] = [
  {
    id: 'cpmt_YOUR_PAYNOW_ID',      // Replace with your CPM Type ID
    hitpayMethod: 'paynow_online',
    displayName: 'PayNow',
    chargeAutomatically: false,     // QR-based, no tokenization
  },
  {
    id: 'cpmt_YOUR_SHOPEEPAY_ID',
    hitpayMethod: 'shopee_pay',
    hitpayRecurringMethod: 'shopee_recurring',
    displayName: 'ShopeePay',
    chargeAutomatically: true,
  },
  {
    id: 'cpmt_YOUR_GRABPAY_ID',
    hitpayMethod: 'grabpay',
    hitpayRecurringMethod: 'grabpay_direct',
    displayName: 'GrabPay',
    chargeAutomatically: true,
  },
];

export function getHitpayMethod(cpmTypeId: string): string | undefined {
  return CUSTOM_PAYMENT_METHODS.find(pm => pm.id === cpmTypeId)?.hitpayMethod;
}
You’ll have different CPM Type IDs in sandbox and production — they are separate Stripe accounts. Consider using environment variables or an ids: { sandbox: string, production: string } structure so you can switch between environments without code changes.
3

Add Custom Payment Method Type to Stripe Elements Configuration

Client-sideNext, add the custom payment method type to your Stripe Elements configuration. In your checkout.js file where you initialise Stripe Elements, specify the customPaymentMethods to add to the Payment Element. Provide the custom payment method ID from the previous step, the options.type and an optional subtitle.Load Stripe.js with the beta flag:
// lib/stripe-client.ts
import { loadStripe } from '@stripe/stripe-js';

export const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
  {
    betas: ['custom_payment_methods_beta_1'],
  }
);
// Add CPMs to Elements provider
import { CUSTOM_PAYMENT_METHODS } from '@/config/payment-methods';

const elementsOptions = {
  clientSecret,
  customPaymentMethods: CUSTOM_PAYMENT_METHODS.map(pm => ({
    id: pm.id,
    options: { type: 'static' as const },
  })),
};

<Elements stripe={stripePromise} options={elementsOptions}>
  <CheckoutForm />
</Elements>
After loading, the Payment Element shows your custom payment method.

Add CPM to Elements

Learn more about configuring Custom Payment Methods in the Payment Element.

Step 2: Display Payment on Stripe Payment Elements

When a customer selects a HitPay payment method in the Payment Element, your backend creates a HitPay payment request and returns a QR code or deep link for the customer to complete payment on their mobile device.
1

Configure Environment Variables

Server-sideSet up the required API keys and configuration for both Stripe and HitPay. These credentials authenticate your server with both payment platforms.
# Stripe
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx

# HitPay
HITPAY_API_KEY=xxx
HITPAY_SALT=xxx
NEXT_PUBLIC_HITPAY_ENV=sandbox  # or 'production'
NEXT_PUBLIC_SITE_URL=https://your-domain.com
Never expose STRIPE_SECRET_KEY or HITPAY_API_KEY to the client.
2

Create HitPay Payment Request

Server-sideWhen a user selects a CPM in the Payment Element, create a HitPay payment request and display the QR code:
// app/api/hitpay/create/route.ts
import { NextResponse } from 'next/server';

const HITPAY_API_URL = process.env.NEXT_PUBLIC_HITPAY_ENV === 'production'
  ? 'https://api.hit-pay.com/v1'
  : 'https://api.sandbox.hit-pay.com/v1';

export async function POST(request: Request) {
  const { amount, currency, paymentMethod, paymentIntentId } = await request.json();

  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;

  const response = await fetch(`${HITPAY_API_URL}/payment-requests`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-BUSINESS-API-KEY': process.env.HITPAY_API_KEY!,
    },
    body: JSON.stringify({
      amount: (amount / 100).toFixed(2),  // Convert cents to dollars
      currency,
      payment_methods: [paymentMethod],
      reference_number: paymentIntentId,
      webhook: `${baseUrl}/api/hitpay/webhook`,
      generate_qr: true,
    }),
  });

  const data = await response.json();

  return NextResponse.json({
    paymentRequestId: data.id,
    qrCode: data.qr_code_data?.qr_code,
    directLinkUrl: data.direct_link?.direct_link_url,
    checkoutUrl: data.url,
  });
}
Displaying QR Codes: The generate_qr: true parameter returns QR code data in qr_code_data.qr_code. For detailed guidance on rendering QR codes and handling different payment methods, see our Embedded QR Code Payments guide.
App-based methods (ShopeePay, GrabPay): Some payment methods don’t generate a QR code — instead, HitPay returns a direct_link.direct_link_url that opens the payment app directly. When directLinkUrl is present, redirect the customer to that URL instead of displaying a QR code.

Embedded QR Code Payments

Learn how to embed and display QR codes for PayNow, ShopeePay, and other supported payment methods.
3

Display QR Code in Payment Element

Client-sideRender the QR code in your checkout form when a customer selects a HitPay payment method. The customer scans the QR code with their mobile app to complete the payment.
// components/CheckoutForm.tsx
'use client';

import { useState } from 'react';
import { PaymentElement } from '@stripe/react-stripe-js';
import { QRCodeSVG } from 'qrcode.react';
import { CUSTOM_PAYMENT_METHODS, getHitpayMethod } from '@/config/payment-methods';

export function CheckoutForm({ amount, currency, paymentIntentId }) {
  const [qrCode, setQrCode] = useState<string | null>(null);
  const [selectedCpm, setSelectedCpm] = useState<string | null>(null);

  const handlePaymentElementChange = async (event: any) => {
    const paymentType = event.value?.type;

    // Check if selected payment method is a CPM
    const isCpm = CUSTOM_PAYMENT_METHODS.some(pm => pm.id === paymentType);

    if (isCpm) {
      setSelectedCpm(paymentType);

      // Create HitPay payment request
      const hitpayMethod = getHitpayMethod(paymentType);
      const response = await fetch('/api/hitpay/create', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          amount,
          currency,
          paymentMethod: hitpayMethod,
          paymentIntentId,
        }),
      });

      const data = await response.json();

      if (data.directLinkUrl) {
        // App-based method (e.g. ShopeePay, GrabPay) — redirect to payment app
        window.location.href = data.directLinkUrl;
      } else {
        setQrCode(data.qrCode);
      }
    } else {
      setSelectedCpm(null);
      setQrCode(null);
    }
  };

  return (
    <div>
      <PaymentElement onChange={handlePaymentElementChange} />

      {qrCode && (
        <div className="qr-container">
          <h3>Scan to Pay</h3>
          <QRCodeSVG value={qrCode} size={256} />
          <p>Waiting for payment...</p>
        </div>
      )}
    </div>
  );
}

Step 3: Record Payment Confirmation

Once the customer pays via HitPay, your backend receives a webhook, verifies the payment, and records it in Stripe using the Payment Records API — making it visible in your Stripe Dashboard alongside native card payments.
Without this step, HitPay payments only appear in your HitPay Dashboard. By recording them via Stripe’s Payment Records API, you get a single source of truth for all transactions — cards, PayNow, ShopeePay, and more — all in one place.

Stripe Payment Records API

Learn how Payment Records work and how they appear in your Stripe Dashboard.
1

Listen for Payment Confirmation

Server-side
2

Record the Payment to Your Stripe Account

Server-sideOnce HitPay confirms the payment is complete, call stripe.paymentRecords.reportPayment() to create a Payment Record (prec_xxx) in Stripe. This is what makes the transaction visible in your Stripe Dashboard alongside native card payments.
The original PaymentIntent remains "Incomplete" — this is expected for external payments. The Payment Record is the canonical record of the transaction and what Stripe uses for revenue reporting.
Each field in the payload serves a specific purpose:
FieldValueWhy
amount_requestedPaymentIntent amount + currencyTies the record to the original order amount
payment_method_details.payment_methodCustom PaymentMethod ID (pm_xxx)Links the CPM type (PayNow, ShopeePay, etc.) to this record
processor_details.custom.payment_referenceHitPay payment IDExternal transaction reference for reconciliation
initiated_atUnix timestampWhen the payment was initiated
customer_presence'on_session'Customer was actively present at time of payment
outcome'guaranteed'HitPay has confirmed settlement — funds are guaranteed
guaranteed.guaranteed_atUnix timestampWhen settlement was confirmed by HitPay
metadataHitPay IDs, PI IDSearchable references for support and debugging
// After HitPay confirms payment is completed:

// 1. Create a PaymentMethod instance for your CPM type
const paymentMethod = await stripe.paymentMethods.create({
  type: 'custom',
  custom: { type: 'cpmt_xxx' }, // your CPM Type ID from Stripe Dashboard
});

// 2. Report the payment — creates a prec_xxx record in Stripe
const paymentRecord = await stripe.paymentRecords.reportPayment({
  amount_requested: {
    value: paymentIntent.amount,    // in cents, matches the original PaymentIntent
    currency: paymentIntent.currency,
  },
  payment_method_details: {
    payment_method: paymentMethod.id, // links the CPM type to this record
  },
  processor_details: {
    type: 'custom',
    custom: {
      payment_reference: hitpayPaymentId, // HitPay's payment ID for reconciliation
    },
  },
  initiated_at: Math.floor(Date.now() / 1000),
  customer_presence: 'on_session',
  outcome: 'guaranteed',
  guaranteed: { guaranteed_at: Math.floor(Date.now() / 1000) },
  metadata: {
    hitpay_payment_id: hitpayPaymentId,
    hitpay_payment_request_id: hitpayPaymentRequestId,
    stripe_payment_intent_id: paymentIntentId,
  },
});

// 3. Update PaymentIntent metadata for easy lookup
await stripe.paymentIntents.update(paymentIntentId, {
  metadata: {
    external_payment_status: 'completed',
    stripe_payment_record_id: paymentRecord.id, // prec_xxx
  },
});

Testing

Sandbox Testing

  1. Set NEXT_PUBLIC_HITPAY_ENV=sandbox in your environment
  2. Use HitPay sandbox API credentials
  3. When a QR code is displayed, HitPay provides a “Complete Mock Payment” link
  4. Click this link to simulate a successful payment

Production Testing

  1. Switch to production credentials
  2. Use real payment apps to scan QR codes
  3. Verify payments appear in both HitPay and Stripe dashboards
PaymentIntents will show as “Incomplete” in Stripe - this is expected. External payments are tracked via Payment Records (prec_* IDs).

FAQ

  • Verify the CPM Type is enabled in Stripe Dashboard
  • Check that the CPM ID in your config matches the Dashboard
  • Ensure you’re loading Stripe.js with custom_payment_methods_beta_1
  • Not all HitPay methods support QR codes (e.g., GrabPay uses redirect)
  • Check the checkoutUrl fallback is being displayed
  • Verify the payment method is enabled in HitPay Dashboard
  • The payment method may not be enabled in your HitPay account
  • Some methods aren’t available in sandbox mode
  • Check the error message for specific details
  • Check server logs for Payment Records API errors
  • Verify your Stripe API version includes the beta flag
  • The payment still succeeded if HitPay shows completed