Skip to content

Security Best Practices

Security is critical when handling cryptocurrency payments. This guide covers essential security practices for integrating PayIn into your application.

Security Fundamentals

PayIn is designed with security at its core:

  • 🔐 Non-Custodial: You control private keys via mnemonic phrases
  • 🔒 End-to-End Encryption: All API communication over HTTPS
  • 🛡️ Signature Verification: Webhooks signed with HMAC-SHA256
  • 📊 Complete Audit Logs: All operations logged and traceable
  • 🔑 Fine-Grained Access: Role-based permissions (Owner, Admin, Member, Viewer)

Your Responsibilities:

  • Secure API keys and webhook secrets
  • Safely store mnemonic phrases
  • Verify all webhook signatures
  • Monitor for suspicious activity
  • Follow production security best practices

API Key Security

API keys are credentials that grant access to your PayIn account. Treat them like passwords.

Storage Best Practices

✅ DO:

  • Store in environment variables (process.env.PAYIN_API_KEY)
  • Use secret management systems (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault)
  • Encrypt keys at rest
  • Use different keys for testnet and mainnet
  • Create separate keys for different services/environments
bash
# .env file
PAYIN_TESTNET_KEY=pk_test_abc123...
PAYIN_MAINNET_KEY=pk_live_xyz789...

❌ DON'T:

  • Commit keys to version control (Git, SVN, etc.)
  • Hardcode keys in application code
  • Log keys in application logs
  • Share keys via email or chat
  • Expose keys in client-side code (JavaScript, mobile apps)
  • Use same key across multiple environments

Key Rotation

Rotate API keys regularly to minimize risk if a key is compromised.

Rotation Schedule:

  • 🔄 Every 90 days: Routine rotation
  • ⚠️ Immediately: Team member departure or suspected compromise
  • 🚨 Within 24 hours: Confirmed security incident

Zero-Downtime Rotation:

bash
# Step 1: Create new API key in PayIn Admin
NEW_KEY=pk_live_new123...

# Step 2: Update environment variables (keep old key temporarily)
PAYIN_API_KEY=$NEW_KEY
PAYIN_API_KEY_OLD=$OLD_KEY

# Step 3: Deploy with dual-key support
# Your app should try new key, fallback to old if needed

# Step 4: After 24 hours, verify new key works
# Check logs for any old key usage

# Step 5: Delete old key from PayIn Admin

Implementation:

typescript
async function makeApiCall(endpoint: string, options: any) {
  // Try new key first
  try {
    return await fetch(endpoint, {
      ...options,
      headers: {
        'Authorization': `Bearer ${process.env.PAYIN_API_KEY}`,
        ...options.headers
      }
    });
  } catch (error) {
    // Fallback to old key during transition
    if (process.env.PAYIN_API_KEY_OLD) {
      console.warn('Falling back to old API key');
      return await fetch(endpoint, {
        ...options,
        headers: {
          'Authorization': `Bearer ${process.env.PAYIN_API_KEY_OLD}`,
          ...options.headers
        }
      });
    }
    throw error;
  }
}

Key Scope and Permissions

PayIn uses role-based access control:

RolePermissionsUse Case
OwnerFull access, can transfer ownershipOrganization founder
AdminManage settings, members, API keysOperations team
MemberCreate orders, deposits, view dataApplication backend
ViewerRead-only accessMonitoring, analytics

Best Practices:

  • ✅ Create keys with minimum necessary permissions
  • ✅ Use separate keys for read vs. write operations
  • ✅ One key per service/application
  • ✅ Monitor key usage in Admin dashboard

Monitoring Key Usage

Monitor API key usage for suspicious activity:

typescript
// Log all API calls with key ID
logger.info('API call', {
  endpoint: '/orders',
  keyId: extractKeyId(apiKey),
  ipAddress: req.ip,
  userAgent: req.headers['user-agent'],
  timestamp: new Date().toISOString()
});

// Alert on unusual patterns
if (callsInLastHour > 1000) {
  await sendAlert({
    type: 'api_abuse',
    message: `Unusual API activity: ${callsInLastHour} calls in last hour`,
    keyId: extractKeyId(apiKey)
  });
}

Webhook Security

Webhooks are HTTP callbacks from PayIn to your server. Securing them is critical.

Signature Verification (Required)

Always verify webhook signatures using HMAC-SHA256:

typescript
import crypto from 'crypto';

