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
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,
});
}
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>
);
}
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 });
}
5. Environment Variables
COINPAY_API_KEY=your_api_key_here
COINPAY_WEBHOOK_SECRET=your_webhook_secret_here
NEXT_PUBLIC_BASE_URL=https://yourapp.com
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
Top comments (0)