The official paycoinpro npm package makes it simple to add crypto payments to any Node.js or Next.js project. This guide walks you through the complete integration.
What You'll Build
By the end, you'll have:
Invoice creation with automatic finalization
Real-time payment status updates
Secure webhook handling with signature verification
A complete checkout flow
Before You Start
Make sure you have:
Node.js 18 or later
A PayCoinPro account (sign up here )
Your API key from the dashboard
A webhook secret for secure handling
Installation
Choose your package manager:
Configuration
Add your credentials to your environment:
# .env
PAYCOINPRO_API_KEY = "your-api-key"
PAYCOINPRO_WEBHOOK_SECRET = "your-webhook-secret"
Initialize the client:
import PayCoinPro from 'paycoinpro' ;
const paycoinpro = new PayCoinPro ( {
apiKey : process . env . PAYCOINPRO_API_KEY ! ,
} ) ;
Core Features
List Available Cryptocurrencies
Show users which payment options are available:
const assets = await paycoinpro . assets . list ( ) ;
// Returns:
// [
// {
// symbol: "USDT",
// name: "Tether",
// iconUrl: "https://...",
// networks: [
// { code: "tron", name: "Tron (TRC20)" },
// { code: "ethereum", name: "Ethereum (ERC20)" },
// { code: "bsc", name: "BSC (BEP20)" }
// ]
// },
// ...
// ]
Create an Invoice
Create an invoice when a customer is ready to pay:
const invoice = await paycoinpro . invoices . create ( {
amount : 49.99 , // Amount in USD
currency : 'USD' , // Base currency
description : 'Order #12345' ,
expiresIn : 60 , // Minutes until expiry (30, 60, or 120)
metadata : {
orderId : '12345' ,
customerId : 'cust_abc' ,
} ,
} ) ;
console . log ( invoice . id ) ; // "inv_xyz123"
console . log ( invoice . status ) ; // "pending"
Create and Finalize in One Step
If the customer already chose their cryptocurrency, finalize immediately:
const invoice = await paycoinpro . invoices . create ( {
amount : 49.99 ,
currency : 'USD' ,
description : 'Premium subscription' ,
expiresIn : 60 ,
asset : 'USDT' , // Selected crypto
network : 'tron' , // Selected network
} ) ;
// Ready for payment
console . log ( invoice . depositAddress ) ; // "TXyz..."
console . log ( invoice . amountCrypto ) ; // 49.99
Check Invoice Status
Get the current state of any invoice:
const invoice = await paycoinpro . invoices . get ( 'inv_xyz123' ) ;
console . log ( invoice . status ) ; // "paid", "pending", "expired", etc.
console . log ( invoice . amountReceived ) ; // Amount paid so far
console . log ( invoice . txHash ) ; // Blockchain transaction hash
Webhook Integration
Webhooks tell your server when payments arrive. This is the most important part of the integration.
Create the Webhook Endpoint
Here's a complete handler for Next.js App Router:
// app/api/webhook/crypto/route.ts
import { NextRequest , NextResponse } from 'next/server' ;
import crypto from 'crypto' ;
const WEBHOOK_SECRET = process . env . PAYCOINPRO_WEBHOOK_SECRET ! ;
export async function POST ( request : NextRequest ) {
try {
// 1. Get the raw body and signature
const rawBody = await request . text ( ) ;
const signature = request . headers . get ( 'x-webhook-signature' ) ;
if ( ! signature ) {
return NextResponse . json (
{ error : 'Missing signature' } ,
{ status : 401 }
) ;
}
// 2. Verify the signature
const expectedSignature = crypto
. createHmac ( 'sha256' , WEBHOOK_SECRET )
. update ( rawBody )
. digest ( 'hex' ) ;
if ( signature !== expectedSignature ) {
return NextResponse . json (
{ error : 'Invalid signature' } ,
{ status : 401 }
) ;
}
// 3. Parse the payload
const payload = JSON . parse ( rawBody ) ;
const { invoiceId , status , txHash , amount , cryptoSymbol } = payload ;
// 4. Handle each status
switch ( status ) {
case 'paid' :
case 'overpaid' :
await fulfillOrder ( invoiceId , { txHash , amount , cryptoSymbol } ) ;
break ;
case 'underpaid' :
await handlePartialPayment ( invoiceId , amount ) ;
break ;
case 'expired' :
await cancelOrder ( invoiceId ) ;
break ;
}
return NextResponse . json ( { success : true } ) ;
} catch ( error ) {
console . error ( 'Webhook error:' , error ) ;
return NextResponse . json (
{ error : 'Internal server error' } ,
{ status : 500 }
) ;
}
}
// Health check
export async function GET ( ) {
return NextResponse . json ( {
status : 'ok' ,
enabled : ! ! WEBHOOK_SECRET
} ) ;
}
Webhook Payload Structure
When a payment arrives, you receive:
{
"invoiceId" : "inv_xyz123" ,
"depositAddress" : "TXyz..." ,
"amount" : 49.99 ,
"cryptoSymbol" : "USDT" ,
"amountFiat" : 49.99 ,
"txHash" : "abc123def456..." ,
"network" : "tron" ,
"status" : "paid"
}
Handle Duplicate Webhooks
Webhooks may be retried. Always check if you've already processed the payment:
const existingPayment = await db . payment . findUnique ( {
where : { invoiceId }
} ) ;
if ( existingPayment ?. status === 'COMPLETED' ) {
return NextResponse . json ( {
success : true ,
alreadyProcessed : true
} ) ;
}
Real-Time Payment Updates
Show customers live payment status for the best experience.
Option 1: Polling (Simple)
Check the status every few seconds:
// hooks/usePaymentStatus.ts
import { useState , useEffect } from 'react' ;
export function usePaymentStatus ( invoiceId : string ) {
const [ status , setStatus ] = useState ( 'pending' ) ;
const [ loading , setLoading ] = useState ( true ) ;
useEffect ( ( ) => {
const checkStatus = async ( ) => {
try {
const res = await fetch ( ` /api/payment/ ${ invoiceId } /status ` ) ;
const data = await res . json ( ) ;
setStatus ( data . status ) ;
// Stop on final states
if ( [ 'paid' , 'completed' , 'expired' , 'failed' ] . includes ( data . status ) ) {
return false ;
}
return true ;
} catch ( error ) {
console . error ( 'Status check failed:' , error ) ;
return true ;
} finally {
setLoading ( false ) ;
}
} ;
checkStatus ( ) ;
const interval = setInterval ( async ( ) => {
const shouldContinue = await checkStatus ( ) ;
if ( ! shouldContinue ) clearInterval ( interval ) ;
} , 5000 ) ;
return ( ) => clearInterval ( interval ) ;
} , [ invoiceId ] ) ;
return { status , loading } ;
}
Option 2: Server-Sent Events (Recommended)
For instant updates, use SSE:
// app/api/payment/[id]/stream/route.ts
import { NextRequest } from 'next/server' ;
export async function GET (
request : NextRequest ,
{ params } : { params : { id : string } }
) {
const encoder = new TextEncoder ( ) ;
const stream = new ReadableStream ( {
async start ( controller ) {
const payment = await db . payment . findUnique ( {
where : { id : params . id }
} ) ;
controller . enqueue (
encoder . encode ( ` data: ${ JSON . stringify ( payment ) } \n\n ` )
) ;
const unsubscribe = subscribeToPaymentUpdates ( params . id , ( update ) => {
controller . enqueue (
encoder . encode ( ` data: ${ JSON . stringify ( update ) } \n\n ` )
) ;
if ( [ 'COMPLETED' , 'FAILED' , 'EXPIRED' ] . includes ( update . status ) ) {
controller . close ( ) ;
}
} ) ;
request . signal . addEventListener ( 'abort' , ( ) => {
unsubscribe ( ) ;
controller . close ( ) ;
} ) ;
}
} ) ;
return new Response ( stream , {
headers : {
'Content-Type' : 'text/event-stream' ,
'Cache-Control' : 'no-cache' ,
'Connection' : 'keep-alive' ,
} ,
} ) ;
}
Client hook for SSE:
// hooks/usePaymentStream.ts
import { useState , useEffect } from 'react' ;
export function usePaymentStream ( paymentId : string ) {
const [ data , setData ] = useState ( null ) ;
const [ status , setStatus ] = useState < 'connecting' | 'connected' | 'error' > ( 'connecting' ) ;
useEffect ( ( ) => {
const eventSource = new EventSource ( ` /api/payment/ ${ paymentId } /stream ` ) ;
eventSource . onopen = ( ) => setStatus ( 'connected' ) ;
eventSource . onmessage = ( event ) => setData ( JSON . parse ( event . data ) ) ;
eventSource . onerror = ( ) => {
setStatus ( 'error' ) ;
eventSource . close ( ) ;
} ;
return ( ) => eventSource . close ( ) ;
} , [ paymentId ] ) ;
return { data , status } ;
}
Complete Payment Page Example
A full payment page component:
// app/checkout/payment/[id]/page.tsx
'use client' ;
import { useEffect , useState } from 'react' ;
import { QRCodeSVG } from 'qrcode.react' ;
import { usePaymentStream } from '@/hooks/usePaymentStream' ;
export default function PaymentPage ( { params } : { params : { id : string } } ) {
const { data : payment , status } = usePaymentStream ( params . id ) ;
const [ timeLeft , setTimeLeft ] = useState < number | null > ( null ) ;
useEffect ( ( ) => {
if ( ! payment ?. expiresAt ) return ;
const interval = setInterval ( ( ) => {
const remaining = new Date ( payment . expiresAt ) . getTime ( ) - Date . now ( ) ;
setTimeLeft ( Math . max ( 0 , Math . floor ( remaining / 1000 ) ) ) ;
} , 1000 ) ;
return ( ) => clearInterval ( interval ) ;
} , [ payment ?. expiresAt ] ) ;
if ( ! payment ) {
return < div > Loading payment details... </ div > ;
}
if ( payment . status === 'COMPLETED' ) {
return (
< div className = " text-center " >
< h1 > Payment Successful! </ h1 >
< p > Your order has been confirmed. </ p >
</ div >
) ;
}
if ( payment . status === 'EXPIRED' ) {
return (
< div className = " text-center " >
< h1 > Payment Expired </ h1 >
< p > This invoice has expired. Please create a new order. </ p >
< a href = " /checkout " > Try Again </ a >
</ div >
) ;
}
return (
< div className = " max-w-md mx-auto p-6 " >
< h1 className = " text-2xl font-bold mb-4 " > Complete Your Payment </ h1 >
{ timeLeft !== null && (
< div className = " mb-4 text-center " >
< span className = " text-lg " >
Time remaining: { Math . floor ( timeLeft / 60 ) } : { ( timeLeft % 60 ) . toString ( ) . padStart ( 2 , '0' ) }
</ span >
</ div >
) }
< div className = " flex justify-center mb-6 " >
< QRCodeSVG value = { payment . depositAddress } size = { 200 } level = " H " />
</ div >
< div className = " bg-gray-100 p-4 rounded-lg mb-4 " >
< div className = " text-sm text-gray-600 " > Send exactly </ div >
< div className = " text-2xl font-mono font-bold " >
{ payment . amountCrypto } { payment . cryptoSymbol }
</ div >
< div className = " text-sm text-gray-500 " > ≈ $ { payment . amountUsd } USD </ div >
</ div >
< div className = " bg-gray-100 p-4 rounded-lg " >
< div className = " text-sm text-gray-600 " > To this address </ div >
< div className = " font-mono text-sm break-all " > { payment . depositAddress } </ div >
< button
onClick = { ( ) => navigator . clipboard . writeText ( payment . depositAddress ) }
className = " mt-2 text-blue-600 text-sm "
>
Copy Address
</ button >
</ div >
< div className = " mt-4 text-center text-sm text-gray-500 " >
Network: { payment . networkName }
</ div >
</ div >
) ;
}
Database Schema
A recommended Prisma schema for storing payments:
model CryptoPayment {
id String @id @default(cuid())
userId String?
guestEmail String?
invoiceId String @unique
amountUsd Decimal @db.Decimal(12, 2)
amountCrypto Decimal? @db.Decimal(18, 8)
cryptoSymbol String?
network String?
depositAddress String?
txHash String?
paidAmountCrypto Decimal? @db.Decimal(18, 8)
status CryptoPaymentStatus @default(PENDING)
expiresAt DateTime
paidAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum CryptoPaymentStatus {
PENDING
UNDERPAID
PAID
EXPIRED
CANCELLED
COMPLETED
FAILED
}
Best Practices
Security
Always verify webhook signatures — Never process unverified webhooks
Use HTTPS — Encrypt all API communication
Protect your API key — Never expose it in client-side code
Handle duplicates — Process each webhook only once
User Experience
Show live updates — Use SSE or polling for real-time status
Display QR codes — Makes mobile payments easy
Add a countdown — Sets expectations and creates urgency
Support multiple networks — Let users pick their preferred chain
Error Handling
Handle partial payments — Decide your policy for underpayments
Log everything — Payment events are critical for debugging
Build fallbacks — If webhooks fail, poll for status
Alert on failures — Notify your team when fulfillment fails
Troubleshooting
Webhooks Not Arriving
Check your webhook URL in the PayCoinPro dashboard
Look for incoming requests in your server logs
Make sure your endpoint returns a 200 status
Test with the health check endpoint
Invalid Signature Errors
Verify your webhook secret matches the dashboard
Use the raw request body, not parsed JSON
Check for middleware that modifies the request
Payments Stuck on Pending
Check the invoice status in the PayCoinPro dashboard
Verify the blockchain transaction was sent
Confirm the customer used the correct network
Check if the invoice expired
Next Steps
You're ready to accept crypto payments:
Create your PayCoinPro account
Install the package: npm install paycoinpro
Set up your webhook endpoint
Build your payment UI
Test with a small payment
Go live!
Need help? Check the API documentation or email support@paycoinpro.com .