The payment integration follows a structured three-stage approach:
Payment Initiation : Generating a unique transaction payload
Payment Processing : Redirecting to PhonePe's secure payment interface
Payment Verification : Confirming transaction status server-side
To begin, let’s set up a fresh Next.js application. Run the following command in your terminal to create a new Next.js project:
terminal npx create - next - app@latest
terminal cd you - app - name
Just normal stuff.
Now create a button which just say Pay ₹1
We start by creating a simple payment button that triggers the payment process. In the Home component, the handlePay function is called when the user clicks the button. This function interacts with the server-side initiatePayment action to initiate the payment flow.
src/app/page.tsx " use client " ;
import { initiatePayment } from " @/actions/initiatePayment " ;
import { useRouter } from " next/navigation " ;
export default function Home () {
const router = useRouter () ;
const handlePay = async ( data : number ) => {
try {
const result = await initiatePayment ( data ) ;
if ( result ) {
router . push ( result . redirectUrl ) ; // Redirect to status check page
}
} catch ( error ) {
console . error ( " Error processing payment: " , error ) ;
}
};
return (
< div className = " flex items-center justify-center h-screen " >
< button
onClick ={() => handlePay ( 1 ) } // Trigger payment for ₹1
>
Pay ₹1
</ button >
</ div >
) ;
}
Now the interesting thing is initiatePayment actions
src/actions/initiatePayment.ts " use server " ;
import { v4 as uuidv4 } from " uuid " ;
import sha256 from " crypto-js/sha256 " ;
import axios from " axios " ;
export async function initiatePayment ( data : number ) {
const transactionId = " Tr- " + uuidv4 () . toString () . slice ( - 6 ) ; // Here I am generating random id you can send the id of the product you are selling or anything else.
const payload = {
merchantId : process . env . NEXT_PUBLIC_MERCHANT_ID ,
merchantTransactionId : transactionId ,
merchantUserId : " MUID- " + uuidv4 () . toString () . slice ( - 6 ) ,
amount : 100 * data , // Amount is converted to the smallest currency unit (e.g., cents/paise) by multiplying by 100. For example, $1 = 100 cents or ₹1 = 100 paise.
redirectUrl : `${ process . env . NEXT_URL } /status/ ${ transactionId }` ,
redirectMode : " REDIRECT " ,
callbackUrl : `${ process . env . NEXT_URL } /status/ ${ transactionId }` ,
paymentInstrument : {
type : " PAY_PAGE " ,
},
};
const dataPayload = JSON . stringify ( payload ) ;
const dataBase64 = Buffer . from ( dataPayload ) . toString ( " base64 " ) ;
const fullURL = dataBase64 + " /pg/v1/pay " + process . env . NEXT_PUBLIC_SALT_KEY ;
const dataSha256 = sha256 ( fullURL ) . toString () ;
const checksum = dataSha256 + " ### " + process . env . NEXT_PUBLIC_SALT_INDEX ;
const UAT_PAY_API_URL = `${ process . env . NEXT_PUBLIC_PHONE_PAY_HOST_URL } /pg/v1/pay ` ;
try {
const response = await axios . post (
UAT_PAY_API_URL ,
{ request : dataBase64 },
{
headers : {
accept : " application/json " ,
" Content-Type " : " application/json " ,
" X-VERIFY " : checksum ,
},
},
) ;
return {
redirectUrl : response . data . data . instrumentResponse . redirectInfo . url ,
transactionId : transactionId ,
};
} catch ( error ) {
console . error ( " Error in server action: " , error ) ;
throw error ;
}
}
Now setting up .env
NEXT_URL= http://localhost:3000
NEXT_PUBLIC_PHONE_PAY_HOST_URL=https://api-preprod.phonepe.com/apis/pg-sandbox
NEXT_PUBLIC_MERCHANT_ID=PGTESTPAYUAT86
NEXT_PUBLIC_SALT_KEY= 96434309-7796-489d-8924-ab56988a6076
NEXT_PUBLIC_SALT_INDEX=1
The following environment variables are provided by PhonePe for testing purposes in their sandbox.
Once you transition to production, PhonePe will provide you with production keys and URLs.
Get OTP
This is test credential provided by phonepe you can find it here Phonepe Documentation
(I have created these components just for fun purpose 👨💻)
Now Step 1 and Step 2 are done . Let’s Move on to Step 3
Now create a new route src/app/status/[id]/page.tsx , because if you see in the initiatePayment function I have given redirectUrl
src/app/status/[id]/page.tsx ' use client '
import { useParams } from ' next/navigation '
import { useState , useEffect } from ' react '
import axios , { AxiosError } from ' axios '
import Link from ' next/link '
const StatusPage = () => {
const params = useParams ()
const [ status , setStatus ] = useState ( null )
const [ loading , setLoading ] = useState ( true )
const [ error , setError ] = useState ( null )
useEffect ( () => {
const fetchStatus = async () => {
try {
const response = await axios . post ( ' /api/status ' , { id : params ?. id } )
setStatus ( response . data )
} catch ( error ) {
const errorMessage =
error instanceof AxiosError
? error . response ?. data ?. message || error . message
: ' Something went wrong. Please contact the website owner. '
setError ( errorMessage )
} finally {
setLoading ( false )
}
}
if ( params ?. id ) {
fetchStatus ()
}
}, [ params ?. id ])
return (
< div className = " flex items-center justify-center h-screen " >
< div className = " bg-white p-8 rounded-lg shadow-lg w-full max-w-md " >
{ loading ? (
< div className = " flex justify-center items-center space-x-2 " >
< div className = " w-8 h-8 border-4 border-t-4 border-gray-300 border-t-transparent rounded-full animate-spin " ></ div >
< p className = " text-lg text-gray-700 " > Loading... </ p >
</ div >
) : error ? (
< div className = " text-center " >
< p className = " text-red-500 text-lg " >{ error }</ p >
< button
className = " mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-700 "
onClick ={() => {
setLoading ( true )
setError ( null )
setStatus ( null )
}}
>
Retry
</ button >
</ div >
) : (
< div className = " text-center " >
< h1 className = " text-2xl font-bold text-gray-800 " >
Status: { status ? status : ' No status available ' }
</ h1 >
< Link href = " / " passHref >
< button className = " mt-4 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-700 " >
Back
</ button >
</ Link >
</ div >
) }
</ div >
</ div >
)
}
export default StatusPage
Now create an api to verify transaction
src/app/api/status/route.ts import { NextRequest , NextResponse } from " next/server " ;
import sha256 from " crypto-js/sha256 " ;
import axios from " axios " ;
export async function POST ( req : NextRequest ) : Promise < NextResponse >{
try {
const { id } = await req . json () ;
const merchantId = process . env . NEXT_PUBLIC_MERCHANT_ID ;
const transactionId = id ;
const st = ` /pg/v1/status/ ${ merchantId } / ${ transactionId }` + process . env . NEXT_PUBLIC_SALT_KEY ;
const dataSha256 = sha256 ( st ) . toString () ;
const checksum = dataSha256 + " ### " + process . env . NEXT_PUBLIC_SALT_INDEX ;
console . log ( checksum ) ;
const options = {
method : " GET " ,
url : `${ process . env . NEXT_PUBLIC_PHONE_PAY_HOST_URL } /pg/v1/status/ ${ merchantId } / ${ transactionId }` ,
headers : {
accept : " application/json " ,
" Content-Type " : " application/json " ,
" X-VERIFY " : checksum ,
" X-MERCHANT-ID " : `${ merchantId }` ,
},
};
const response = await axios . request ( options ) ;
console . log ( " r=== " , response ) ;
if ( response . data . code === " PAYMENT_SUCCESS " ) {
return new NextResponse ( response . data . code , { status : 200 } ) ;
} else {
return new NextResponse ( " FAIL " , { status : 200 } ) ;
}
} catch ( error ) {
console . error ( " Error in payment status check: " , error ) ;
return NextResponse . redirect ( " https://faisalhusa.in " , {
status : 301 ,
} ) ;
}
}
Congratulations! 🎊
You've successfully integrated the PhonePe payment gateway into your Next.js app. Thank you for reading through the guide. If you encounter any issues or have further questions, feel free to reach out to me at faisalhusain1320@gmail.com . The code and official documentation for PhonePe are provided below for your reference.
PhonePe UAT Testing Documentation
PhonePe Pay API Reference
PhonePe Example Code on GitHub
Live Link
Feel free to explore these resources for more detailed information and updates. Happy coding!