Documentation Index
Fetch the complete documentation index at: https://docs.credibill.tech/docs/llms.txt
Use this file to discover all available pages before exploring further.
Overview
CrediBill implements production-grade security measures. This guide explains the security features and best practices for integrating with CrediBill.
Authentication & API Keys
API Key Types
CrediBill uses different API keys for different purposes:
| Key Type | Prefix | Usage | Security |
|---|
| Secret Key | cb_ | Server-to-server API calls | Keep secret, never expose |
| Publishable Key | pk_ | Client-side operations | Safe to expose in frontend |
| Webhook Secret | whsec_ | Verify webhook signatures | Keep secret, rotate periodically |
API Key Security
Never commit API keys to version control:
# ❌ Bad
export CREDIBILL_SECRET_KEY="cb_live_abc123def456" # Don't commit!
# ✅ Good
# .env.local (git-ignored)
CREDIBILL_SECRET_KEY=cb_live_abc123def456
# .env.example (committed)
CREDIBILL_SECRET_KEY=your_secret_key_here
Rotate keys regularly:
- Go to Settings → API Keys
- Click Regenerate next to key
- Update your app with new key
- Delete old key
- Monitor for failed requests
Key rotation schedule:
- After accidental exposure: Immediately
- Regular rotation: Quarterly
- After employee departure: Immediately
- After suspected compromise: Immediately
Using API Keys
# ✅ Correct: API key in Authorization header
curl -X POST https://giant-goldfish-922.convex.site/api/payments \
-H "Authorization: Bearer cb_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"customerId": "cust_123",
"amount": 50000,
"currency": "UGX"
}'
Never expose secret keys:
// ❌ Wrong: Exposing secret key in frontend code
const apiKey = "cb_live_abc123"; // EXPOSED!
fetch("https://giant-goldfish-922.convex.site/api/payments", {
headers: { Authorization: `Bearer ${apiKey}` },
});
// ❌ Wrong: Logging secret key
console.log("API Key:", process.env.CREDIBILL_SECRET_KEY); // LOGS CREDENTIALS!
// ❌ Wrong: Sending secret key to frontend
res.json({ apiKey: process.env.CREDIBILL_SECRET_KEY }); // EXPOSED!
// ✅ Correct: Keep secret key on server only
// Call CrediBill API only from your backend
app.post("/api/payments", (req, res) => {
// Only backend knows the secret key
// Frontend never sees it
});
Webhook Security
Signature Verification
All webhooks must be verified. This prevents attackers from forging webhook events.
Verification Implementation
import crypto from "crypto";
import express from "express";
const app = express();
// Critical: Use raw body, not parsed JSON
app.post(
"/webhooks/credibill",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-credibill-signature"];
const timestamp = req.headers["x-credibill-timestamp"];
const rawBody = req.body.toString("utf-8");
// Step 1: Construct signature payload
const signaturePayload = `${timestamp}.${rawBody}`;
// Step 2: Compute expected signature
const webhookSecret = process.env.CREDIBILL_WEBHOOK_SECRET;
const expectedSignature = crypto
.createHmac("sha256", webhookSecret)
.update(signaturePayload)
.digest("hex");
// Step 3: Timing-safe comparison
let isValid = false;
try {
isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
} catch (err) {
isValid = false;
}
if (!isValid) {
console.error("Invalid webhook signature");
return res.status(401).json({ error: "Unauthorized" });
}
// Step 4: Verify timestamp
const webhookTime = parseInt(timestamp);
const now = Date.now();
const ageMs = now - webhookTime;
if (ageMs > 5 * 60 * 1000) {
// 5 minutes
console.error("Webhook too old:", ageMs);
return res.status(400).json({ error: "Webhook too old" });
}
if (ageMs < -60 * 1000) {
// 1 minute in future
console.error("Webhook timestamp in future");
return res.status(400).json({ error: "Clock skew" });
}
// Step 5: Acknowledge receipt immediately
res.status(200).json({ success: true });
// Step 6: Process webhook asynchronously
const webhook = JSON.parse(rawBody);
handleWebhookAsync(webhook).catch((err) => {
console.error("Webhook processing error:", err);
});
}
);
async function handleWebhookAsync(webhook) {
// Process webhook here
}
Common Signature Verification Mistakes
// ❌ Wrong: Using parsed JSON body
app.post("/webhook", express.json(), (req, res) => {
// req.body is already parsed, losing original raw bytes
const rawBody = JSON.stringify(req.body); // WRONG! Different bytes
const signature = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
// Will never match because bytes are different!
});
// ❌ Wrong: Storing raw body without encoding
const rawBody = req.body; // If Buffer, needs .toString()
// ❌ Wrong: Not doing timing-safe comparison
if (expectedSignature === signature) {
// TIMING ATTACK VULNERABLE!
// This comparison time varies based on mismatch position
}
// ✅ Correct: Using raw body middleware and timing-safe comparison
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf-8");
// ... compute signature ...
let isValid = false;
try {
isValid = crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
} catch (err) {
isValid = false;
}
});
Replay Attack Prevention
CrediBill includes built-in replay attack prevention through:
- Timestamp validation: Reject webhooks older than 5 minutes
- Event ID deduplication: Track processed event IDs
- Nonce tracking: Prevent reusing old webhooks
Implementation:
// Check timestamp (already done in signature verification)
const ageMs = Date.now() - parseInt(timestamp);
if (ageMs > 5 * 60 * 1000) {
return res.status(400).json({ error: "Webhook too old" });
}
// Deduplicate event IDs
const eventId = `${webhook.event}:${webhook.data.id}:${webhook.timestamp}`;
const existingEvent = await db.processedEvents.findOne({ eventId });
if (existingEvent) {
console.log("Duplicate webhook, ignoring:", eventId);
return; // Silently ignore duplicate
}
// Process webhook
await processWebhook(webhook);
// Mark as processed
await db.processedEvents.create({
eventId,
processedAt: new Date(),
});
Credential Management
Encrypted Storage
All payment provider credentials are encrypted using AES-256-GCM:
// How CrediBill stores credentials internally
function encryptCredential(plaintext, appId, masterKey) {
// Derive app-specific key from app ID
const appKey = crypto.createHmac("sha256", masterKey).update(appId).digest();
// Generate random IV
const iv = crypto.randomBytes(16);
// Create cipher
const cipher = crypto.createCipheriv("aes-256-gcm", appKey, iv);
// Encrypt credential
const encrypted =
cipher.update(plaintext, "utf-8", "hex") + cipher.final("hex");
// Get authentication tag
const authTag = cipher.getAuthTag();
// Return: iv:encrypted:authTag
return `${iv.toString("hex")}:${encrypted}:${authTag.toString("hex")}`;
}
Best Practices for Credentials
Never log credentials:
// ❌ Wrong
console.log("Provider credentials:", credentials);
console.log("Secret key:", secretKey);
// ✅ Correct
console.log("Provider:", credentials.provider);
console.log("Environment:", credentials.environment);
// Don't log sensitive values
Don’t expose in responses:
// ❌ Wrong
res.json({
provider: "flutterwave",
publicKey: "FLWPUBK_TEST-xxx",
secretKey: "FLWSECK_TEST-xxx", // EXPOSED!
});
// ✅ Correct
res.json({
provider: "flutterwave",
configured: true,
environment: "test",
// Don't include credentials in response
});
Rotate credentials periodically:
- Go to provider dashboard
- Generate new API credentials
- Update in CrediBill settings
- Rotate out old credentials
- Delete old credentials from provider
Transaction Security
Race Condition Prevention
CrediBill prevents race conditions where multiple webhooks update the same transaction:
// Terminal state guard
async function updateTransactionStatus(transactionId, newStatus) {
const transaction = await db.paymentTransactions.get(transactionId);
// Never overwrite terminal states
const terminalStates = ["success", "canceled", "refunded"];
if (terminalStates.includes(transaction.status)) {
throw new Error("Cannot update transaction in terminal state");
}
// Atomic update
await db.paymentTransactions.patch(transactionId, {
status: newStatus,
completedAt: Date.now(),
});
}
Idempotency
Every payment operation should be idempotent. Include an Idempotency-Key header on server-side requests.
# Example: create payment with idempotency key (cURL)
curl -X POST https://api.credibill.io/v1/payments \
-H "Authorization: Bearer sk_live_abc123def456" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: your-unique-idempotency-key" \
-d '{ "customerId": "cust_123", "amount": 50000, "currency": "UGX" }'
// Server-side (App Router) example using fetch
const res = await fetch("https://api.credibill.io/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CREDIBILL_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({
customerId: "cust_123",
amount: 50000,
currency: "UGX",
}),
});
// The same idempotency key ensures retries do not create duplicate payments
Data Protection
Data Retention
CrediBill retains data according to:
- Transaction records: 7 years (regulatory requirement)
- Webhook logs: 90 days
- Customer data: Until account deletion
- Payment method tokens: Until removed by customer
Delete sensitive data:
# Delete customer data via API (server-side)
curl -X DELETE https://api.credibill.io/v1/customers/cust_123 \
-H "Authorization: Bearer sk_live_abc123def456"
# All associated payment data is also deleted (where applicable)
PCI Compliance
CrediBill is PCI DSS compliant:
- ✅ Never stores raw card numbers
- ✅ Encrypted transmission (TLS 1.2+)
- ✅ Secure credential storage (AES-256-GCM)
- ✅ Access logging and monitoring
- ✅ Regular penetration testing
Your responsibility:
- ✅ Never accept raw card data on your server
- ✅ Use CrediBill’s tokenization for cards
- ✅ Don’t log or store card numbers
- ✅ Use HTTPS for all communication
- ✅ Validate SSL certificates
Network Security
TLS/HTTPS
All CrediBill endpoints require HTTPS. Verify TLS by making requests over https:// and ensuring your server enforces HTTPS for webhook endpoints.
# Check connectivity and TLS by making an HTTPS request
curl -I https://api.credibill.io/v1/ \
-H "Authorization: Bearer $CREDIBILL_SECRET_KEY"
Webhook endpoints must be HTTPS:
# ❌ Wrong: HTTP webhook will be rejected
# Input: http://your-app.com/webhooks/credibill
# ✅ Correct: HTTPS webhook
# Input: https://your-app.com/webhooks/credibill
# Ensure your server accepts only HTTPS
TLS Certificate Validation
// ✅ Correct: TLS validation enabled by default
import https from "https";
const options = {
hostname: "api.credibill.io",
port: 443,
rejectUnauthorized: true, // Validate certificate
};
https.get(options, (res) => {
// Certificate is validated automatically
});
// ❌ Wrong: Disabling certificate validation
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; // INSECURE!
Monitoring & Alerting
Security Events to Monitor
// Log security-relevant events
async function logSecurityEvent(type, details) {
await db.securityLogs.create({
type,
details,
timestamp: Date.now(),
userId: currentUser?.id,
ipAddress: req.ip,
userAgent: req.headers["user-agent"],
});
// Alert on critical events
if (["unauthorized_access", "credential_compromise"].includes(type)) {
await alertSecurityTeam(type, details);
}
}
// Monitor these events:
// - Invalid webhook signatures
// - Multiple failed API authentications
// - Credential access from unusual locations
// - Large payment amounts
// - Unusual payment patterns
// - Failed payment retries
Audit Logging
// Log all sensitive operations
async function logAuditEvent(action, subject, changes) {
await db.auditLogs.create({
action,
subject,
changes,
timestamp: Date.now(),
userId: currentUser?.id,
ipAddress: req.ip,
userAgent: req.headers["user-agent"],
});
}
// Audit log examples:
await logAuditEvent("credential_created", "payment_provider", {
provider: "flutterwave",
environment: "live",
});
await logAuditEvent("credential_rotated", "api_key", {
oldKeyId: "...",
newKeyId: "...",
});
await logAuditEvent("payment_initiated", "transaction", {
customerId: "cust_123",
amount: 50000,
currency: "UGX",
});
Access Control
Role-Based Access
Implement role-based access control:
// Admin: Full access
if (user.role === "admin") {
// Can view all payments, credentials, users
}
// Finance: View only, no modifications
if (user.role === "finance") {
// Can view payments, invoices
// Cannot modify credentials, delete data
}
// Developer: API access
if (user.role === "developer") {
// Can create/view API keys
// Can configure webhooks
// Cannot access payment data
}
// Support: Limited customer support
if (user.role === "support") {
// Can view customer subscriptions
// Cannot view payment methods
// Cannot modify configurations
}
API Key Scoping
// Some API keys should have limited scope:
// Full access key (keep secure)
const fullAccessKey = "sk_live_abc123def456";
// Read-only key (can expose in logs)
const readOnlyKey = "sk_live_ro_xyz789abc123";
// Webhook-specific key (limited to webhooks)
const webhookKey = "sk_live_wh_def456ghi789";
// Each key has:
// - Specific permissions (read, write, delete)
// - Resource restrictions (payments only, webhooks only)
// - IP whitelisting (optional)
// - Rate limits
Incident Response
If Credentials Are Compromised
- Immediately revoke the exposed key/credential
- Generate new key/credential
- Update all applications with new credentials
- Review logs for unauthorized usage
- Monitor for suspicious activity
- Contact provider to revoke provider credentials if exposed
- Document the incident
If Payment Data Is Exposed
- Assess scope: Which data was exposed?
- Notify users: If PII/payment method exposed
- Contact CrediBill: Report the incident
- Enable monitoring: Watch for fraudulent activity
- Review logs: Understand how exposure occurred
- Implement fixes: Prevent recurrence
If Webhooks Are Being Spoofed
- Verify signature: Ensure proper verification
- Check logs: Look for pattern of invalid signatures
- Check IP source: See where invalid webhooks come from
- Block IP: Add to firewall if needed
- Rotate webhook secret: Generate new secret
- Update endpoint: If verification code has bug
Compliance
Supported Standards
CrediBill complies with:
- PCI DSS 3.2.1: Payment Card Industry Data Security Standard
- GDPR: General Data Protection Regulation (EU)
- CCPA: California Consumer Privacy Act
- HIPAA: Health Insurance Portability (if applicable)
- SOC 2 Type II: Service Organization Control
Your Compliance Responsibilities
Privacy:
- ✅ Collect customer data with consent
- ✅ Provide privacy policy
- ✅ Allow data deletion (GDPR right to be forgotten)
- ✅ Handle data breaches
- ✅ Keep PII secure
PCI:
- ✅ Never store raw card numbers
- ✅ Use tokenization for cards
- ✅ Maintain HTTPS
- ✅ Validate SSL certificates
- ✅ Log and monitor access
Financial:
- ✅ Maintain transaction records (7 years)
- ✅ Keep audit logs
- ✅ Reconcile with provider regularly
- ✅ Report suspiciously large transactions
- ✅ Comply with local regulations
Security Checklist
Before going to production:
Setup
Code
Monitoring
Operational
Resources