GuidesJanuary 23, 20268 min read

How to Integrate Crypto Payments with the PayCoinPro npm Package

A developer's guide to adding cryptocurrency payments to your Node.js or Next.js application using the official PayCoinPro npm package. Learn to create invoices, handle webhooks, and build real-time payment experiences.

PayCoinPro Team

Merchant StrategistPayCoinPro Team

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:

bash
npm install paycoinpro
bash
yarn add paycoinpro
bash
pnpm add paycoinpro

Configuration

Add your credentials to your environment:

bash
# .env
PAYCOINPRO_API_KEY="your-api-key"
PAYCOINPRO_WEBHOOK_SECRET="your-webhook-secret"

Initialize the client:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
// 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:

json
{
  "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:

typescript
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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

tsx
// 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:

prisma
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

  1. Check your webhook URL in the PayCoinPro dashboard
  2. Look for incoming requests in your server logs
  3. Make sure your endpoint returns a 200 status
  4. Test with the health check endpoint

Invalid Signature Errors

  1. Verify your webhook secret matches the dashboard
  2. Use the raw request body, not parsed JSON
  3. Check for middleware that modifies the request

Payments Stuck on Pending

  1. Check the invoice status in the PayCoinPro dashboard
  2. Verify the blockchain transaction was sent
  3. Confirm the customer used the correct network
  4. Check if the invoice expired

Next Steps

You're ready to accept crypto payments:

  1. Create your PayCoinPro account
  2. Install the package: npm install paycoinpro
  3. Set up your webhook endpoint
  4. Build your payment UI
  5. Test with a small payment
  6. Go live!

Need help? Check the API documentation or email support@paycoinpro.com.

Expand your knowledge

View Journal

Power your business with crypto.

Thousands of forward-thinking merchants use PayCoinPro to settle international payments in seconds, not days.