Next.js Starter Template
Complete Next.js app with auth, pricing page, and ChainPay payments pre-wired. Clone and run in 5 minutes.
Quick Start
Accept crypto payments in three steps — no blockchain knowledge required.
Create a payment order
const response = await fetch("https://chainpay.pro/api/v1/orders", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
amount: 29.99,
currency: "USDT",
chain: "trc20",
externalId: "order_abc123", // your internal order ID
metadata: { userId: "user_456" } // arbitrary JSON, returned in webhook
})
});
const order = await response.json();
// order.checkoutUrl → redirect user here
// order.payAddress → or show the address directlyRedirect the user to checkout
// Server-side redirect
res.redirect(order.checkoutUrl);
// Or client-side
window.location.href = order.checkoutUrl;Receive a webhook when payment is confirmed
{
"event": "payment.completed",
"orderId": "ord_a1b2c3d4e5f6",
"externalId": "order_abc123",
"amount": "29.99",
"netAmount": "29.75", // after 0.8% fee
"currency": "USDT",
"chain": "trc20",
"txHash": "e3b0c44298fc1c149afb...",
"completedAt": "2026-03-22T08:15:00Z",
"metadata": { "userId": "user_456" }
}Verify the X-ChainPay-Signature header before processing. See the section for verification code.
Authentication
All API requests must include your API key as a Bearer token.
Authorization: Bearer sk_live_YOUR_API_KEYcurl -X POST https://chainpay.pro/api/v1/orders \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 10, "currency": "USDT", "chain": "trc20"}'| Key prefix | Environment | Blockchain |
|---|---|---|
| sk_live_… | Production | Real transactions |
| sk_test_… | Sandbox | No real funds moved |
Create Order
https://chainpay.pro/api/v1/ordersRequest parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | number | required | Payment amount in USD (e.g. 29.99). The equivalent crypto amount is calculated at current market rates. |
| currency | string | required | Cryptocurrency to receive. One of: "USDT", "ETH", "BTC", "SOL". |
| chain | string | required | Blockchain network. One of: "trc20", "erc20", "btc", "sol". Must be compatible with the chosen currency. |
| externalId | string | optional | Your internal order identifier. Returned unchanged in webhooks. Must be unique per merchant. |
| metadata | object | optional | Arbitrary JSON object (max 2 KB). Returned in all webhooks for this order — useful for passing user IDs, cart references, etc. |
Supported currency + chain combinations
| currency | chain | Network | Avg confirmation time |
|---|---|---|---|
| USDT | trc20 | TRON (TRC-20) | ~1 min |
| USDT | erc20 | Ethereum (ERC-20) | ~2 min |
| ETH | erc20 | Ethereum | ~2 min |
| BTC | btc | Bitcoin | ~30 min |
| SOL | sol | Solana | <5 sec |
Response
{
"orderId": "ord_a1b2c3d4e5f6g7h8",
"payAddress": "TXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"cryptoAmount": "29.99",
"currency": "USDT",
"chain": "trc20",
"checkoutUrl": "https://chainpay.pro/pay/ord_a1b2c3d4",
"expiresAt": "2026-03-22T09:00:00Z",
"createdAt": "2026-03-22T08:30:00Z"
}Checkout Flow
How the hosted payment page works.
- 1Redirect — Your server redirects the user to the checkoutUrl returned when creating the order.
- 2Payment page — The hosted page displays a QR code and wallet address pre-populated with the exact crypto amount. The user scans the QR or copies the address into their wallet app.
- 3Confirmation — ChainPay monitors the blockchain. Once the required number of confirmations is reached, the order status changes to confirming, then completed.
- 4Redirect back — The payment page shows a success screen. If you have a successUrl configured in the dashboard, the user is automatically redirected there.
- 5Webhook — Your server receives a payment.completed webhook. Fulfil the order only after verifying the webhook signature.
expired and a payment.expired webhook is sent. Create a new order to retry.Webhooks
ChainPay sends signed HTTP POST requests to your webhook URL when order status changes.
Setup
Go to Dashboard → Apps → New App
Fill in your Webhook URL (e.g. https://yourapp.com/api/webhooks/chainpay)
Copy the generated Webhook Secret — use it to verify signatures
Webhook Events
Currently supported events:
| Event | When triggered |
|---|---|
| payment.completed | Order paid and confirmed on-chain |
| payment.expired | Order expired without payment |
Payload Format
{
"event": "payment.completed",
"orderId": "ord_xxxxxxxx",
"amount": "9.99",
"currency": "USDT",
"chain": "trc20",
"txHash": "abc123...",
"netAmount": "9.92",
"completedAt": "2026-01-01T00:00:00.000Z",
"metadata": {
"userId": "your-user-id",
"plan": "monthly"
}
}Signature Verification
Every webhook request includes a X-ChainPay-Signature header. Verify it using HMAC-SHA256 with your App's Webhook Secret:
import crypto from 'crypto'
function verifyWebhook(rawBody: string, signature: string, secret: string): boolean {
const computed = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(signature)
)
}
// In your webhook handler:
export async function POST(req: Request) {
const rawBody = await req.text()
const signature = req.headers.get('x-chainpay-signature') ?? ''
if (!verifyWebhook(rawBody, signature, process.env.CHAINPAY_WEBHOOK_SECRET!)) {
return new Response('Unauthorized', { status: 401 })
}
const event = JSON.parse(rawBody)
if (event.event === 'payment.completed') {
const { userId, plan } = event.metadata
// activate user's plan
}
return new Response('OK')
}import hmac, hashlib
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
computed = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, signature)Test your webhook
Go to Dashboard → Apps → select your app
Click "Send Test Event"
Check your server received the request (HTTP 200 expected)
Retry Policy
Failed deliveries are retried with exponential backoff: 1 min → 5 min → 30 min → 2 hours → 24 hours (5 attempts total).
After 5 failed attempts, the webhook is marked as failed. Check Dashboard → Alerts for failed deliveries.
Best Practices
- Always verify the signature before processing
- Return HTTP 200 quickly — do heavy processing asynchronously
- Use the
orderIdto deduplicate events (webhooks may be delivered more than once) - Store the raw body before parsing to ensure accurate signature verification
Query Order
https://chainpay.pro/api/v1/orders/:id{
"id": "ord_a1b2c3d4e5f6g7h8",
"amount": "29.99",
"cryptoAmount":"29.99",
"netAmount": "29.75",
"currency": "USDT",
"chain": "trc20",
"status": "completed",
"payAddress": "TXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"txHash": "e3b0c44298fc1c149afbf4c8996fb92...",
"externalId": "order_abc123",
"metadata": { "userId": "user_456" },
"createdAt": "2026-03-22T08:30:00Z",
"expiresAt": "2026-03-22T09:00:00Z",
"completedAt": "2026-03-22T08:45:00Z"
}Order status values
| Status | Description |
|---|---|
| pending | Order created, waiting for the user to send payment. |
| confirming | Transaction detected on-chain, waiting for enough block confirmations. |
| completed | Payment fully confirmed. Safe to fulfil the order. |
| expired | 30 minutes elapsed without a detected payment. |
| failed | An unexpected error occurred. Contact support with the orderId. |
Rates API
Fetch current USD exchange rates. No API key required.
https://chainpay.pro/api/v1/rates{
"USDT": 1.0,
"ETH": 3450.00,
"BTC": 87500.00,
"SOL": 145.00,
"updatedAt": "2026-03-22T08:00:00Z"
}Rates are updated every 60 seconds from aggregated market data. The amount you pass to the Create Order endpoint is always interpreted in USD; the API converts to crypto at the rate current at order creation time.
Sandbox Mode
Test your full integration without moving real funds.
Register with ?mode=test to receive a sk_test_ API key. All standard endpoints work identically in sandbox mode — blockchain polling is skipped and you trigger confirmation manually.
Step 1 — Get a sandbox API key
curl -X POST "https://chainpay.pro/api/auth/register?mode=test" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "yourpassword"}'
# Response includes:
# { "merchant": { "apiKey": "sk_test_xxxxxxxxxxxxxxxx" } }Step 2 — Create an order with your test key
curl -X POST https://chainpay.pro/api/v1/orders \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 10, "currency": "USDT", "chain": "trc20", "externalId": "test_order_1"}'Step 3 — Simulate a payment
curl -X POST https://chainpay.pro/api/v1/sandbox/simulate-payment \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"orderId": "ord_abc123"}'
# Response:
{
"success": true,
"orderId": "ord_abc123",
"txHash": "sandbox_ord_abc123_1711000000000",
"netAmount": "9.92000000"
}
# The order status is now "completed" and your webhook is fired.sandbox_. The simulate-payment endpoint is only available with sk_test_ keys. Attempting it with a live key returns 403.Error Codes
All error responses use a consistent JSON envelope:
{
"error": "INVALID_CHAIN",
"message": "chain 'foo' is not supported for currency USDT",
"status": 400
}| HTTP status | Common error codes | Cause & resolution |
|---|---|---|
| 400 | INVALID_AMOUNT, INVALID_CURRENCY, INVALID_CHAIN, MISSING_FIELD | Request body is malformed or missing a required field. Check the request parameters table above. |
| 401 | UNAUTHORIZED, INVALID_API_KEY | Missing or invalid Authorization header. Ensure you are sending Bearer <key>. |
| 403 | FORBIDDEN, SANDBOX_ONLY | API key does not have permission for this action (e.g. using simulate-payment with a live key). |
| 404 | ORDER_NOT_FOUND | No order exists with the given ID, or it does not belong to your merchant account. |
| 429 | RATE_LIMITED | Too many requests. Default limit is 60 req/min per API key. Back off and retry after the Retry-After header value. |
| 500 | INTERNAL_ERROR | Unexpected server-side error. Retry with exponential backoff. If persistent, contact support with the request ID. |
Settlement
How and when ChainPay sends collected funds to your wallet.
Settlement sends all completed, unsettled amounts to the wallet addresses you configure in the dashboard under Settings → Settlement Wallets. You can configure separate payout addresses per currency.
netAmount in webhooks already reflects the post-fee amount. Settlement transfers the sum of all netAmount values for the day.Ready to integrate?
Create your account and get your API key in seconds.