Skip to content

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


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. Visit ccpayment.com
  2. Sign up as a merchant
  3. Complete KYC verification (minimal for anonymous service)
  4. Access Developer Settings page
  5. Generate API credentials:
    • Merchant ID
    • API Key
    • API Secret
  6. Set Webhook URL: https://trystpilot.xyz/api/webhooks/ccpayment
  7. Enable webhook events: payment.completed, payment.failed, refund.completed

Add to .env.example and Vercel:

Terminal window
# ─── CCPayment Configuration ───────────────────────────────────────────
# Merchant account credentials (from CCPayment Developer Settings)
CCPAYMENT_MERCHANT_ID=merchant_abc123def456
CCPAYMENT_API_KEY=key_xyz789abc123
CCPAYMENT_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 testing
CCPAYMENT_WEBHOOK_TEST_URL=https://webhook.site/xxxxx

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.

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 signature
function 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 reviewer
async 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]
);
}

File: db/migrations/007_ccpayment_integration.sql

-- CCPayment transaction tracking
CREATE 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 performance
CREATE 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);

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 client
export function initializeCCPayment() {
return new CCPaymentClient({
merchantId: process.env.CCPAYMENT_MERCHANT_ID!,
apiKey: process.env.CCPAYMENT_API_KEY!,
apiSecret: process.env.CCPAYMENT_API_SECRET!
});
}

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 }
);
}
}

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 report
export 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);
}

Terminal window
# ─── CCPayment Configuration ───────────────────────────────────────────
# Merchant credentials (from CCPayment Developer Settings)
CCPAYMENT_MERCHANT_ID=merchant_xxx
CCPAYMENT_API_KEY=key_xxx
CCPAYMENT_API_SECRET=secret_xxx
# Webhook security
CCPAYMENT_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 limiting
CCPAYMENT_RATE_LIMIT=100
CCPAYMENT_RATE_LIMIT_WINDOW=1000

  • 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
  • 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

Terminal window
# Install webhook.site or use ngrok
ngrok 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"
}
}'
Terminal window
# Get merchant balance
curl -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 transactions
curl -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)"

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)

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');
}
}

IssueCauseSolution
Webhook signature validation failsSignature mismatch or wrong secretVerify CCPAYMENT_WEBHOOK_SECRET matches CCPayment settings
Payment creation returns 401API credentials wrongCheck merchant ID, API key, and secret in Vercel
Webhook not deliveringWebhook URL not HTTPSUse HTTPS only; test with curl
Entitlements not grantedWebhook parsed incorrectlyCheck webhook event structure in logs
API rate limit exceededToo many requests from same IPVerify rate limit (100 req/sec); add backoff


  • docs/IP_WHITELIST.md — IP whitelisting for CCPayment & other services
  • docs/SECURITY.md — Security & compliance
  • ROADMAP.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/status request)
  • 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