function verifyWebhookSignature(
  rawBody: Buffer,
  signature: string,
  secret: string
): boolean {
  const parts = signature.split(',');
  const timestamp = parseInt(parts.find(p => p.startsWith('t='))!.split('=')[1]);
  const sig = parts.find(p => p.startsWith('v1='))!.split('=')[1];

  // Check timestamp tolerance (5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (now - timestamp > 300) {
    throw new Error('Webhook timestamp expired');
  }

  // Compute expected signature
  const payload = `${timestamp}.${rawBody.toString()}`;
  const expected = crypto.createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(sig, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

Complete Webhook Security

See Webhooks Guide for complete signature verification implementation in TypeScript, Python, and PHP.

Endpoint Protection

  1. HTTPS Only

    • PayIn requires HTTPS webhook endpoints
    • Use valid SSL/TLS certificates
    • Never use self-signed certificates in production
  2. Authentication (Optional)

    • Add custom authentication header
    • Implement IP whitelist if feasible
    • Use VPN or private network for sensitive webhooks
  3. Rate Limiting

    typescript
    import rateLimit from 'express-rate-limit';
    
    const webhookLimiter = rateLimit({
      windowMs: 1 * 60 * 1000, // 1 minute
      max: 100, // 100 requests per minute
      message: 'Too many webhook requests'
    });
    
    app.post('/webhooks/payin', webhookLimiter, handleWebhook);
  4. Input Validation

    typescript
    app.post('/webhooks/payin', async (req, res) => {
      // Validate event structure
      const event = req.body;
      if (!event.id || !event.type || !event.data) {
        return res.status(400).json({ error: 'Invalid webhook structure' });
      }
    
      // Validate event type
      const validTypes = [
        'order.completed', 'order.expired',
        'deposit.pending', 'deposit.confirmed'
      ];
      if (!validTypes.includes(event.type)) {
        return res.status(400).json({ error: 'Unknown event type' });
      }
    
      // Process webhook...
    });

Replay Attack Prevention

Prevent attackers from replaying old webhooks:

typescript
// Track processed webhook IDs (use Redis or database in production)
const processedWebhooks = new Set<string>();

app.post('/webhooks/payin', async (req, res) => {
  const event = req.body;

  // Check if already processed
  if (processedWebhooks.has(event.id)) {
    console.warn('Replay attack detected:', event.id);
    return res.status(200).json({ received: true }); // Still return 200
  }

  // Verify signature (includes timestamp check)
  if (!verifyWebhookSignature(req.body, signature, secret)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process webhook
  await handleWebhook(event);
  processedWebhooks.add(event.id);

  res.json({ received: true });
});

Mnemonic Phrase Security

Your mnemonic phrase is the master key to all addresses generated by PayIn. If someone obtains your mnemonic, they control your funds.

Storage Requirements

🔴 CRITICAL:

  • Never store mnemonics in database
  • Never log mnemonics
  • Never transmit mnemonics over network
  • Never commit mnemonics to version control

Recommended Storage:

  1. Hardware Security Module (HSM) - Best for high-value operations
  2. Air-gapped computer - Store offline, never connected to internet
  3. Encrypted paper backup - Physical storage in secure location
  4. Encrypted USB drive - Stored in bank safe deposit box

Generating Addresses

Use PayIn's address generation tool securely:

bash
# Generate addresses offline on air-gapped machine
payin-address-tool generate \
  --mnemonic "your twelve word seed phrase..." \
  --protocol evm \
  --count 1000 \
  --output addresses.json

# Transfer addresses.json to online machine
# Import to PayIn via Admin UI or API

# ⚠️ NEVER upload mnemonic to any online service

Multi-Signature Alternative

For high-value deployments, consider multi-signature wallets:

  • Require multiple approvals for withdrawals
  • Distribute control across multiple parties
  • Reduce single point of failure

Payment Verification

Always verify payments before fulfilling orders.

Amount Verification

typescript
async function verifyPayment(webhook: any) {
  const { amount, currency, orderId } = webhook.data;

  // 1. Fetch original order
  const order = await db.orders.findUnique({ where: { id: orderId } });

  // 2. Verify exact amount (in smallest units)
  if (amount !== order.amount) {
    throw new Error(`Amount mismatch: expected ${order.amount}, got ${amount}`);
  }

  // 3. Verify currency
  if (currency !== order.currency) {
    throw new Error(`Currency mismatch: expected ${order.currency}, got ${currency}`);
  }

  // 4. Verify order not already fulfilled
  if (order.status === 'fulfilled') {
    throw new Error('Order already fulfilled');
  }

  return true;
}

Transaction Verification

typescript
async function verifyTransaction(txHash: string, chainId: string) {
  // 1. Check transaction exists on blockchain
  const tx = await blockchainProvider.getTransaction(txHash);
  if (!tx) {
    throw new Error('Transaction not found');
  }

  // 2. Verify sufficient confirmations
  const confirmations = await blockchainProvider.getConfirmations(txHash);
  const required = getRequiredConfirmations(chainId); // e.g., 3 for Ethereum
  if (confirmations < required) {
    throw new Error(`Insufficient confirmations: ${confirmations}/${required}`);
  }

  // 3. Verify transaction succeeded
  if (!tx.status) {
    throw new Error('Transaction failed');
  }

  return true;
}

Address Verification

typescript
async function verifyAddress(address: string, protocol: string) {
  // 1. Verify address format
  if (!isValidAddress(address, protocol)) {
    throw new Error('Invalid address format');
  }

  // 2. Verify address ownership (belongs to your pool)
  const owned = await db.addressPool.findUnique({
    where: { address, protocol }
  });
  if (!owned) {
    throw new Error('Address not in pool');
  }

  // 3. Verify address allocated correctly
  if (owned.status !== 'allocated' && owned.status !== 'bound') {
    throw new Error('Address not properly allocated');
  }

  return true;
}

Network Security

HTTPS Everywhere

All communication must use HTTPS:

typescript
// ❌ BAD: HTTP endpoint
const API_BASE = 'http://api.payin.com'; // INSECURE

// ✅ GOOD: HTTPS endpoint
const API_BASE = 'https://api.payin.com'; // SECURE

Requirements:

  • Use TLS 1.2 or higher
  • Valid SSL/TLS certificates (not self-signed in production)
  • HSTS headers enabled
  • Certificate pinning for mobile apps

Firewall Configuration

bash
# Allow only necessary ports
sudo ufw allow 443/tcp  # HTTPS
sudo ufw allow 22/tcp   # SSH (restrict to specific IPs)
sudo ufw deny 80/tcp    # Block HTTP
sudo ufw enable

# Restrict database access
sudo ufw allow from 10.0.1.0/24 to any port 5432  # PostgreSQL

DDoS Protection

Use DDoS protection services:

  • Cloudflare
  • AWS Shield
  • Akamai
  • Fastly

Access Control

Role-Based Access Control (RBAC)

Implement least privilege principle:

typescript
// Define roles and permissions
const ROLES = {
  owner: ['*'],  // All permissions
  admin: ['orders:*', 'deposits:*', 'webhooks:*', 'keys:*'],
  member: ['orders:create', 'orders:read', 'deposits:create', 'deposits:read'],
  viewer: ['orders:read', 'deposits:read']
};

function checkPermission(user: User, permission: string): boolean {
  const userPermissions = ROLES[user.role];
  return userPermissions.includes('*') ||
         userPermissions.includes(permission) ||
         userPermissions.some(p => p.endsWith(':*') && permission.startsWith(p.split(':')[0]));
}

// Usage
if (!checkPermission(user, 'orders:create')) {
  throw new Error('Insufficient permissions');
}

Multi-Factor Authentication (MFA)

Enable MFA for PayIn Admin dashboard:

  1. Navigate to SettingsSecurity
  2. Enable Two-Factor Authentication
  3. Scan QR code with authenticator app (Google Authenticator, Authy)
  4. Save backup codes securely

Enforce MFA for:

  • ✅ All Owner accounts
  • ✅ All Admin accounts
  • ✅ Production environment access
  • ✅ API key creation

Session Management

typescript
// Implement secure session handling
const sessionConfig = {
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,      // HTTPS only
    httpOnly: true,    // Not accessible via JavaScript
    sameSite: 'strict', // CSRF protection
    maxAge: 3600000    // 1 hour
  }
};

Logging and Monitoring

Comprehensive Logging

Log all critical operations:

typescript
import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Log API calls
app.use((req, res, next) => {
  logger.info('API request', {
    method: req.method,
    path: req.path,
    ip: req.ip,
    userAgent: req.headers['user-agent'],
    userId: req.user?.id,
    timestamp: new Date().toISOString()
  });
  next();
});

// Log webhook deliveries
logger.info('Webhook received', {
  eventId: event.id,
  eventType: event.type,
  deliveryId: req.headers['x-payin-delivery-id'],
  timestamp: new Date().toISOString()
});

// Log payment confirmations
logger.info('Payment confirmed', {
  orderId: order.id,
  amount: order.amount,
  currency: order.currency,
  txHash: payment.txHash,
  timestamp: new Date().toISOString()
});

⚠️ Never Log:

  • API keys or webhook secrets
  • Mnemonic phrases
  • Private keys
  • Full credit card numbers (if applicable)
  • Passwords

Real-Time Monitoring

Set up monitoring and alerts:

typescript
// Monitor for unusual activity
async function monitorApiActivity() {
  const last10MinCalls = await db.auditLogs.count({
    where: {
      createdAt: { gte: new Date(Date.now() - 10 * 60 * 1000) }
    }
  });

  if (last10MinCalls > 1000) {
    await sendAlert({
      type: 'api_spike',
      message: `High API activity: ${last10MinCalls} calls in 10 minutes`,
      severity: 'high'
    });
  }
}

// Monitor for failed webhooks
async function monitorWebhookFailures() {
  const failed = await db.webhookLogs.count({
    where: {
      status: 'failed',
      createdAt: { gte: new Date(Date.now() - 15 * 60 * 1000) }
    }
  });

  if (failed > 10) {
    await sendAlert({
      type: 'webhook_failures',
      message: `${failed} webhook failures in 15 minutes`,
      severity: 'critical'
    });
  }
}

// Run monitors periodically
setInterval(monitorApiActivity, 5 * 60 * 1000);  // Every 5 minutes
setInterval(monitorWebhookFailures, 5 * 60 * 1000);

Data Security

Encryption at Rest

Encrypt sensitive data in database:

typescript
import crypto from 'crypto';

const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; // 32 bytes
const ALGORITHM = 'aes-256-gcm';

function encrypt(text: string): string {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);

  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
}

