All Articles
PayCoinPro
GuidesJanuary 23, 20269 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

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:

bash
npm install paycoinpro

Or with yarn:

bash
yarn add paycoinpro

Or with pnpm:

bash
pnpm add paycoinpro

Configuration

First, set up your environment variables:

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

Then initialize the client in your application:

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

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

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"

Auto-Finalizing with Selected Currency

If you already know which cryptocurrency the customer wants to use, you can auto-finalize the invoice:

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

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

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

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

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

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

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) {
      // 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:

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) => {
      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:

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);

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

prisma
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

  1. Always verify webhook signatures - Never process unverified webhooks
  2. Use HTTPS - All API communication should be encrypted
  3. Secure your API key - Never expose it in client-side code
  4. Implement idempotency - Handle duplicate webhooks gracefully

User Experience

  1. Show real-time updates - Use SSE or polling for live status
  2. Display QR codes - Makes mobile payments easy
  3. Include countdown timer - Creates urgency and sets expectations
  4. Support multiple networks - Let users choose their preferred network

Error Handling

  1. Handle partial payments - Decide your policy for underpayments
  2. Log everything - Payment events are critical for debugging
  3. Have a fallback - If webhooks fail, implement status polling
  4. Notify on failures - Alert your team when fulfillment fails

Troubleshooting

Webhooks Not Received

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

Invalid Signature Errors

  1. Confirm webhook secret matches dashboard setting
  2. Ensure you're using the raw request body, not parsed JSON
  3. Check for any middleware modifying the request

Payments Stuck on Pending

  1. Check PayCoinPro dashboard for invoice status
  2. Verify blockchain transaction was sent
  3. Ensure correct network was used
  4. Check if invoice expired

Next Steps

You now have everything you need to accept crypto payments:

  1. Create your PayCoinPro account
  2. Install the npm 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 out our API documentation or contact 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.