Define four access tiers for payment operations: (1) Developer — can read transaction logs, deploy code, rotate API keys in non-production; (2) Finance/Ops — can read transaction history and download reports, cannot initiate transfers; (3) Transfer Approver — can authorize transfers but requires a second approval for amounts above a threshold; (4) Auditor — read-only access to all payment records, no ability to modify. Enforce these with database-level row permissions and separate Paystack dashboard team roles.
| Role | Can Do | Cannot Do |
|---|---|---|
| Developer | Deploy code, read logs, use test API keys, rotate keys in staging | Initiate production transfers, access live customer PII outside audit context |
| Finance / Ops | View transaction history, download CSV reports, view refund status | Trigger refunds independently, initiate transfers, modify API keys |
| Transfer Approver | Approve transfers initiated by the system, view transfer queue | Approve their own transfers, bypass dual-approval on amounts above threshold |
| Auditor | Read all payment records, download audit logs, view access history | Modify any records, trigger any API calls |
| Admin | All of the above, plus manage team members and API key rotation | Should still require dual approval for large transfers |
// middleware/requirePaymentRole.js
function requirePaymentRole(...allowedRoles) {
return (req, res, next) => {
var userRole = req.user?.payment_role; // Set from your session/JWT
if (!userRole || !allowedRoles.includes(userRole)) {
return res.status(403).json({ error: 'Insufficient permissions for this payment operation' });
}
next();
};
}
// Applying roles to payment routes
// Transaction list — Finance and above can see
app.get('/api/transactions', requirePaymentRole('finance', 'transfer_approver', 'admin', 'auditor'), listTransactions);
// Initiate transfer — system only (no human-facing endpoint), or admin with audit log
app.post('/api/transfers/initiate', requirePaymentRole('admin'), async (req, res) => {
var { amount, recipient } = req.body;
// Log the initiating user for audit trail
await db.audit_log.create({
user_id: req.user.id,
action: 'transfer_initiated',
amount,
recipient,
timestamp: new Date(),
});
// Proceed with transfer...
});
// Approve transfer — transfer_approver role, but not the same user who initiated
app.post('/api/transfers/:id/approve', requirePaymentRole('transfer_approver', 'admin'), async (req, res) => {
var transfer = await db.transfers.findById(req.params.id);
if (transfer.initiated_by === req.user.id) {
return res.status(403).json({ error: 'Cannot approve your own transfer' });
}
// Proceed with approval...
});
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
When a team member with payment access leaves:
audit_log for transfers or data exports in the last 30 daysThis guide is part of the Paystack security guide.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course