function decrypt(encrypted: string): string {
  const parts = encrypted.split(':');
  const iv = Buffer.from(parts[0], 'hex');
  const authTag = Buffer.from(parts[1], 'hex');
  const encryptedText = parts[2];

  const decipher = crypto.createDecipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
  decipher.setAuthTag(authTag);

  let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}

// Usage
await db.users.create({
  data: {
    email: user.email,
    sensitiveData: encrypt(user.sensitiveData)
  }
});

Database Security

bash
# PostgreSQL configuration
# /etc/postgresql/postgresql.conf

# Encrypt connections
ssl = on
ssl_cert_file = '/path/to/server.crt'
ssl_key_file = '/path/to/server.key'

# Strong authentication
password_encryption = scram-sha-256

# Connection limits
max_connections = 100

Backup Security

bash
# Encrypt database backups
pg_dump payindb | gzip | \
  openssl enc -aes-256-cbc -salt -pbkdf2 \
  -out backup_$(date +%Y%m%d).sql.gz.enc

# Store encrypted backups in multiple locations
aws s3 cp backup_*.enc s3://payinbackups/ --sse AES256

Incident Response

Preparation

  1. Create Incident Response Plan

    • Define roles and responsibilities
    • Document escalation procedures
    • Establish communication channels
    • Prepare runbooks for common scenarios
  2. Incident Response Team

    • Incident Commander
    • Technical Lead
    • Security Officer
    • Communications Lead

