@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.getTokens(params?)
List checkout-ready tokens for a business. Use this instead of hard-coding a coin list. Business wallets take priority; merchant global wallets are used as fallback.
const { tokens } = await client.getTokens({
businessId: 'biz_123',
activeOnly: true,
});
for (const token of tokens) {
console.log(token.code);
}
businessId | string | JWT only | Optional with a business API key; if set, it must match the key scope |
activeOnly | boolean | — | Return active wallets only |
client.getSupportedCoins(params?)
Returns the lower-level /api/supported-coins shape with coins instead of token-picker code fields.
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 tokens list --business-id biz_123 --active-only
coinpay payment qr pay_abc123
Invoices
Invoice creation is draft-only. It does not send the invoice or move funds.
Drafts do not have a public payment link. Publish the draft to create payment
details for manual sharing, or send it to create payment details and email the
client.
When using a business-scoped API key, --business-id may be omitted.
coinpay invoice create \
--amount 250 \
--currency USD \
--client-id cli_001 \
--crypto-currency USDC \
--due-date 2026-09-01 \
--notes "August development"
coinpay invoice create --amount 250 --wallet-id wal_001
coinpay invoice create --amount 250 --merchant-wallet-address 0xabc123
coinpay invoice get inv_abc123
coinpay invoice update inv_abc123 \
--amount 275 \
--due-date 2026-09-15 \
--notes "Updated scope"
coinpay invoice publish inv_abc123
coinpay invoice publish inv_abc123 --yes --json
coinpay invoice send inv_abc123
coinpay invoice send inv_abc123 --yes --json
coinpay invoice delete inv_abc123
coinpay invoice list \
--business-id biz_123 \
--status sent \
--client-id cli_001 \
--date-from 2026-08-01 \
--date-to 2026-08-31
coinpay invoice list --status paid --json
Only draft invoices can be updated or deleted. Draft and overdue invoices can
be sent; sending an overdue invoice issues new payment details and emails the
client again. Only drafts can be published. --yes skips the confirmation for
publish/send/delete, and is required when any of those commands runs without a
terminal or with --json.
--wallet-id updates the invoice's stored wallet reference. To change the
actual settlement destination, use --merchant-wallet-address (or change the
crypto currency and let the server resolve the configured payee).
The CLI does not post invoice links to /chat automatically. Copy the printed
share link into chat manually. Treat COINPAY_API_KEY and ~/.coinpay.json as
sensitive: either credential can authorize invoice updates, sends, and deletes.
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
Finances — your money in one place
coinpay finances
coinpay finances --days 90
coinpay finances summary
coinpay finances position
coinpay finances scope <id> business
coinpay finances accounts
coinpay finances ledger --search anthropic --limit 20
coinpay finances connections
coinpay finances sync
The dashboard has seven screens (1–7, Tab): Overview (gross volume, crypto vs
cards, commission paid, processor fees, refunds, net earnings, bank position, cashflow,
invoices, escrow, payouts, a volume-vs-commission graph and a live feed), Bank &
Cards, Ledger, Crypto, Cards, Invoices & Escrow, Debt & Income.
r refreshes, s
syncs the bank feed, w cycles the window, p pauses, ? shows help, q quits. It
refreshes every 30 seconds (--interval) and listens to the payments event stream, so
a crypto payment shows up the moment it is detected.
Debt & Income is the basic-accounting view, and it reads the same for a company or
a person: income against spending per month, total owed split into revolving and
instalment, months to clear each balance at its current payment rate, debt-to-income,
debt-service ratio, months of cover, card utilisation, the recurring bills it found with
their next due date, and a business-versus-personal split of all of it.
The business/personal split starts as a guess from the account name, which gets
"Business Checking (4672)" right and cannot see a personal card carrying company
spend. Correct it with coinpay finances scope <account-id> business|personal,
or clear to hand it back to the guess. The correction is stored separately from
the derived value, so a re-sync cannot clobber it, and corrected accounts carry a
trailing * in the Side column.
Two things about those numbers. Transfers and card payments are excluded from both
income and spending — a feed holding both a checking account and the card it pays
contains every card payment twice, so counting raw credits as income inflates both
sides by the whole card-payment volume. The untouched totals stay on screen as gross
credits and gross debits, so the netting is auditable. And the figures come from about
six months of history rather than the dashboard window (w does not move them), because
a monthly charge cannot be seen in thirty days of rows; the per-month averages divide by
the history that actually exists, which for a recently linked feed is much less than six
months.
It is built on @profullstack/hqtui and needs Node 22.6+; the
plain-text subcommands work on Node 20. Bank data needs the merchant session from
coinpay login (business API keys are refused on purpose).
From the SDK:
import { CoinPayClient } from '@profullstack/coinpay';
import { collectFinanceSnapshot } from '@profullstack/coinpay/finances';
const client = new CoinPayClient({ apiKey: sessionToken });
const snapshot = await collectFinanceSnapshot(client, { days: 30 });
console.log(snapshot.earnings.netUsd, snapshot.bank.liabilities, snapshot.invoices.totals.overdue);
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
TaskMarket delegation (x402 v2)
@profullstack/coinpay/taskmarket lets an agent or user create and fund a
TaskMarket task from inside a CoinPay-powered app,
using the standard x402 v2 transfer-authorization flow this SDK already speaks.
import { createTask, discoverTasks, getTask, listSubmissions } from '@profullstack/coinpay/taskmarket';
const open = await discoverTasks({ status: 'open', limit: 25, mode: 'bounty' });
const { taskId } = await createTask(
{ title: 'Fix my parser bug', description: 'Repro + expected output attached in the linked issue.', reward: 2500000 },
{
signer: myWalletSigner,
capabilities: ['eip155:8453'],
spendingLimitUsd: 10,
authorize: async (d) => confirm(`Send ${d.amount} ${d.asset} to ${d.payTo}?`),
}
);
const live = await getTask(taskId);
const submissions = await listSubmissions(taskId);
Safety: no keys ever enter the module; every payment needs fresh authorize()
consent; spendingLimitUsd is enforced against the quoted amount in base
units; after a payment with unknown settlement the caller is told to verify
with getTask instead of retrying blindly.
License
MIT © Profullstack, Inc.