Adding cryptocurrency payments to your application has never been easier. The official paycoinpro npm package provides a type-safe, developer-friendly way to integrate crypto payments into any Node.js or Next.js project. This guide walks you through the complete integration process.
What You'll Build
By the end of this guide, you'll have:
Invoice creation with automatic finalization
Real-time payment status updates
Secure webhook handling with signature verification
A complete checkout flow for your users
Prerequisites
Before starting, make sure you have:
Node.js 18+ installed
A PayCoinPro account (sign up here )
Your API key from the dashboard
A webhook secret for secure webhook handling
Installation
Install the package using your preferred package manager:
Or with yarn:
Or with pnpm:
Configuration
First, set up your environment variables:
# .env
PAYCOINPRO_API_KEY = "your-api-key"
PAYCOINPRO_WEBHOOK_SECRET = "your-webhook-secret"
Then initialize the client in your application:
import PayCoinPro from 'paycoinpro' ;
const paycoinpro = new PayCoinPro ( {
apiKey : process . env . PAYCOINPRO_API_KEY ! ,
} ) ;
Core Features
Fetching Available Cryptocurrencies
Before creating an invoice, you might want to show users which cryptocurrencies they can pay with:
const assets = await paycoinpro . assets . list ( ) ;
// Returns an array of available assets
// [
// {
// symbol: "USDT",
// name: "Tether",
// iconUrl: "https://...",
// networks: [
// { code: "tron", name: "Tron (TRC20)" },
// { code: "ethereum", name: "Ethereum (ERC20)" },
// { code: "bsc", name: "BSC (BEP20)" }
// ]
// },
// ...
// ]
Creating 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"
Auto-Finalizing with Selected Currency
If you already know which cryptocurrency the customer wants to use, you can auto-finalize the invoice:
const invoice = await paycoinpro . invoices . create ( {
amount : 49.99 ,
currency : 'USD' ,
description : 'Premium subscription' ,
expiresIn : 60 ,
asset : 'USDT' , // Customer's selected crypto
network : 'tron' , // Selected network
} ) ;
// Invoice is immediately finalized with deposit address
console . log ( invoice . depositAddress ) ; // "TXyz..."
console . log ( invoice . amountCrypto ) ; // 49.99 (USDT amount)
Retrieving Invoice Details
Fetch the current status of an invoice:
const invoice = await paycoinpro . invoices . get ( 'inv_xyz123' ) ;
console . log ( invoice . status ) ; // "paid", "pending", "expired", etc.
console . log ( invoice . amountReceived ) ; // Amount customer has paid
console . log ( invoice . txHash ) ; // Blockchain transaction hash
Webhook Integration
Webhooks are how PayCoinPro notifies your server when a payment is received. This is the most critical part of the integration.
Setting Up the Webhook Endpoint
Here's a complete webhook 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. Process based on status
switch ( status ) {
case 'paid' :
case 'overpaid' :
// Payment received - fulfill the order
await fulfillOrder ( invoiceId , {
txHash ,
amount ,
cryptoSymbol ,
} ) ;
break ;
case 'underpaid' :
// Partial payment - notify customer
await handlePartialPayment ( invoiceId , amount ) ;
break ;
case 'expired' :
// Invoice expired - cancel order
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 endpoint
export async function GET ( ) {
return NextResponse . json ( {
status : 'ok' ,
enabled : ! ! WEBHOOK_SECRET
} ) ;
}
Webhook Payload Structure
When a payment is received, PayCoinPro sends this payload:
{
"invoiceId" : "inv_xyz123" ,
"depositAddress" : "TXyz..." ,
"amount" : 49.99 ,
"cryptoSymbol" : "USDT" ,
"amountFiat" : 49.99 ,
"txHash" : "abc123def456..." ,
"network" : "tron" ,
"status" : "paid"
}
Idempotency
Always handle webhook idempotency—webhooks may be retried:
// Check if already processed
const existingPayment = await db . payment . findUnique ( {
where : { invoiceId }
} ) ;
if ( existingPayment ?. status === 'COMPLETED' ) {
// Already processed, return success
return NextResponse . json ( {
success : true ,
alreadyProcessed : true
} ) ;
}
Real-Time Payment Updates
For the best user experience, show real-time payment status on your payment page.
Option 1: Polling (Simple)
Poll the invoice 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 polling on terminal states
if ( [ 'paid' , 'completed' , 'expired' , 'failed' ] . includes ( data . status ) ) {
return false ; // Stop polling
}
return true ; // Continue polling
} catch ( error ) {
console . error ( 'Status check failed:' , error ) ;
return true ;
} finally {
setLoading ( false ) ;
}
} ;
// Initial check
checkStatus ( ) ;
// Poll every 5 seconds
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 ) {
// Send initial status
const payment = await db . payment . findUnique ( {
where : { id : params . id }
} ) ;
controller . enqueue (
encoder . encode ( ` data: ${ JSON . stringify ( payment ) } \n\n ` )
) ;
// Subscribe to updates (using Redis pub/sub, etc.)
const unsubscribe = subscribeToPaymentUpdates ( params . id , ( update ) => {
controller . enqueue (
encoder . encode ( ` data: ${ JSON . stringify ( update ) } \n\n ` )
) ;
// Close on terminal state
if ( [ 'COMPLETED' , 'FAILED' , 'EXPIRED' ] . includes ( update . status ) ) {
controller . close ( ) ;
}
} ) ;
// Cleanup on disconnect
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-side 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 ) => {
const update = JSON . parse ( event . data ) ;
setData ( update ) ;
} ;
eventSource . onerror = ( ) => {
setStatus ( 'error' ) ;
eventSource . close ( ) ;
} ;
return ( ) => eventSource . close ( ) ;
} , [ paymentId ] ) ;
return { data , status } ;
}
Complete Payment Page Example
Here's 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 ) ;
// Countdown timer
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 > ;
}
// Success state
if ( payment . status === 'COMPLETED' ) {
return (
< div className = " text-center " >
< h1 > Payment Successful! </ h1 >
< p > Your order has been confirmed. </ p >
{ /* Show order details, download links, etc. */ }
</ div >
) ;
}
// Expired state
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 >
) ;
}
// Waiting for payment
return (
< div className = " max-w-md mx-auto p-6 " >
< h1 className = " text-2xl font-bold mb-4 " > Complete Your Payment </ h1 >
{ /* Timer */ }
{ 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 >
) }
{ /* QR Code */ }
< div className = " flex justify-center mb-6 " >
< QRCodeSVG
value = { payment . depositAddress }
size = { 200 }
level = " H "
/>
</ div >
{ /* Amount */ }
< 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 >
{ /* Address */ }
< 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 >
{ /* Network info */ }
< div className = " mt-4 text-center text-sm text-gray-500 " >
Network: { payment . networkName }
</ div >
</ div >
) ;
}
Database Schema
Here's a recommended Prisma schema for storing payments:
model CryptoPayment {
id String @id @default(cuid())
userId String? // Optional for guest checkout
guestEmail String?
// PayCoinPro invoice
invoiceId String @unique
// Payment details
amountUsd Decimal @db.Decimal(12, 2)
amountCrypto Decimal? @db.Decimal(18, 8)
cryptoSymbol String?
network String?
depositAddress String?
// Transaction details (set when paid)
txHash String?
paidAmountCrypto Decimal? @db.Decimal(18, 8)
// Status
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 - All API communication should be encrypted
Secure your API key - Never expose it in client-side code
Implement idempotency - Handle duplicate webhooks gracefully
User Experience
Show real-time updates - Use SSE or polling for live status
Display QR codes - Makes mobile payments easy
Include countdown timer - Creates urgency and sets expectations
Support multiple networks - Let users choose their preferred network
Error Handling
Handle partial payments - Decide your policy for underpayments
Log everything - Payment events are critical for debugging
Have a fallback - If webhooks fail, implement status polling
Notify on failures - Alert your team when fulfillment fails
Troubleshooting
Webhooks Not Received
Verify webhook URL in PayCoinPro dashboard
Check your server logs for incoming requests
Ensure your endpoint returns 200 status
Test with the health check endpoint
Invalid Signature Errors
Confirm webhook secret matches dashboard setting
Ensure you're using the raw request body, not parsed JSON
Check for any middleware modifying the request
Payments Stuck on Pending
Check PayCoinPro dashboard for invoice status
Verify blockchain transaction was sent
Ensure correct network was used
Check if invoice expired
Next Steps
You now have everything you need to accept crypto payments:
Create your PayCoinPro account
Install the npm package: npm install paycoinpro
Set up your webhook endpoint
Build your payment UI
Test with a small payment
Go live!
Need help? Check out our API documentation or contact support@paycoinpro.com .