The Two-Phase Payment Verification System provides fast user feedback through immediate verification (3-second checks for 15 minutes) followed by background verification (hourly checks for up to 24 hours). This approach ensures most payments are confirmed within seconds while handling edge cases where bank processing takes hours.
- ✅ Immediate Verification: 3-second checks for 15 minutes after user confirmation
- ✅ Background Verification: Hourly checks for delayed payments
- ✅ Email Notifications: Confirmation and expiration emails with HTML templates
- ✅ Webhook Integration: Real-time merchant notifications
- ✅ 24-Hour Timeout: Automatic expiration handling
- ✅ Atomic Updates: Race condition prevention between verification phases
- ✅ Comprehensive Logging: Full audit trail of all verification activities
- Trigger: User clicks "I've sent the money" via
/verifyendpoint - Frequency: Every 3 seconds
- Duration: 15 minutes maximum
- Purpose: Fast feedback for payments that confirm quickly (majority of cases)
- Trigger: Automatic cron job
- Frequency: Every 1 hour
- Duration: Until confirmed or 24 hours expire
- Purpose: Handle delayed bank confirmations and edge cases
- Action: Mark transaction as PAYOUT_FAILED
- Notification: Send expiration email to user
- Fallback: User can contact support with proof of payment
-
Verification Service (
src/services/verification.service.ts)- Shared verification logic for both immediate and background phases
- Toronet API integration with proper error handling
- Atomic transaction updates to prevent race conditions
-
Immediate Verification (
src/routes/transactions.ts)/verifyendpoint that starts 3-second verification checks- Runs for 15 minutes maximum per transaction
- Stops automatically when payment confirmed or time expires
-
Background Verification Service (
src/services/verify-pending-transactions.ts)- Hourly cron job for transactions past immediate verification phase
- Handles expired transactions and cleanup
- Continues until payment confirmed or 24-hour timeout
-
EmailService (
src/services/email.service.ts)- Sends confirmation and expiration emails using Nodemailer
- Professional HTML templates with ChainPaye branding
-
Transaction Model Updates (
src/models/Transaction.ts)- Added
verificationStartedAtfield to track verification phase - Enhanced
lastVerificationCheckandexpiresAtfields
- Added
// New fields added to Transaction schema
verificationStartedAt: {
type: Date,
index: true,
}
lastVerificationCheck: {
type: Date,
index: true,
}
expiresAt: {
type: Date,
index: true,
default: function() {
return new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours from creation
}
}Endpoint: POST /api/v1/transactions/:reference/verify
Headers:
admin: your_admin_address
adminpwd: your_admin_password
Request Body:
{
"senderName": "John Doe",
"senderPhone": "+1234567890",
"senderEmail": "john.doe@example.com",
"currency": "USD",
"txid": "toro_ref_123456",
"paymentType": "card",
"amount": "250.00",
"successUrl": "https://merchant.com/webhook",
"paymentLinkId": "507f1f77bcf86cd799439011"
}Response:
{
"success": true,
"message": "Verification started. You will receive an email confirmation when payment is confirmed.",
"data": {
"transactionId": "TXN_REF_123",
"email": "john.doe@example.com",
"verificationPhase": "immediate",
"checkInterval": "3 seconds",
"duration": "15 minutes"
}
}PENDING → (User clicks "I've sent money") → /verify endpoint called
↓
Phase 1: Immediate Verification (0-15 min, every 3 seconds)
↓
Phase 2: Background Verification (15 min - 24 hours, every 1 hour)
↓
PAID → COMPLETED (or PAYOUT_FAILED after 24 hours)
- User Action: Clicks "I've sent the money" button
- API Call: Frontend calls
POST /transactions/:reference/verify - Verification Start:
- Saves payment details and Toronet reference
- Sets
verificationStartedAttimestamp - Starts 3-second interval checks
- Checking Process:
- Calls Toronet API every 3 seconds
- Continues for maximum 15 minutes
- Stops when payment confirmed or time expires
- On Confirmation: Updates transaction, sends email, calls webhook
- Cron Job: Runs every 1 hour
- Transaction Selection:
- Finds PENDING transactions with
verificationStartedAt> 15 minutes ago - Excludes expired transactions (> 24 hours)
- Finds PENDING transactions with
- Verification Process:
- Calls Toronet API for each transaction
- Updates
lastVerificationChecktimestamp - Handles confirmations and expirations
- Continuation: Repeats until payment confirmed or expired
- Detection: Transactions with
expiresAt< current time - State Update: Changes state to PAYOUT_FAILED
- Notification: Sends expiration email to user
- Audit: Logs expiration event for tracking
Endpoint: POST https://www.toronet.org/api/payment/toro/
Request:
{
"op": "recordfiattransaction",
"params": ["USD", "transaction_reference", "card"]
}Headers:
admin: your_admin_address
adminpwd: your_admin_password
Trigger: When payment is confirmed via background verification
Recipient: transaction.payerInfo.email
Subject: "Payment Confirmed - Transaction #{transactionId}"
Content:
- ✅ Success icon and confirmation message
- Transaction details table (ID, amount, merchant, reference, confirmed time)
- Thank you message and support contact
- ChainPaye branding
Trigger: When transaction expires after 24 hours
Recipient: transaction.payerInfo.email
Subject: "Payment Verification Pending - Action Required"
Content:
- ⏰ Warning icon and timeout message
- Transaction details and next steps
- Contact support button
- ChainPaye branding
When a payment is confirmed, the system sends a POST request to the payment link's successUrl:
Webhook Payload:
{
"event": "payment.confirmed",
"transaction": {
"id": "trans_123",
"reference": "TXN_REF_123",
"amount": "250.00",
"currency": "USD",
"state": "PAID",
"paidAt": "2026-02-04T09:35:00.000Z",
"payerInfo": {
"email": "user@example.com",
"name": "John Doe",
"phone": "+1234567890"
}
},
"paymentLink": {
"id": "link_123",
"merchantId": "merchant_123",
"name": "My Business"
},
"timestamp": "2026-02-04T09:35:00.000Z"
}Headers:
Content-Type: application/json
User-Agent: ChainPaye-Webhook/1.0
# Existing Toronet Configuration
TORONET_ADMIN=your_admin_address
TORONET_ADMIN_PWD=your_admin_password
# Nodemailer SMTP Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM=noreply@chainpaye.com
# Support Configuration
SUPPORT_EMAIL=support@chainpaye.com- Enable 2-Factor Authentication on your Gmail account
- Generate App Password:
- Go to https://myaccount.google.com/apppasswords
- Select "Mail" as the app
- Copy the generated 16-character password
- Configure Environment:
- Set
SMTP_USERto your Gmail address - Set
SMTP_PASSto the generated app password
- Set
The background verification service starts automatically when the server starts:
// In src/server.ts
import { startPaymentVerification } from "./services/verify-pending-transactions";
// After database connection
startPaymentVerification();The service stops gracefully during server shutdown:
// In src/server.ts
import { stopPaymentVerification } from "./services/verify-pending-transactions";
// During graceful shutdown
stopPaymentVerification();Check if the service is running:
import { paymentVerificationService } from "./services/verify-pending-transactions";
const status = paymentVerificationService.getStatus();
console.log('Service running:', status.isRunning);-
Full Background Verification Test:
node test-background-verification.js
-
Email Configuration Test:
node test-background-verification.js email
-
Monitor Pending Transactions:
node test-background-verification.js monitor
- Email saves correctly in
payerInfo.email - Cron job fetches PENDING transactions
- Toronet API is called with correct parameters
-
lastVerificationCheckupdates after each check - State updates to PAID when confirmed
- Success URL receives POST with correct data
- Confirmation email sends successfully
- Expired transactions are handled after 24 hours
- Expiration email sends successfully
The service provides detailed console logging:
🚀 Starting payment verification service...
🔍 Starting payment verification at 2026-02-04T10:00:00.000Z
📋 Found 3 transactions to verify
🔍 Verifying transaction trans_123...
✅ Payment confirmed for transaction trans_123
📡 Webhook sent successfully to https://merchant.com/webhook (status: 200)
📧 Confirmation email sent successfully to user@example.com
✅ Verification completed in 1250ms
📊 Results: 3 verified, 1 confirmed, 0 errors
All verification activities are logged to the audit system:
VERIFICATION_ERROR: When verification failsPAYMENT_CONFIRMED: When payment is confirmedTRANSACTION_EXPIRED: When transaction expiresWEBHOOK_SENT: When webhook is sent successfullyWEBHOOK_FAILED: When webhook fails
- Toronet API errors: Logged but don't stop processing other transactions
- Email failures: Logged but don't block the confirmation flow
- Webhook failures: Logged but don't block the confirmation flow
- Database errors: Logged and may stop processing to prevent data corruption
- Verification: Continues every 5 minutes until payment confirmed or expired
- Webhooks: Single attempt (merchant should implement idempotency)
- Emails: Single attempt (can be manually resent if needed)
The following indexes are automatically created:
// Transaction indexes for efficient querying
{ state: 1, expiresAt: 1 } // Find pending non-expired transactions
{ lastVerificationCheck: 1 } // Find transactions needing verification
{ expiresAt: 1 } // Find expired transactionsThe service uses optimized queries to minimize database load:
// Efficient query for pending transactions
{
state: 'PENDING',
expiresAt: { $gt: new Date() },
$or: [
{ lastVerificationCheck: { $lt: fiveMinutesAgo } },
{ lastVerificationCheck: { $exists: false } }
]
}- Toronet API: Uses admin credentials (secure these properly)
- Webhooks: Use HTTPS endpoints and implement signature verification
- Email: Use app passwords, not account passwords
- Email Storage: Emails are stored in existing
payerInfo.emailfield - Audit Logs: Sensitive data is not logged in audit trails
- Webhook Data: Only necessary transaction data is sent
-
Service Not Starting:
- Check database connection
- Verify environment variables
- Check console logs for errors
-
Emails Not Sending:
- Verify SMTP configuration
- Check Gmail app password setup
- Test email service connection
-
Webhooks Failing:
- Verify merchant endpoint is accessible
- Check webhook URL format
- Review webhook logs in audit trail
-
Transactions Not Updating:
- Check Toronet API credentials
- Verify transaction references
- Review API response logs
Enable detailed logging by setting:
NODE_ENV=developmentThis provides additional debug information in console logs.
- Retry Logic: Configurable retry attempts for failed webhooks
- Email Templates: Customizable email templates per merchant
- Webhook Signatures: HMAC signature verification for webhooks
- Rate Limiting: Configurable verification intervals
- Dashboard: Admin interface to monitor verification status
- Metrics: Prometheus metrics for monitoring
- Alerting: Slack/email alerts for system issues
Future versions may include:
# Verification Configuration
VERIFICATION_INTERVAL_MINUTES=5
TRANSACTION_TIMEOUT_HOURS=24
MAX_VERIFICATION_ATTEMPTS=288 # 24 hours / 5 minutes
# Webhook Configuration
WEBHOOK_TIMEOUT_SECONDS=10
WEBHOOK_RETRY_ATTEMPTS=3
WEBHOOK_SIGNATURE_SECRET=your-secret-key
# Email Configuration
EMAIL_TEMPLATE_PATH=./templates/emails/
EMAIL_RETRY_ATTEMPTS=2For issues or questions about the background verification system:
- Email: support@chainpaye.com
- Documentation: This file and API_DOCUMENTATION.md
- Test Scripts: Use provided test scripts for debugging
- Audit Logs: Check audit trail for detailed operation history