Accept Bitcoin Payments in Next.js — 5 Minute Guide

Accept Bitcoin Payments in Next.js — 5 Minute Guide

# bitcoin# nextjs# crypto# webdev
Accept Bitcoin Payments in Next.js — 5 Minute Guidechovy

Accept Bitcoin Payments in Next.js — 5 Minute Guide You want to accept crypto in your...

Accept Bitcoin Payments in Next.js — 5 Minute Guide

You want to accept crypto in your Next.js app. No third-party custody, no KYC paperwork, no waiting weeks for approvals. Just install a package, add a few routes, and start receiving BTC, ETH, SOL, and more directly to your own wallets.

Here's how with @profullstack/coinpay.


What You Get

  • Non-custodial — your keys never leave your server
  • 6 chains — BTC, ETH, SOL, POL, BCH, USDC
  • HD wallet derivation — unique address per payment
  • Automatic USD conversion rates
  • Webhook notifications on payment confirmation

1. Install

npm install @profullstack/coinpay
Enter fullscreen mode Exit fullscreen mode

2. Create a Payment (API Route)

Create app/api/payments/route.ts:

import { NextRequest, NextResponse } from 'next/server';

const COINPAY_API = 'https://coinpayportal.com/api';
const API_KEY = process.env.COINPAY_API_KEY!;

export async function POST(req: NextRequest) {
  const { amount, currency, metadata } = await req.json();

  const res = await fetch(`${COINPAY_API}/payments`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`,
    },
    body: JSON.stringify({
      amount,          // amount in USD
      currency,        // 'BTC', 'ETH', 'SOL', 'POL', 'BCH', 'USDC'
      metadata,        // your order ID, user info, etc.
      webhook_url: `${process.env.NEXT_PUBLIC_BASE_URL}/api/webhooks/coinpay`,
    }),
  });

  const payment = await res.json();

  return NextResponse.json({
    paymentId: payment.id,
    address: payment.address,
    amount: payment.crypto_amount,
    currency: payment.currency,
    qr: payment.qr_code_url,
    expires_at: payment.expires_at,
  });
}
Enter fullscreen mode Exit fullscreen mode

3. Display the QR Code in React

'use client';

import { useState } from 'react';

interface PaymentData {
  paymentId: string;
  address: string;
  amount: string;
  currency: string;
  qr: string;
  expires_at: string;
}

export default function PaymentButton({ amount }: { amount: number }) {
  const [payment, setPayment] = useState<PaymentData | null>(null);
  const [loading, setLoading] = useState(false);

  async function handlePay(currency: string) {
    setLoading(true);
    const res = await fetch('/api/payments', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ amount, currency, metadata: { orderId: '123' } }),
    });
    const data = await res.json();
    setPayment(data);
    setLoading(false);
  }

  if (payment) {
    return (
      <div style={{ textAlign: 'center', padding: '2rem' }}>
        <h3>Send {payment.amount} {payment.currency}</h3>
        <img src={payment.qr} alt="Payment QR Code" width={256} height={256} />
        <p style={{ fontFamily: 'monospace', fontSize: '0.85rem', wordBreak: 'break-all' }}>
          {payment.address}
        </p>
        <button onClick={() => navigator.clipboard.writeText(payment.address)}>
          Copy Address
        </button>
      </div>
    );
  }

  return (
    <div>
      <p>Pay ${amount}</p>
      <div style={{ display: 'flex', gap: '0.5rem' }}>
        {['BTC', 'ETH', 'SOL', 'USDC'].map((c) => (
          <button key={c} onClick={() => handlePay(c)} disabled={loading}>
            {c}
          </button>
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Handle the Webhook

Create app/api/webhooks/coinpay/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

const WEBHOOK_SECRET = process.env.COINPAY_WEBHOOK_SECRET!;

function verifySignature(payload: string, signature: string): boolean {
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

export async function POST(req: NextRequest) {
  const body = await req.text();
  const signature = req.headers.get('x-coinpay-signature') || '';

  if (!verifySignature(body, signature)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const event = JSON.parse(body);

  switch (event.status) {
    case 'confirmed':
      // Payment confirmed on-chain
      // Update your order, grant access, send receipt
      console.log(`Payment ${event.payment_id} confirmed for ${event.amount} ${event.currency}`);
      break;
    case 'expired':
      // Payment window expired
      break;
  }

  return NextResponse.json({ received: true });
}
Enter fullscreen mode Exit fullscreen mode

5. Environment Variables

COINPAY_API_KEY=your_api_key_here
COINPAY_WEBHOOK_SECRET=your_webhook_secret_here
NEXT_PUBLIC_BASE_URL=https://yourapp.com
Enter fullscreen mode Exit fullscreen mode

That's It

Five minutes, four files. You're now accepting crypto payments with no middleman holding your funds. Every payment goes directly to an address derived from your own HD wallet.

What makes this different from BitPay/Coinbase Commerce: Your keys stay on your server. There's no custodian. No one can freeze your funds or require KYC to release them.


📖 Docs: coinpayportal.com
📦 npm: @profullstack/coinpay