Detection

Monitor for security incidents:

typescript
// Detect suspicious patterns
const suspiciousPatterns = {
  rapidApiCalls: { threshold: 100, window: 60 }, // 100 calls/minute
  failedLogins: { threshold: 5, window: 300 },   // 5 failed/5min
  unusualIPs: { threshold: 10, window: 3600 },   // 10 different IPs/hour
  largeOrders: { threshold: 10000, currency: 'USDT' }
};

async function detectAnomalies() {
  // Check for rapid API calls from single IP
  const rapidCalls = await db.auditLogs.groupBy({
    by: ['ipAddress'],
    where: {
      createdAt: { gte: new Date(Date.now() - 60000) }
    },
    _count: true,
    having: {
      _count: { gte: 100 }
    }
  });

  if (rapidCalls.length > 0) {
    await triggerIncident({
      type: 'api_abuse',
      details: rapidCalls,
      severity: 'medium'
    });
  }
}

Response

When incident detected:

  1. Assess - Determine scope and severity
  2. Contain - Stop ongoing attack
  3. Eradicate - Remove threat
  4. Recover - Restore normal operations
  5. Review - Post-incident analysis

Common Actions:

typescript
// Revoke compromised API key
await revokeApiKey(compromisedKeyId);

// Block suspicious IP
await addIpToBlocklist(suspiciousIp);

