Skip to main content

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 TypePrefixUsageSecurity
Secret Keycb_Server-to-server API callsKeep secret, never expose
Publishable Keypk_Client-side operationsSafe to expose in frontend
Webhook Secretwhsec_Verify webhook signaturesKeep 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:
  1. Go to Settings → API Keys
  2. Click Regenerate next to key
  3. Update your app with new key
  4. Delete old key
  5. 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:
  1. Timestamp validation: Reject webhooks older than 5 minutes
  2. Event ID deduplication: Track processed event IDs
  3. 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:
  1. Go to provider dashboard
  2. Generate new API credentials
  3. Update in CrediBill settings
  4. Rotate out old credentials
  5. 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

  1. Immediately revoke the exposed key/credential
  2. Generate new key/credential
  3. Update all applications with new credentials
  4. Review logs for unauthorized usage
  5. Monitor for suspicious activity
  6. Contact provider to revoke provider credentials if exposed
  7. Document the incident

If Payment Data Is Exposed

  1. Assess scope: Which data was exposed?
  2. Notify users: If PII/payment method exposed
  3. Contact CrediBill: Report the incident
  4. Enable monitoring: Watch for fraudulent activity
  5. Review logs: Understand how exposure occurred
  6. Implement fixes: Prevent recurrence

If Webhooks Are Being Spoofed

  1. Verify signature: Ensure proper verification
  2. Check logs: Look for pattern of invalid signatures
  3. Check IP source: See where invalid webhooks come from
  4. Block IP: Add to firewall if needed
  5. Rotate webhook secret: Generate new secret
  6. 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

  • All credentials in environment variables (not hardcoded)
  • API keys rotated (first time setup)
  • Webhook secret saved securely
  • HTTPS enabled on webhook endpoint
  • SSL certificate valid (not self-signed)

Code

  • Webhook signature verification implemented
  • Timing-safe comparison used
  • Timestamp validation implemented
  • Replay attack prevention in place
  • Idempotency keys used for payments
  • Error messages don’t expose credentials
  • Logging doesn’t log sensitive data

Monitoring

  • Invalid webhook attempts logged
  • Failed API auth attempts monitored
  • Security alerts configured
  • Audit logs enabled
  • Payment anomalies monitored

Operational

  • Key rotation plan documented
  • Incident response plan documented
  • Team training completed
  • Backup credentials stored securely
  • Regular security reviews scheduled

Resources