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
.envto your.gitignorefile.
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_urlvalues 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
-
.envfile added to.gitignore - Webhook endpoint uses HTTPS
- Webhook verifies
merchant_idfor both completed and expired - Webhook verifies
api_keyfor completed webhooks - Webhook finds order by
request_idortrans_id - Webhook handling is idempotent (no duplicate processing)
- Webhook verifies
amountmatches order amount - Webhook checks
transaction_hashnot 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
- Immediately go to Store management → Generate New API Key
- Update the new API Key on all servers
- Review recent API logs for unauthorized requests
- Contact support if suspicious transactions are found
If Fake Webhooks Are Detected
- Verify your webhook handler checks
merchant_idandapi_key - Check logs for
amountmismatches or duplicatetransaction_hashattempts - Block the source IP (if identifiable)
- Add IP allowlisting if possible
- Contact support to report the incident
Contact Support
For urgent security issues, contact us immediately:
- Email: payerscan@gmail.com