Trystpilot - CCPayment Integration & Automation Guide
Trystpilot — CCPayment Automation & Integration
Section titled “Trystpilot — CCPayment Automation & Integration”Version: 1.1.0 · Last updated: 2026-03-07 Status: ✅ Implemented — webhook live, 117 tests passing, self-tests in /api/status Processor: CCPayment.io — Crypto + Fiat payments
Overview
Section titled “Overview”This guide covers automating CCPayment merchant settings, webhook configuration, and payment tracking for Trystpilot’s text review monetization feature.
Why CCPayment?
- ✅ Low KYC for anonymous payments
- ✅ 900+ cryptocurrency support
- ✅ Fiat card checkout available
- ✅ No separate Stripe account required
- ✅ Webhook-based settlement notifications
- ✅ Aligned with anonymous ethos (no email trail)
1. CCPayment Merchant Account Setup
Section titled “1. CCPayment Merchant Account Setup”1.1 Manual Account Creation
Section titled “1.1 Manual Account Creation”- Visit ccpayment.com
- Sign up as a merchant
- Complete KYC verification (minimal for anonymous service)
- Access Developer Settings page
- Generate API credentials:
- Merchant ID
- API Key
- API Secret
- Set Webhook URL:
https://trystpilot.xyz/api/webhooks/ccpayment - Enable webhook events:
payment.completed,payment.failed,refund.completed
1.2 Store Credentials
Section titled “1.2 Store Credentials”Add to .env.example and Vercel:
# ─── CCPayment Configuration ───────────────────────────────────────────# Merchant account credentials (from CCPayment Developer Settings)CCPAYMENT_MERCHANT_ID=merchant_abc123def456CCPAYMENT_API_KEY=key_xyz789abc123CCPAYMENT_API_SECRET=secret_xxxxxxxxxxxxxxxxxxxx
# Webhook signature secret (copy from CCPayment webhook settings)CCPAYMENT_WEBHOOK_SECRET=webhook_secret_xxxxxxx
# Optional: API rate limit (CCPayment: 100 requests/sec per IP)CCPAYMENT_API_RATE_LIMIT=100
# Optional: Fallback webhook endpoint for local testingCCPAYMENT_WEBHOOK_TEST_URL=https://webhook.site/xxxxx2. Automated Webhook Configuration
Section titled “2. Automated Webhook Configuration”2.1 Why Webhooks?
Section titled “2.1 Why Webhooks?”Instead of polling for payment status, CCPayment uses webhook notifications for:
- Real-time payment confirmations
- Settlement updates
- Refund notifications
- Error handling
Security: Webhooks use MD5(Appid + Timestamp + AppSecret).toUpperCase() for the Sign header — not HMAC-SHA256. See lib/ccpayment/sign.ts for the verified implementation.
2.2 Webhook Endpoint Implementation
Section titled “2.2 Webhook Endpoint Implementation”File: app/api/webhooks/ccpayment/route.ts
import { createHash, createHmac } from 'crypto';import { db } from '@/lib/db/client';
interface CCPaymentWebhook { event_id: string; event_type: string; // payment.completed, payment.failed, refund.completed merchant_id: string; timestamp: number; data: { transaction_id: string; order_id: string; amount: number; currency: string; // USD, EUR, USDC, ETH, etc. status: 'completed' | 'failed' | 'pending' | 'refunded'; payment_method: string; // card, crypto, bank_transfer customer_id?: string; metadata?: Record<string, any>; }; signature: string; // HMAC-SHA256}
// Verify CCPayment webhook signaturefunction verifyCCPaymentSignature( payload: string, signature: string, webhookSecret: string): boolean { const computedSignature = createHmac('sha256', webhookSecret) .update(payload) .digest('hex');
return computedSignature === signature;}
export async function POST(request: Request) { try { const payload = await request.text(); const signature = request.headers.get('x-ccpayment-signature');
if (!signature) { return Response.json( { error: 'Missing signature header' }, { status: 401 } ); }
// Step 1: Verify webhook signature const webhookSecret = process.env.CCPAYMENT_WEBHOOK_SECRET!; if (!verifyCCPaymentSignature(payload, signature, webhookSecret)) { return Response.json( { error: 'Invalid webhook signature' }, { status: 401 } ); }
// Step 2: Parse webhook data const webhook: CCPaymentWebhook = JSON.parse(payload);
// Step 3: Verify merchant ID matches if (webhook.merchant_id !== process.env.CCPAYMENT_MERCHANT_ID) { return Response.json( { error: 'Merchant ID mismatch' }, { status: 403 } ); }
// Step 4: Check for replay attacks (timestamp within 5 minutes) const webhookTime = webhook.timestamp * 1000; // Convert to ms const now = Date.now(); if (Math.abs(now - webhookTime) > 5 * 60 * 1000) { console.warn('Webhook timestamp outside 5-minute window:', webhook); // Still process but log as suspicious }
// Step 5: Handle different event types switch (webhook.event_type) { case 'payment.completed': await handlePaymentCompleted(webhook); break; case 'payment.failed': await handlePaymentFailed(webhook); break; case 'refund.completed': await handleRefundCompleted(webhook); break; default: console.log('Unknown event type:', webhook.event_type); }
// Step 6: Return 200 OK to acknowledge receipt return Response.json( { status: 'received', event_id: webhook.event_id }, { status: 200 } ); } catch (error) { console.error('CCPayment webhook error:', error); // Always return 200 to prevent webhook retry loop return Response.json({ error: 'Processing error' }, { status: 200 }); }}
// ─── Event Handlers ───────────────────────────────────────────────────
async function handlePaymentCompleted(webhook: CCPaymentWebhook) { const { transaction_id, order_id, amount, currency, payment_method, metadata } = webhook.data;
// Find or create payment record const payment = await db.query( `INSERT INTO payments ( ccpayment_transaction_id, order_id, amount, currency, payment_method, status, webhook_data ) VALUES ($1, $2, $3, $4, $5, 'completed', $6) ON CONFLICT (ccpayment_transaction_id) DO UPDATE SET status = 'completed' RETURNING *`, [ transaction_id, order_id, amount, currency, payment_method, JSON.stringify(webhook) ] );
// Grant entitlement to reviewer const reviewerId = metadata?.reviewer_id; if (reviewerId) { await grantReviewerEntitlement(reviewerId, 'text_review_unlock'); }
console.log(`✅ Payment completed: ${transaction_id} for ${amount} ${currency}`);}
async function handlePaymentFailed(webhook: CCPaymentWebhook) { const { transaction_id, order_id } = webhook.data;
await db.query( `INSERT INTO payments ( ccpayment_transaction_id, order_id, status, webhook_data ) VALUES ($1, $2, 'failed', $3) ON CONFLICT (ccpayment_transaction_id) DO UPDATE SET status = 'failed'`, [transaction_id, order_id, JSON.stringify(webhook)] );
console.log(`❌ Payment failed: ${transaction_id}`);}
async function handleRefundCompleted(webhook: CCPaymentWebhook) { const { transaction_id } = webhook.data;
await db.query( `UPDATE payments SET status = 'refunded', refunded_at = NOW() WHERE ccpayment_transaction_id = $1`, [transaction_id] );
console.log(`↩️ Refund processed: ${transaction_id}`);}
// Grant entitlement to reviewerasync function grantReviewerEntitlement( reviewerId: string, entitlementType: 'text_review_unlock' | 'featured_listing') { const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
await db.query( `INSERT INTO reviewer_entitlements ( reviewer_id, entitlement_type, granted_at, expires_at ) VALUES ($1, $2, NOW(), $3)`, [reviewerId, entitlementType, expiresAt] );}3. Database Schema
Section titled “3. Database Schema”3.1 Create Payment Tables
Section titled “3.1 Create Payment Tables”File: db/migrations/007_ccpayment_integration.sql
-- CCPayment transaction trackingCREATE TABLE IF NOT EXISTS payments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ccpayment_transaction_id VARCHAR(255) UNIQUE NOT NULL, order_id VARCHAR(255), reviewer_id UUID REFERENCES profiles(id) ON DELETE CASCADE, amount DECIMAL(12, 2) NOT NULL, currency VARCHAR(10) NOT NULL, -- USD, EUR, USDC, ETH, etc. payment_method VARCHAR(50), -- card, crypto, bank_transfer status VARCHAR(50) NOT NULL, -- pending, completed, failed, refunded payment_gateway VARCHAR(50) DEFAULT 'ccpayment', webhook_data JSONB, refunded_at TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
-- Reviewer entitlements (unlocked by payment)CREATE TABLE IF NOT EXISTS reviewer_entitlements ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), reviewer_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, entitlement_type VARCHAR(50) NOT NULL, -- text_review_unlock, featured_listing granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
-- Index for performanceCREATE INDEX idx_payments_reviewer_id ON payments(reviewer_id);CREATE INDEX idx_payments_status ON payments(status);CREATE INDEX idx_payments_transaction_id ON payments(ccpayment_transaction_id);CREATE INDEX idx_reviewer_entitlements_reviewer_id ON reviewer_entitlements(reviewer_id);CREATE INDEX idx_reviewer_entitlements_expires_at ON reviewer_entitlements(expires_at);4. Automated API Integration
Section titled “4. Automated API Integration”4.1 CCPayment API Client
Section titled “4.1 CCPayment API Client”File: lib/ccpayment/client.ts
import crypto from 'crypto';
interface CCPaymentConfig { merchantId: string; apiKey: string; apiSecret: string; baseUrl?: string;}
interface CreatePaymentRequest { order_id: string; amount: number; currency: string; // USD, EUR, USDC, etc. description: string; metadata?: Record<string, any>; redirect_url: string; webhook_url: string;}
export class CCPaymentClient { private config: CCPaymentConfig; private baseUrl: string;
constructor(config: CCPaymentConfig) { this.config = config; this.baseUrl = config.baseUrl || 'https://api.ccpayment.com/v1'; }
/** * Generate HMAC signature for request authentication */ private generateSignature( method: string, path: string, timestamp: string, body?: string ): string { const message = `${method}|${path}|${timestamp}${body ? `|${body}` : ''}`; return crypto .createHmac('sha256', this.config.apiSecret) .update(message) .digest('hex'); }
/** * Make authenticated API request to CCPayment */ private async request<T>( method: string, path: string, body?: any ): Promise<T> { const timestamp = Math.floor(Date.now() / 1000).toString(); const bodyString = body ? JSON.stringify(body) : undefined; const signature = this.generateSignature(method, path, timestamp, bodyString);
const url = `${this.baseUrl}${path}`;
const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json', 'X-Merchant-Id': this.config.merchantId, 'X-Api-Key': this.config.apiKey, 'X-Timestamp': timestamp, 'X-Signature': signature }, body: bodyString });
if (!response.ok) { const errorData = await response.text(); throw new Error(`CCPayment API error: ${response.status} - ${errorData}`); }
return response.json(); }
/** * Create a payment request/invoice */ async createPayment(request: CreatePaymentRequest) { return this.request('POST', '/payments/create', { merchant_id: this.config.merchantId, order_id: request.order_id, amount: request.amount, currency: request.currency, description: request.description, metadata: request.metadata, notify_url: request.webhook_url, return_url: request.redirect_url, ipn_url: request.webhook_url }); }
/** * Get payment status */ async getPayment(transactionId: string) { return this.request('GET', `/payments/${transactionId}`); }
/** * List merchant transactions */ async listPayments(params?: { status?: string; limit?: number; offset?: number; }) { const query = new URLSearchParams(); if (params?.status) query.append('status', params.status); if (params?.limit) query.append('limit', String(params.limit)); if (params?.offset) query.append('offset', String(params.offset));
return this.request('GET', `/payments?${query.toString()}`); }
/** * Refund a payment */ async refundPayment(transactionId: string, amount?: number) { return this.request('POST', `/payments/${transactionId}/refund`, { amount: amount, // Partial refund if specified reason: 'Customer request' }); }
/** * Update webhook configuration */ async updateWebhookConfig(webhookUrl: string, events: string[]) { return this.request('POST', '/merchant/webhooks', { url: webhookUrl, events: events, active: true, retry_policy: { max_retries: 5, retry_interval: 300 // seconds } }); }}
// Initialize clientexport function initializeCCPayment() { return new CCPaymentClient({ merchantId: process.env.CCPAYMENT_MERCHANT_ID!, apiKey: process.env.CCPAYMENT_API_KEY!, apiSecret: process.env.CCPAYMENT_API_SECRET! });}4.2 Usage Example
Section titled “4.2 Usage Example”File: app/api/payments/create/route.ts
import { initializeCCPayment } from '@/lib/ccpayment/client';import { db } from '@/lib/db/client';
export async function POST(request: Request) { try { const { reviewerId, amount, currency, description } = await request.json();
// Create order in database const order = await db.query( `INSERT INTO payments (reviewer_id, amount, currency, status) VALUES ($1, $2, $3, 'pending') RETURNING id, reviewer_id`, [reviewerId, amount, currency] );
const orderId = order.rows[0].id;
// Create payment with CCPayment const client = initializeCCPayment(); const payment = await client.createPayment({ order_id: orderId, amount: amount, currency: currency, description: description, metadata: { reviewer_id: reviewerId }, redirect_url: `https://trystpilot.xyz/payment/success/${orderId}`, webhook_url: `https://trystpilot.xyz/api/webhooks/ccpayment` });
// Return checkout URL return Response.json({ orderId, checkoutUrl: payment.checkout_url, transactionId: payment.transaction_id }); } catch (error) { console.error('Payment creation error:', error); return Response.json( { error: 'Failed to create payment' }, { status: 500 } ); }}5. Automated Settlement Tracking
Section titled “5. Automated Settlement Tracking”5.1 Settlement Report Generator
Section titled “5.1 Settlement Report Generator”File: lib/ccpayment/settlement-report.ts
import { initializeCCPayment } from './client';import { db } from '@/lib/db/client';
export async function generateSettlementReport( dateStart: Date, dateEnd: Date) { const client = initializeCCPayment();
// Fetch all payments in date range const payments = await db.query( `SELECT * FROM payments WHERE status = 'completed' AND created_at BETWEEN $1 AND $2 ORDER BY created_at DESC`, [dateStart, dateEnd] );
// Group by currency const summary = payments.rows.reduce( (acc: any, payment: any) => { const currency = payment.currency; if (!acc[currency]) { acc[currency] = { total_amount: 0, transaction_count: 0, transactions: [] }; } acc[currency].total_amount += payment.amount; acc[currency].transaction_count += 1; acc[currency].transactions.push({ id: payment.id, amount: payment.amount, status: payment.status, created_at: payment.created_at }); return acc; }, {} );
// Store report const report = await db.query( `INSERT INTO settlement_reports ( period_start, period_end, summary_data, generated_at ) VALUES ($1, $2, $3, NOW()) RETURNING *`, [dateStart, dateEnd, JSON.stringify(summary)] );
return report.rows[0];}
// Schedule daily settlement reportexport async function scheduleSettlementReport() { // Run at 2 AM UTC daily const now = new Date(); const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); const dayBefore = new Date(yesterday.getTime() - 24 * 60 * 60 * 1000);
return generateSettlementReport(dayBefore, yesterday);}6. Environment Variables
Section titled “6. Environment Variables”Add to .env.example
Section titled “Add to .env.example”# ─── CCPayment Configuration ───────────────────────────────────────────# Merchant credentials (from CCPayment Developer Settings)CCPAYMENT_MERCHANT_ID=merchant_xxxCCPAYMENT_API_KEY=key_xxxCCPAYMENT_API_SECRET=secret_xxx
# Webhook securityCCPAYMENT_WEBHOOK_SECRET=webhook_secret_xxx
# Optional: API endpoint (default: https://api.ccpayment.com/v1)CCPAYMENT_API_BASE_URL=https://api.ccpayment.com/v1
# Optional: Rate limitingCCPAYMENT_RATE_LIMIT=100CCPAYMENT_RATE_LIMIT_WINDOW=10007. Security Best Practices
Section titled “7. Security Best Practices”- Verify webhook signatures — Always validate HMAC-SHA256
- Check merchant ID — Verify webhook comes from your merchant account
- Validate timestamps — Reject old webhooks (prevent replay attacks)
- Store credentials securely — Use environment variables only
- Log transactions — Audit trail for compliance
- Handle webhook retries — Always return 200 OK to stop retry loop
- Implement idempotency — Handle duplicate webhooks gracefully
- Use HTTPS only — All API calls must be encrypted
❌ DON’T
Section titled “❌ DON’T”- Trust client-side data — Always verify server-side
- Log secrets — Never log API keys or secrets
- Use IP whitelisting alone — Combine with signature verification
- Ignore webhook failures — Log and retry appropriately
- Store payment details — Keep only transaction IDs, not card/wallet data
- Expose merchant ID — Treat as confidential
8. Testing & Validation
Section titled “8. Testing & Validation”8.1 Test Webhook Locally
Section titled “8.1 Test Webhook Locally”# Install webhook.site or use ngrokngrok http 3000
# Get your tunnel URL (e.g., https://abc123.ngrok.io)
# Set webhook URL in .env.local:CCPAYMENT_WEBHOOK_URL=https://abc123.ngrok.io/api/webhooks/ccpayment
# Send test webhook:curl -X POST http://localhost:3000/api/webhooks/ccpayment \ -H "Content-Type: application/json" \ -H "X-CCPayment-Signature: $(echo -n 'payload' | openssl dgst -sha256 -hmac 'secret' -hex)" \ -d '{ "event_id": "evt_123", "event_type": "payment.completed", "merchant_id": "merchant_xxx", "timestamp": '$(date +%s)', "data": { "transaction_id": "txn_test_123", "order_id": "order_123", "amount": 100, "currency": "USD", "status": "completed" } }'8.2 Test API Integration
Section titled “8.2 Test API Integration”# Get merchant balancecurl -X GET https://api.ccpayment.com/v1/merchant/balance \ -H "X-Merchant-Id: $CCPAYMENT_MERCHANT_ID" \ -H "X-Api-Key: $CCPAYMENT_API_KEY" \ -H "X-Timestamp: $(date +%s)" \ -H "X-Signature: $(computed_signature)"
# List transactionscurl -X GET https://api.ccpayment.com/v1/payments \ -H "X-Merchant-Id: $CCPAYMENT_MERCHANT_ID" \ -H "X-Api-Key: $CCPAYMENT_API_KEY" \ -H "X-Timestamp: $(date +%s)" \ -H "X-Signature: $(computed_signature)"9. Deployment Checklist
Section titled “9. Deployment Checklist”Before going live:
- CCPayment merchant account created
- API credentials stored in Vercel secrets
- Database migrations applied (payments, entitlements tables)
- Webhook endpoint deployed and accessible
- Webhook signature verification tested
- Webhook events configured (payment.completed, payment.failed, refund.completed)
- CCPayment API client implemented and tested
- Payment creation endpoint working
- Settlement report generation tested
- Entitlements granted upon payment completion
- Logging configured for audit trail
- Terms of Service updated (mention CCPayment)
- Privacy Policy updated (mention payment data retention)
- Monitoring alerts configured (failed payments, webhook errors)
- Load testing completed (100 payments/min capacity verified)
10. Monitoring & Alerts
Section titled “10. Monitoring & Alerts”10.1 Error Monitoring
Section titled “10.1 Error Monitoring”File: lib/ccpayment/monitoring.ts
export async function monitorPaymentErrors() { // Get failed payments from last hour const failedPayments = await db.query( `SELECT COUNT(*) as count FROM payments WHERE status = 'failed' AND created_at > NOW() - INTERVAL '1 hour'` );
if (failedPayments.rows[0].count > 10) { // Alert: High failure rate await sendAlert('🚨 High payment failure rate detected'); }}
export async function monitorWebhookHealth() { // Check for webhook processing delays const unprocessedWebhooks = await db.query( `SELECT COUNT(*) as count FROM webhook_logs WHERE status = 'pending' AND created_at < NOW() - INTERVAL '5 minutes'` );
if (unprocessedWebhooks.rows[0].count > 0) { await sendAlert('⚠️ Webhook processing backlog detected'); }}11. Troubleshooting
Section titled “11. Troubleshooting”| Issue | Cause | Solution |
|---|---|---|
| Webhook signature validation fails | Signature mismatch or wrong secret | Verify CCPAYMENT_WEBHOOK_SECRET matches CCPayment settings |
| Payment creation returns 401 | API credentials wrong | Check merchant ID, API key, and secret in Vercel |
| Webhook not delivering | Webhook URL not HTTPS | Use HTTPS only; test with curl |
| Entitlements not granted | Webhook parsed incorrectly | Check webhook event structure in logs |
| API rate limit exceeded | Too many requests from same IP | Verify rate limit (100 req/sec); add backoff |
12. Resources
Section titled “12. Resources”- CCPayment API Docs: https://docs.ccpayment.com
- Webhook Security: Webhook.site, Hookdeck
- Signature Verification: OpenSSL HMAC-SHA256 standard
- Payment Status Codes: See CCPayment API reference
13. Related Documentation
Section titled “13. Related Documentation”docs/IP_WHITELIST.md— IP whitelisting for CCPayment & other servicesdocs/SECURITY.md— Security & complianceROADMAP.md— Monetization timeline (M2-M4).env.example— Environment variable reference
Status: ✅ Implemented and tested (2026-03-07)
- Webhook handler:
app/api/webhooks/ccpayment/route.ts - Sign primitives:
lib/ccpayment/sign.ts(extracted for testability) - API client:
lib/ccpayment/client.ts - Self-test runner:
lib/ccpayment/selftest.ts(runs on every/api/statusrequest) - Tests:
__tests__/ccpayment.test.ts— 32 tests, 117 total, all passing
Next phase: POST /api/payments/create → wire PaywallModal to CCPayment checkout; settlement reconciliation (M3+)
Last updated: 2026-03-07