// Rotate webhook secret
await rotateWebhookSecret(endpointId);

// Notify affected users
await notifyUsers({
  type: 'security_incident',
  message: 'We detected suspicious activity...',
  actions: ['Reset your password', 'Review recent transactions']
});

Recovery

After incident:

  1. Document incident timeline
  2. Identify root cause
  3. Implement fixes
  4. Update security procedures
  5. Share lessons learned with team

Security Checklist

Development Phase

  • [ ] API keys stored in environment variables
  • [ ] Webhook signature verification implemented
  • [ ] All endpoints use HTTPS
  • [ ] Input validation on all user inputs
  • [ ] Error messages don't leak sensitive information
  • [ ] No secrets in code or version control
  • [ ] Dependency vulnerabilities checked (npm audit)
  • [ ] Code reviewed for security issues

Testing Phase

  • [ ] Security testing performed
  • [ ] Testnet testing completed
  • [ ] Webhook signature verification tested
  • [ ] Payment verification logic tested
  • [ ] Error scenarios tested
  • [ ] Rate limiting tested
  • [ ] Penetration testing (if high-value)

Pre-Production

  • [ ] API keys rotated from testnet keys
  • [ ] Webhook secrets configured
  • [ ] HTTPS certificates valid
  • [ ] Firewall rules configured
  • [ ] Database encrypted
  • [ ] Backups configured and tested
  • [ ] Monitoring and alerts set up
  • [ ] Incident response plan documented
  • [ ] Team trained on security procedures

Production

  • [ ] Regular key rotation schedule (90 days)
  • [ ] Active monitoring and alerting
  • [ ] Regular security audits
  • [ ] Backup procedures tested monthly
  • [ ] Incident response drills quarterly
  • [ ] Dependencies updated regularly
  • [ ] Audit logs reviewed weekly
  • [ ] Access control reviewed quarterly

Compliance (If Applicable)

  • [ ] GDPR compliance (EU users)
  • [ ] CCPA compliance (California users)
  • [ ] PCI DSS compliance (if handling cards)
  • [ ] KYC/AML procedures (if required)
  • [ ] Data retention policies
  • [ ] Privacy policy published
  • [ ] Terms of service published

Common Vulnerabilities

API Abuse

Problem: Attacker makes excessive API calls

Prevention:

typescript
import rateLimit from 'express-rate-limit';

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/api/', apiLimiter);

SQL Injection

Problem: Attacker injects malicious SQL

Prevention:

typescript
// ❌ BAD: String concatenation
const query = `SELECT * FROM users WHERE email = '${userInput}'`;

// ✅ GOOD: Parameterized queries
const query = 'SELECT * FROM users WHERE email = $1';
const result = await db.query(query, [userInput]);

Cross-Site Scripting (XSS)

Problem: Attacker injects malicious scripts

Prevention:

typescript
import DOMPurify from 'isomorphic-dompurify';

// Sanitize user input
const cleanInput = DOMPurify.sanitize(userInput);

// Set Content-Security-Policy header
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'"
  );
  next();
});

Insecure Dependencies

Problem: Using packages with known vulnerabilities

Prevention:

bash
# Check for vulnerabilities
npm audit

# Fix vulnerabilities automatically
npm audit fix

# Check before installing
npm install --audit=true

# Use Snyk for continuous monitoring
npm install -g snyk
snyk test

Security Resources

Tools

  • Dependency Scanning: npm audit, Snyk, Dependabot
  • Secret Detection: GitGuardian, TruffleHog
  • Static Analysis: ESLint (with security plugins), SonarQube
  • Dynamic Analysis: OWASP ZAP, Burp Suite
  • Monitoring: Datadog, New Relic, Sentry

Learning Resources

Reporting Security Issues

If you discover a security vulnerability in PayIn:

📧 Email: security@payin.com

Include:

  • Description of the vulnerability
  • Steps to reproduce
  • Potential impact
  • Suggested fix (if any)

Please:

  • ✅ Report privately first
  • ✅ Give us reasonable time to fix (90 days)
  • ✅ Don't exploit the vulnerability
  • ❌ Don't disclose publicly until resolved

We appreciate responsible disclosure and may recognize security researchers who help improve PayIn's security.

Next Steps

Essential Security Guides

Setup Guides


Remember: Security is not a one-time setup. It's an ongoing process of monitoring, updating, and improving your defenses. Stay vigilant!

Released under the MIT License.