Skip to main content

Security Guide

Best practices for securing your PayerScan Payment Gateway integration.

API Key Security

Never Expose Your API Key

Your API Key authenticates requests to the PayerScan API. It must never be visible to end users.

// ❌ WRONG - Calling API from frontend (API Key exposed to anyone)
fetch('https://api.payerscan.com/payment/crypto', {
headers: { 'x-api-key': 'EQnhBYpknGAP...' } // API Key exposed!
})

// ✅ CORRECT - Calling API from backend server
// Frontend calls your internal API
fetch('/api/create-payment', { body: { amount: 100 } })

// Your backend calls PayerScan (API Key stays on your server)
app.post('/api/create-payment', async (req, res) => {
const result = await fetch('https://api.payerscan.com/payment/crypto', {
headers: { 'x-api-key': process.env.PAYERSCAN_API_KEY }
})
res.json(result)
})

Store API Key Securely

# ✅ Use environment variables
PAYERSCAN_API_KEY=EQnhBYpknGAP...

# ❌ Don't hardcode in code
const API_KEY = 'EQnhBYpknGAP...' # DON'T DO THIS

Note: Never commit API Keys to version control (Git). Add .env to your .gitignore file.

Rotate API Key Periodically

  • Change API Key if you suspect it's been compromised
  • Go to Store management → Generate New API Key
  • Update the new API Key on your server immediately
  • The old API Key will be revoked immediately after generating a new one

Webhook Security

Use HTTPS

Always use HTTPS for your callback_url to encrypt data in transit and prevent man-in-the-middle attacks.

// ✅ CORRECT - Use HTTPS
callback_url: 'https://your-server.com/webhook/payment'

// ❌ WRONG - Using HTTP (data not encrypted, vulnerable to interception)
callback_url: 'http://your-server.com/webhook/payment'

Verify Webhook Requests

Always verify that webhook requests genuinely come from PayerScan:

app.post('/webhook/payment', express.json(), async (req, res) => {
try {
const { merchant_id, api_key, trans_id, request_id, status, amount, transaction_hash } = req.body

// 1. Verify merchant_id (both completed and expired)
if (merchant_id !== process.env.PAYERSCAN_MERCHANT_ID) {
return res.status(401).json({ error: 'Invalid merchant' })
}

// 2. Verify api_key (completed only — expired does not include api_key)
if (status === 'completed' && api_key !== process.env.PAYERSCAN_API_KEY) {
return res.status(401).json({ error: 'Invalid API Key' })
}

// 3. Find order by request_id (if provided) or trans_id
let order = null
if (request_id) {
order = await db.orders.findOne({ request_id })
}
if (!order) {
order = await db.orders.findOne({ trans_id })
}
if (!order) {
return res.status(404).json({ error: 'Order not found' })
}

// 4. Only process orders in processable states (idempotent)
const processableStatuses = ['waiting', 'pending', 'processing']
if (!processableStatuses.includes(order.status)) {
return res.status(200).json({ message: 'Already processed' })
}

// 5. Verify amount matches (completed only — prevent tampering)
if (status === 'completed') {
if (parseFloat(amount) !== parseFloat(order.amount)) {
return res.status(400).json({ error: 'Amount mismatch' })
}
}

// 6. Check transaction_hash not already used (prevent replay)
if (status === 'completed') {
const existingTx = await db.orders.findOne({ transaction_hash })
if (existingTx) {
return res.status(409).json({ error: 'Transaction hash already used' })
}
}

// 7. Atomic update — include status filter to prevent race condition
if (status === 'completed') {
const result = await db.orders.update(
{ trans_id, status: { $in: processableStatuses } },
{ status: 'completed', transaction_hash }
)
if (result.modifiedCount === 0) {
return res.status(200).json({ message: 'Already processed' })
}
} else if (status === 'expired') {
const result = await db.orders.update(
{ trans_id, status: { $in: processableStatuses } },
{ status: 'expired' }
)
if (result.modifiedCount === 0) {
return res.status(200).json({ message: 'Already processed' })
}
}

res.status(200).json({ success: true })
} catch (error) {
console.error('Webhook processing error:', error)
res.status(500).json({ error: 'Internal server error' })
}
})

Handle Idempotently

Webhooks can be retried up to 5 times. Ensure processing multiple times is safe:

// ✅ CORRECT - Only update orders in processable states
const processableStatuses = ['waiting', 'pending', 'processing']
if (!processableStatuses.includes(order.status)) {
return res.status(200).json({ message: 'Already processed' })
}

// ❌ WRONG - No check, could credit balance multiple times
await addBalance(user, amount) // If webhook sent twice → credited twice!

Data Security

Don't Log Sensitive Data

// ❌ WRONG - Logging API Key
console.log('Request with API Key:', req.headers['x-api-key'])

// ✅ CORRECT - Only log what's necessary
console.log('Payment created:', { trans_id, amount, status })

Validate Input

PayerScan validates input on the server side, but you should also validate on your end:

// Validate amount (must be positive, max 1,000,000 USD)
const amount = parseFloat(req.body.amount)
if (isNaN(amount) || amount <= 0 || amount > 1000000) {
return res.status(400).json({ error: 'Invalid amount' })
}

// Validate callback_url format
if (req.body.callback_url) {
try {
const url = new URL(req.body.callback_url)
if (url.protocol !== 'https:') {
console.warn('callback_url should use HTTPS for security')
}
} catch {
return res.status(400).json({ error: 'Invalid callback_url' })
}
}

PayerScan Server-Side Protections

PayerScan implements multiple layers of security on the server side:

  • SSRF Protection: The system validates all callback_url values before sending webhooks. Internal/private IPs and suspicious URLs are blocked automatically.
  • Rate Limiting: API endpoints are protected with both burst and sustained rate limits to prevent abuse. See Rate Limits for details.
  • Input Validation: All request parameters are validated using strict schemas (amount range, URL format, string length limits).
  • Webhook Timeout: Webhook callbacks have a 10-second timeout. If your server doesn't respond within 10 seconds, the attempt is marked as failed and will be retried.

Security Checklist

Before Go-Live

  • API Key stored in environment variables (not hardcoded)
  • API Key not exposed on frontend/client-side code
  • .env file added to .gitignore
  • Webhook endpoint uses HTTPS
  • Webhook verifies merchant_id for both completed and expired
  • Webhook verifies api_key for completed webhooks
  • Webhook finds order by request_id or trans_id
  • Webhook handling is idempotent (no duplicate processing)
  • Webhook verifies amount matches order amount
  • Webhook checks transaction_hash not already used (prevent replay)
  • Webhook uses atomic update with status filter (prevent race condition)
  • No sensitive data logged (API Keys, private keys)
  • Input validation for amounts, URLs

Ongoing

  • Review access logs for webhook endpoint regularly
  • Monitor for unauthorized or suspicious webhook requests
  • Rotate API Key periodically or if compromised
  • Update dependencies with security patches
  • Test webhook endpoint resilience (responds within 10 seconds)

Security Incident Response

If API Key Is Compromised

  1. Immediately go to Store management → Generate New API Key
  2. Update the new API Key on all servers
  3. Review recent API logs for unauthorized requests
  4. Contact support if suspicious transactions are found

If Fake Webhooks Are Detected

  1. Verify your webhook handler checks merchant_id and api_key
  2. Check logs for amount mismatches or duplicate transaction_hash attempts
  3. Block the source IP (if identifiable)
  4. Add IP allowlisting if possible
  5. Contact support to report the incident

Contact Support

For urgent security issues, contact us immediately: