@profullstack/coinpay
CoinPay SDK & CLI — Accept cryptocurrency payments in your Node.js application.

Non-custodial, multi-chain payment processing for Bitcoin, Ethereum, Solana, Polygon, Bitcoin Cash, and USDC (on ETH, POL, SOL).
Table of Contents
How It Works
┌──────────┐ 1. Create payment ┌──────────┐
│ Your │ ───────────────────────> │ CoinPay │
│ Server │ <─────────────────────── │ API │
│ │ Address + QR code │ │
└────┬─────┘ └────┬─────┘
│ │
│ 2. Show address/QR │ 4. Webhook notification
│ to customer │ (payment confirmed)
▼ │
┌──────────┐ 3. Sends crypto ┌────▼─────┐
│ Customer │ ───────────────────────> │Blockchain│
│ │ │ Network │
└──────────┘ └──────────┘
- Your server calls the CoinPay API to create a payment request
- CoinPay generates a unique payment address and QR code — display these to your customer
- Customer sends cryptocurrency to the address
- CoinPay monitors the blockchain and notifies you via webhook when payment is confirmed
- Funds are automatically forwarded to your configured wallet
Installation
pnpm add @profullstack/coinpay
npm install @profullstack/coinpay
pnpm add -g @profullstack/coinpay
Requirements: Node.js ≥ 20. Zero runtime dependencies — uses built-in fetch and crypto.
Quick Start
1. Get Your API Key
- Sign up at coinpayportal.com
- Create a business in your dashboard
- Configure wallet addresses for each crypto you want to accept
- Copy your API key (starts with
cp_live_)
2. Create a Payment
import { CoinPayClient, Blockchain } from '@profullstack/coinpay';
const client = new CoinPayClient({
apiKey: 'cp_live_your_api_key_here',
});
const { payment } = await client.createPayment({
businessId: 'your-business-id',
amount: 99.99,
currency: 'USD',
blockchain: Blockchain.BTC,
description: 'Order #12345',
metadata: { orderId: '12345' },
});
console.log('Send payment to:', payment.payment_address);
console.log('Amount:', payment.crypto_amount, 'BTC');
console.log('QR Code:', payment.qr_code);
3. Handle Payment Confirmation
import { createWebhookHandler, WebhookEvent } from '@profullstack/coinpay';
app.post('/webhook', createWebhookHandler({
secret: 'your-webhook-secret',
onEvent: async (event) => {
if (event.type === WebhookEvent.PAYMENT_COMPLETED) {
const orderId = event.data.payment.metadata.orderId;
await markOrderAsPaid(orderId);
}
},
}));
Supported Blockchains
| Bitcoin | BTC | Native |
| Bitcoin Cash | BCH | Native |
| Ethereum | ETH | Native |
| Polygon | POL | Native |
| Solana | SOL | Native |
| USDC (Ethereum) | USDC_ETH | Stablecoin |
| USDC (Polygon) | USDC_POL | Stablecoin |
| USDC (Solana) | USDC_SOL | Stablecoin |
Use the Blockchain constant to avoid typos:
import { Blockchain } from '@profullstack/coinpay';
Blockchain.BTC
Blockchain.ETH
Blockchain.USDC_POL
API Reference
CoinPayClient
The main class for all API operations.
import { CoinPayClient } from '@profullstack/coinpay';
const client = new CoinPayClient({
apiKey: 'cp_live_xxxxx',
baseUrl: 'https://coinpayportal.com/api',
timeout: 30000,
});
apiKey | string | — | Required. Your CoinPay API key |
baseUrl | string | https://coinpayportal.com/api | API base URL |
timeout | number | 30000 | Request timeout in milliseconds |
Throws Error if apiKey is missing or empty.
Payments
client.createPayment(params)
Create a new payment request. Generates a unique blockchain address for the customer to pay.
const { payment, usage } = await client.createPayment({
businessId: 'biz_123',
amount: 100.00,
currency: 'USD',
blockchain: 'ETH',
description: 'Order #123',
metadata: {
orderId: '123',
customerEmail: 'a@b.com',
},
});
Parameters:
businessId | string | ✅ | Business ID from your dashboard |
amount | number | ✅ | Amount in fiat currency |
currency | string | — | Fiat currency code (default: 'USD'). Supports: USD, EUR, GBP, CAD, AUD |
blockchain | string | ✅ | Blockchain code (e.g., 'BTC', 'ETH', 'USDC_POL') |
description | string | — | Payment description visible to the customer |
metadata | object | — | Arbitrary key-value data attached to the payment |
Returns:
{
success: true,
payment: {
id: 'pay_abc123',
business_id: 'biz_123',
amount: 100,
currency: 'USD',
blockchain: 'ETH',
crypto_amount: '0.0456',
payment_address: '0x1234...5678',
qr_code: 'data:image/png;base64,...',
status: 'pending',
expires_at: '2024-01-01T01:00:00.000Z',
created_at: '2024-01-01T00:00:00.000Z',
metadata: { orderId: '123' }
},
usage: {
current: 45,
limit: 100,
remaining: 55
}
}
client.getPayment(paymentId)
Retrieve a payment by its ID.
const { payment } = await client.getPayment('pay_abc123');
console.log(payment.status);
console.log(payment.crypto_amount);
console.log(payment.tx_hash);
client.listPayments(params)
List payments for a business with optional filtering and pagination.
const { payments } = await client.listPayments({
businessId: 'biz_123',
status: 'completed',
limit: 20,
offset: 0,
});
Payment Status Polling
client.waitForPayment(paymentId, options?)
Polls getPayment() until the payment reaches a terminal status. Useful for simple integrations that don't use webhooks.
const { payment } = await client.waitForPayment('pay_abc123', {
interval: 5000,
timeout: 600000,
targetStatuses: ['confirmed', 'forwarded', 'expired', 'failed'],
onStatusChange: (status, payment) => {
console.log(`Status → ${status}`);
},
});
if (payment.status === 'confirmed' || payment.status === 'forwarded') {
console.log('Payment successful!');
}
interval | number | 5000 | Polling interval in ms |
timeout | number | 3600000 | Max wait time in ms |
targetStatuses | string[] | ['confirmed','forwarded','expired','failed'] | Statuses that stop polling |
onStatusChange | function | — | Callback (status, payment) => void |
⚠️ For production, use webhooks instead of polling.
Payment Statuses
pending | Waiting for customer to send payment |
detected | Payment detected on blockchain, awaiting confirmations |
confirmed | Payment confirmed — safe to fulfill the order |
forwarding | Forwarding funds to your wallet |
forwarded | Funds successfully sent to your wallet |
expired | Payment request expired (customer didn't pay in time) |
failed | Payment failed |
QR Codes
client.getPaymentQRUrl(paymentId)
Returns the URL to the QR code image. Synchronous — no network request.
const url = client.getPaymentQRUrl('pay_abc123');
client.getPaymentQR(paymentId)
Fetches the QR code as binary PNG data.
import fs from 'fs';
const imageData = await client.getPaymentQR('pay_abc123');
fs.writeFileSync('payment-qr.png', Buffer.from(imageData));
Exchange Rates
client.getExchangeRate(crypto, fiat?)
Get the exchange rate for a single cryptocurrency.
const rate = await client.getExchangeRate('BTC', 'USD');
client.getExchangeRates(cryptos, fiat?)
Get rates for multiple cryptocurrencies in one request.
const rates = await client.getExchangeRates(['BTC', 'ETH', 'SOL'], 'USD');
Business Management
client.createBusiness(params)
const result = await client.createBusiness({
name: 'My Store',
webhookUrl: 'https://mystore.com/webhook',
walletAddresses: {
BTC: 'bc1q...',
ETH: '0x...',
SOL: '...',
},
});
client.getBusiness(businessId)
const result = await client.getBusiness('biz_123');
client.listBusinesses()
const result = await client.listBusinesses();
client.updateBusiness(businessId, params)
const result = await client.updateBusiness('biz_123', {
name: 'Updated Store Name',
webhookUrl: 'https://mystore.com/webhook/v2',
});
Webhooks
client.getWebhookLogs(businessId, limit?)
Retrieve recent webhook delivery logs.
const logs = await client.getWebhookLogs('biz_123', 50);
client.testWebhook(businessId, eventType?)
Send a test webhook event to your configured endpoint.
await client.testWebhook('biz_123', 'payment.completed');
Standalone Functions
Convenience functions that auto-create a client. Best for one-off operations.
import { createPayment, getPayment, listPayments } from '@profullstack/coinpay';
const result = await createPayment({
apiKey: 'cp_live_xxxxx',
businessId: 'biz_123',
amount: 50,
blockchain: 'BTC',
});
const result2 = await createPayment({
client: existingClient,
businessId: 'biz_123',
amount: 50,
blockchain: 'BTC',
});
const payment = await getPayment({
apiKey: 'cp_live_xxxxx',
paymentId: 'pay_abc123',
});
const list = await listPayments({
apiKey: 'cp_live_xxxxx',
businessId: 'biz_123',
status: 'completed',
limit: 10,
});
Constants
import {
Blockchain,
PaymentStatus,
FiatCurrency,
WebhookEvent,
} from '@profullstack/coinpay';
Blockchain
BTC | 'BTC' | Bitcoin |
BCH | 'BCH' | Bitcoin Cash |
ETH | 'ETH' | Ethereum |
POL | 'POL' | Polygon |
SOL | 'SOL' | Solana |
USDC_ETH | 'USDC_ETH' | USDC on Ethereum |
USDC_POL | 'USDC_POL' | USDC on Polygon |
USDC_SOL | 'USDC_SOL' | USDC on Solana |
Cryptocurrency is exported as a deprecated alias for Blockchain.
PaymentStatus
PENDING | 'pending' |
CONFIRMING | 'confirming' |
COMPLETED | 'completed' |
EXPIRED | 'expired' |
FAILED | 'failed' |
REFUNDED | 'refunded' |
FiatCurrency
USD | 'USD' |
EUR | 'EUR' |
GBP | 'GBP' |
CAD | 'CAD' |
AUD | 'AUD' |
WebhookEvent
PAYMENT_CREATED | 'payment.created' |
PAYMENT_PENDING | 'payment.pending' |
PAYMENT_CONFIRMING | 'payment.confirming' |
PAYMENT_COMPLETED | 'payment.completed' |
PAYMENT_EXPIRED | 'payment.expired' |
PAYMENT_FAILED | 'payment.failed' |
PAYMENT_REFUNDED | 'payment.refunded' |
BUSINESS_CREATED | 'business.created' |
BUSINESS_UPDATED | 'business.updated' |
CLI Reference
Installation
pnpm add -g @profullstack/coinpay
npx @profullstack/coinpay --help
Configuration
coinpay config set-key cp_live_xxxxx
coinpay config set-url http://localhost:3000/api
coinpay config show
Payments
coinpay payment create \
--business-id biz_123 \
--amount 100 \
--blockchain BTC \
--description "Order #12345"
coinpay payment get pay_abc123
coinpay payment list --business-id biz_123 --status pending --limit 10
coinpay payment qr pay_abc123
Businesses
coinpay business list
coinpay business get biz_123
coinpay business create --name "My Store" --webhook-url https://mysite.com/webhook
coinpay business update biz_123 --name "New Name"
Exchange Rates
coinpay rates get BTC
coinpay rates list
Webhooks
coinpay webhook logs biz_123
coinpay webhook test biz_123 --event payment.completed
Webhook Integration
Webhook Payload
When a payment status changes, CoinPay sends a POST request to your webhook URL:
{
"id": "evt_abc123",
"type": "payment.completed",
"created_at": "2024-01-01T00:15:00.000Z",
"business_id": "biz_123",
"data": {
"payment": {
"id": "pay_abc123",
"status": "confirmed",
"amount": 100.00,
"currency": "USD",
"crypto_amount": "0.0456",
"blockchain": "ETH",
"tx_hash": "0xabc...def",
"metadata": { "orderId": "12345" }
}
}
}
Signature Verification
Every webhook includes an X-CoinPay-Signature header in the format t=<timestamp>,v1=<hmac-sha256>. Always verify signatures before processing events.
Using the Middleware (Express)
import express from 'express';
import { createWebhookHandler, WebhookEvent } from '@profullstack/coinpay';
const app = express();
app.post('/webhook',
express.text({ type: 'application/json' }),
createWebhookHandler({
secret: process.env.COINPAY_WEBHOOK_SECRET,
onEvent: async (event) => {
switch (event.type) {
case WebhookEvent.PAYMENT_COMPLETED:
await fulfillOrder(event.data.payment.metadata.orderId);
break;
case WebhookEvent.PAYMENT_EXPIRED:
await cancelOrder(event.data.payment.metadata.orderId);
break;
}
},
onError: (error) => {
console.error('Webhook error:', error);
},
})
);
Manual Verification
import { verifyWebhookSignature, parseWebhookPayload } from '@profullstack/coinpay';
const isValid = verifyWebhookSignature({
payload: rawBody,
signature: req.headers['x-coinpay-signature'],
secret: process.env.COINPAY_WEBHOOK_SECRET,
tolerance: 300,
});
if (isValid) {
const event = parseWebhookPayload(rawBody);
}
Generating Test Signatures
import { generateWebhookSignature } from '@profullstack/coinpay';
const signature = generateWebhookSignature({
payload: JSON.stringify(testEvent),
secret: 'whsec_test_secret',
timestamp: Math.floor(Date.now() / 1000),
});
Webhook Events
payment.created | Payment request created |
payment.pending | Awaiting blockchain detection |
payment.confirming | Transaction detected, awaiting confirmations |
payment.completed | Payment confirmed — safe to fulfill |
payment.expired | Customer didn't pay in time |
payment.failed | Payment failed |
payment.refunded | Payment was refunded |
business.created | New business created |
business.updated | Business settings updated |
Error Handling
All API errors include a status code and optional response object:
try {
const payment = await client.createPayment({ ... });
} catch (error) {
console.log(error.message);
console.log(error.status);
console.log(error.response);
switch (error.status) {
case 400:
break;
case 401:
break;
case 404:
break;
case 429:
console.log('Usage:', error.response?.usage);
break;
}
}
Timeout errors throw a standard Error with message "Request timeout after {ms}ms".
Constructor errors throw if apiKey is missing: "API key is required".
Integration Patterns
E-commerce Checkout
import { CoinPayClient, createWebhookHandler, WebhookEvent } from '@profullstack/coinpay';
const client = new CoinPayClient({ apiKey: process.env.COINPAY_API_KEY });
app.post('/checkout', async (req, res) => {
const { orderId, amount, blockchain } = req.body;
const { payment } = await client.createPayment({
businessId: process.env.COINPAY_BUSINESS_ID,
amount,
blockchain,
description: `Order #${orderId}`,
metadata: { orderId },
});
await db.orders.update(orderId, {
paymentId: payment.id,
paymentAddress: payment.payment_address,
});
res.json({
paymentAddress: payment.payment_address,
cryptoAmount: payment.crypto_amount,
qrCode: payment.qr_code,
expiresAt: payment.expires_at,
});
});
app.post('/webhook', express.text({ type: 'application/json' }),
createWebhookHandler({
secret: process.env.COINPAY_WEBHOOK_SECRET,
onEvent: async (event) => {
if (event.type === WebhookEvent.PAYMENT_COMPLETED) {
const { orderId } = event.data.payment.metadata;
await db.orders.update(orderId, { status: 'paid' });
await sendConfirmationEmail(orderId);
}
},
})
);
Stablecoin Subscriptions
Use USDC for predictable pricing — no volatility:
const { payment } = await client.createPayment({
businessId: BUSINESS_ID,
amount: 9.99,
blockchain: Blockchain.USDC_POL,
description: 'Monthly subscription',
metadata: { userId: user.id, period: '2024-01' },
});
Direct API (fetch / cURL)
curl -X POST https://coinpayportal.com/api/payments/create \
-H "Authorization: Bearer cp_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"business_id": "your-business-id",
"amount": 50.00,
"currency": "USD",
"blockchain": "ETH"
}'
curl https://coinpayportal.com/api/payments/pay_abc123 \
-H "Authorization: Bearer cp_live_your_api_key"
TypeScript
Full TypeScript support via .d.ts declaration files — no build step required.
import {
CoinPayClient,
Blockchain,
PaymentStatus,
WebhookEvent,
} from '@profullstack/coinpay';
import type {
CoinPayClientOptions,
PaymentParams,
Payment,
CreatePaymentResponse,
WaitForPaymentOptions,
VerifyWebhookParams,
ParsedWebhookEvent,
} from '@profullstack/coinpay';
const client = new CoinPayClient({ apiKey: 'cp_live_xxxxx' });
const { payment }: CreatePaymentResponse = await client.createPayment({
businessId: 'biz_123',
amount: 100,
blockchain: Blockchain.ETH,
});
Subpath Imports
import { Blockchain, PaymentStatus } from '@profullstack/coinpay/payments';
import { verifyWebhookSignature, WebhookEvent } from '@profullstack/coinpay/webhooks';
Environment Variables
COINPAY_API_KEY | API key (overrides config file in CLI) |
COINPAY_BASE_URL | Custom API URL (for development) |
Examples
See the examples/ directory for runnable code:
COINPAY_API_KEY=cp_live_xxx COINPAY_BUSINESS_ID=biz_xxx node examples/01-quick-start.js
Testing
pnpm test
pnpm test:watch
Tests use Vitest with mocked fetch — no API key needed.
Support
License
MIT © Profullstack, Inc.