Skip to main content

Overview

This guide walks through integrating CrediBill payment infrastructure into your SaaS application. We’ll cover:
  1. Setting up payment providers
  2. Creating subscriptions
  3. Handling webhooks
  4. Managing customer payments
  5. Monitoring and debugging

Prerequisites

  • Active CrediBill account
  • Secret API key from dashboard
  • Payment provider account (Flutterwave, PawaPay, Pesapal, or DPO)
  • Backend server (Node.js, Python, etc.)
  • Basic understanding of webhooks

1. Setup

Store API Key in Environment

# .env.local
CREDIBILL_SECRET_KEY=cb_live_abc123def456
CREDIBILL_WEBHOOK_SECRET=whsec_xyz789

API Base URL

https://giant-goldfish-922.convex.site
All requests must include your API key in the Authorization header:
Authorization: Bearer cb_live_abc123def456
Content-Type: application/json

2. Configure Payment Providers

Step 1: Get Provider Credentials

Follow the Provider Setup Guide to obtain credentials for your chosen provider.

Step 2: Add to CrediBill Dashboard

  1. Go to Settings → Payment Providers
  2. Click Add Provider
  3. Select provider (Flutterwave, PawaPay, Pesapal, or DPO)
  4. Enter credentials obtained from provider
  5. Set environment (Test or Live)
  6. Click Save & Test Connection

Step 3: Verify Connection

curl -X GET https://giant-goldfish-922.convex.site/api/payment-providers/provider_id_here \
  -H "Authorization: Bearer cb_live_abc123def456" \
  -H "Content-Type: application/json"
Response:
{
  "id": "provider_abc123",
  "provider": "flutterwave",
  "connectionStatus": "active",
  "isPrimary": true
}
Add providers via API:
# Primary provider (Flutterwave)
curl -X POST https://giant-goldfish-922.convex.site/api/payment-providers \
  -H "Authorization: Bearer cb_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "flutterwave",
    "publicKey": "pk_test_xyz",
    "secretKey": "cb_test_abc",
    "webhookSecret": "whsec_test_123",
    "environment": "live",
    "isPrimary": true
  }'

# Backup provider (PawaPay)
curl -X POST https://giant-goldfish-922.convex.site/api/payment-providers \
  -H "Authorization: Bearer cb_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "pawapay",
    "apiToken": "token_test_xyz",
    "apiUrl": "https://api.pawapay.cloud",
    "webhookSecret": "whsec_test_456",
    "environment": "live",
    "isPrimary": false
  }'

3. Create Customers

Basic Customer Creation

curl -X POST https://giant-goldfish-922.convex.site/api/customers \
  -H "Authorization: Bearer cb_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "name": "Acme Corp",
    "metadata": {
      "customerId": "12345",
      "industry": "software"
    }
  }'
Response:
{
  "id": "cust_abc123def456",
  "email": "[email protected]",
  "name": "Acme Corp",
  "createdAt": 1703510400
}

With Payment Method

curl -X POST https://giant-goldfish-922.convex.site/api/customers \
  -H "Authorization: Bearer cb_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "name": "Acme Corp",
    "paymentMethod": "mobile_money_mtn",
    "paymentDetails": {
      "phoneNumber": "+256772123456",
      "provider": "flutterwave"
    }
  }'

4. Create Subscriptions

Simple Subscription

curl -X POST https://giant-goldfish-922.convex.site/api/subscriptions \
  -H "Authorization: Bearer cb_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "cust_abc123def456",
    "planId": "plan_pro_monthly",
    "trialDays": 14
  }'
Response:
{
  "id": "sub_abc123def456",
  "customerId": "cust_abc123def456",
  "status": "trialing",
  "trialEndsAt": 1704201600,
  "currentPeriodStart": 1703596800,
  "currentPeriodEnd": 1706188800
}

With Usage-Based Pricing

# Create subscription for plan with usage-based component
curl -X POST https://giant-goldfish-922.convex.site/api/subscriptions \
  -H "Authorization: Bearer cb_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "cust_abc123def456",
    "planId": "plan_pro_usage"
  }'

# Report usage as it happens
curl -X POST https://giant-goldfish-922.convex.site/api/usage \
  -H "Authorization: Bearer cb_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriptionId": "sub_abc123def456",
    "metricName": "api_calls",
    "quantity": 150,
    "timestamp": 1703596800,
    "idempotencyKey": "your-unique-key"
  }'

Subscription Lifecycle

# Retrieve subscription
curl -X GET https://giant-goldfish-922.convex.site/api/subscriptions/sub_abc123 \
  -H "Authorization: Bearer cb_live_abc123def456"

# Update subscription (change plan)
curl -X PATCH https://giant-goldfish-922.convex.site/api/subscriptions/sub_abc123 \
  -H "Authorization: Bearer cb_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{"planId": "plan_enterprise_monthly"}'

# Cancel subscription
curl -X POST https://giant-goldfish-922.convex.site/api/subscriptions/sub_abc123/cancel \
  -H "Authorization: Bearer cb_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{"reason": "customer_request", "immediateEffect": true}'

5. Handle Webhooks

Set Up Webhook Endpoint

import express from "express";
import crypto from "crypto";

const app = express();

// Critical: Use raw body middleware
app.post(
  "/webhooks/credibill",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    try {
      // Step 1: Verify signature
      const signature = req.headers["x-credibill-signature"];
      const timestamp = req.headers["x-credibill-timestamp"];
      const rawBody = req.body.toString("utf-8");

      const signaturePayload = `${timestamp}.${rawBody}`;
      const expectedSignature = crypto
        .createHmac("sha256", process.env.CREDIBILL_WEBHOOK_SECRET)
        .update(signaturePayload)
        .digest("hex");

      let isValid = false;
      try {
        isValid = crypto.timingSafeEqual(
          Buffer.from(expectedSignature),
          Buffer.from(signature)
        );
      } catch (err) {
        isValid = false;
      }

      if (!isValid) {
        console.warn("❌ Invalid webhook signature");
        return res.status(401).json({ error: "Invalid signature" });
      }

      // Step 2: Verify timestamp (prevent replay attacks)
      const webhookTime = parseInt(timestamp);
      const now = Date.now();
      const ageMs = now - webhookTime;

      if (ageMs > 5 * 60 * 1000) {
        // 5 minutes
        console.warn("⚠️ Webhook too old:", ageMs);
        return res.status(400).json({ error: "Webhook too old" });
      }

      // Step 3: Acknowledge receipt immediately
      res.status(200).json({ success: true });

      // Step 4: Process webhook asynchronously
      const webhook = JSON.parse(rawBody);
      handleWebhookAsync(webhook).catch((err) => {
        console.error("❌ Webhook processing error:", err);
      });
    } catch (error) {
      console.error("❌ Webhook handling error:", error);
      res.status(500).json({ error: "Internal server error" });
    }
  }
);

async function handleWebhookAsync(webhook) {
  const { event, data } = webhook;

  // Handle idempotency
  const eventId = `${event}:${data.id}:${webhook.timestamp}`;
  const existingEvent = await db.processedEvents.findOne({ eventId });

  if (existingEvent) {
    console.log("✅ Webhook already processed:", eventId);
    return;
  }

  // Process based on event type
  switch (event) {
    case "payment.success":
      await handlePaymentSuccess(data);
      break;

    case "payment.failed":
      await handlePaymentFailed(data);
      break;

    case "subscription.activated":
      await handleSubscriptionActivated(data);
      break;

    case "subscription.past_due":
      await handleSubscriptionPastDue(data);
      break;

    case "subscription.canceled":
      await handleSubscriptionCanceled(data);
      break;

    default:
      console.log("⚠️ Unknown event type:", event);
  }

  // Mark as processed
  await db.processedEvents.create({
    eventId,
    processedAt: new Date(),
  });
}

Handle Payment Success

async function handlePaymentSuccess(data) {
  const {
    id: transactionId,
    customerId,
    subscriptionId,
    amount,
    currency,
  } = data;

  console.log("✅ Payment successful:", { transactionId, customerId, amount });

  // Update subscription in your database
  await db.subscription.update(subscriptionId, {
    status: "active",
    paidAt: new Date(),
  });

  // Grant feature access
  await enableFeatures(customerId);

  // Send confirmation email
  const customer = await db.customer.get(customerId);
  await sendEmail(customer.email, "payment_success", {
    amount,
    currency,
    date: new Date(),
    invoiceUrl: `https://credibill.tech/invoices/${data.invoiceId}`,
  });

  // Track metric for analytics
  analytics.track("payment_success", {
    customerId,
    subscriptionId,
    amount,
  });
}

Handle Payment Failure

async function handlePaymentFailed(data) {
  const {
    id: transactionId,
    customerId,
    subscriptionId,
    failureReason,
    attemptNumber,
  } = data;

  console.log("⚠️ Payment failed:", {
    transactionId,
    customerId,
    failureReason,
  });

  // Update subscription to past_due
  await db.subscription.update(subscriptionId, {
    status: "past_due",
    failedAttempts: attemptNumber,
    failedAt: new Date(),
  });

  // Send failure notification
  const customer = await db.customer.get(customerId);
  await sendEmail(customer.email, "payment_failed", {
    reason: failureReason,
    updateLink: `https://your-app.com/settings/payment`,
    retryInfo:
      attemptNumber < 3
        ? "We will retry automatically"
        : "Please update your payment method",
  });

  // Optionally disable access after multiple failures
  if (attemptNumber >= 3) {
    await disableFeatures(customerId);
    await sendEmail(customer.email, "subscription_suspended", {
      reason: "Payment failed after 3 retry attempts",
      resolution: "Update your payment method to reactivate",
    });
  }
}

Handle Subscription Events

async function handleSubscriptionActivated(data) {
  const { customerId, subscriptionId } = data;

  console.log("✅ Subscription activated:", { subscriptionId, customerId });

  // Mark subscription as active in your system
  await db.subscription.update(subscriptionId, {
    status: "active",
  });

  // Send welcome email
  const customer = await db.customer.get(customerId);
  await sendEmail(customer.email, "welcome", {
    name: customer.name,
    features: ["Feature 1", "Feature 2", "Feature 3"],
  });

  // Start tracking usage if applicable
  await db.usageTracking.create({
    customerId,
    subscriptionId,
    startedAt: new Date(),
  });
}

async function handleSubscriptionPastDue(data) {
  const { customerId, subscriptionId, failedPaymentAttempts } = data;

  console.log("⚠️ Subscription past due:", { subscriptionId, customerId });

  // Show payment failure notice
  await db.notification.create({
    customerId,
    type: "payment_failed",
    message: `Your recent payment failed. Please update your payment method.`,
    actionUrl: `https://your-app.com/settings/payment`,
  });
}

async function handleSubscriptionCanceled(data) {
  const { customerId, subscriptionId, cancelReason } = data;

  console.log("✅ Subscription canceled:", { subscriptionId, cancelReason });

  // Disable features immediately
  await disableFeatures(customerId);

  // Send cancellation confirmation
  const customer = await db.customer.get(customerId);
  await sendEmail(customer.email, "cancellation_confirmed", {
    reason: cancelReason,
    feedback: "We would love to hear your feedback",
  });

  // Archive customer data if applicable
  await archiveCustomerData(customerId);
}

6. Test Your Integration

Test Mode

Use provider test credentials to test without real payments. Prefer testing by calling the CrediBill API from your server (App Router) or using cURL.
// app/api/test/setup/route.ts
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  // Sample payload should include provider credentials and test customer details
  const body = await request.json();

  // 1. Add provider
  await fetch("https://api.credibill.io/v1/payment-providers", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.CREDIBILL_SECRET_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body.provider),
  });

  // 2. Create test customer
  const custRes = await fetch("https://api.credibill.io/v1/customers", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.CREDIBILL_SECRET_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body.customer),
  });
  const testCustomer = await custRes.json();

  // 3. Create subscription
  const subRes = await fetch("https://api.credibill.io/v1/subscriptions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.CREDIBILL_SECRET_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ customerId: testCustomer.id, planId: body.planId }),
  });

  const testSubscription = await subRes.json();

  return NextResponse.json({ testCustomer, testSubscription });
}

Test Webhooks Locally

Use ngrok to receive webhooks locally:
# In one terminal: Start your Node app
npm run dev

# In another terminal: Expose to internet
ngrok http 3000
Update webhook URL in CrediBill dashboard:
  1. Go to Settings → Webhooks
  2. Update URL to: https://your-ngrok-url.ngrok.io/webhooks/credibill
  3. Send test webhook from dashboard

Test Webhook Handling

// Manually test webhook handler
const testWebhook = {
  event: "payment.success",
  data: {
    id: "txn_test_123",
    customerId: "cust_test_456",
    amount: 50000,
    currency: "UGX",
  },
  timestamp: Date.now(),
};

// Call handler directly
await handleWebhookAsync(testWebhook);

7. Go Live Checklist

Provider Setup

  • Live merchant accounts created
  • Business verification completed
  • Live API credentials obtained
  • Webhook URLs configured in provider dashboards
  • Test payment successful with real provider
  • Webhook delivery confirmed

CrediBill Configuration

  • Primary provider configured in dashboard
  • Backup provider configured (optional but recommended)
  • Environment set to Live (not Test)
  • Webhook secret saved securely
  • Webhook URL points to production
  • HTTPS certificate valid (not self-signed)

Code Review

  • Webhook signature verification implemented
  • Timing-safe comparison used
  • Timestamp validation implemented
  • Idempotency checks in place
  • No credentials hardcoded
  • All secrets in environment variables
  • Error messages don’t expose credentials
  • Logging doesn’t log sensitive data

Testing

  • End-to-end payment test successful
  • Failed payment handling tested
  • Webhook delivery tested
  • Signature verification works
  • Idempotency handling works
  • Multiple payment methods tested

Monitoring

  • Error alerts configured
  • Payment success metrics tracked
  • Webhook delivery monitored
  • Failed payments monitored
  • Unusual activity alerts set up
  • Daily reconciliation process established

Deployment

  • Code deployed to production
  • Configuration deployed (env vars)
  • Webhooks configured in production
  • First payment tested end-to-end
  • Monitor logs for first 24 hours
  • Team on standby for issues

8. Common Integration Issues

Issue: Webhook Signature Verification Fails

Cause: Using parsed JSON body instead of raw body Solution:
// ❌ Wrong
app.use(express.json()); // This parses body
app.post("/webhook", (req, res) => {
  const body = JSON.stringify(req.body); // Different bytes!
});

// ✅ Correct
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const body = req.body.toString("utf-8"); // Original bytes
});

Issue: Webhooks Not Received

Checklist:
  • Webhook URL is publicly accessible (not localhost)
  • HTTPS certificate is valid
  • Firewall allows traffic from CrediBill
  • Endpoint returns 200 OK
  • Endpoint responds within 10 seconds
  • Check webhook logs in CrediBill dashboard

Issue: Payment Stuck in Pending

Cause: Provider API slow or webhook delivery delayed Solution:
  • Wait 5-10 minutes (mobile money can be slow)
  • Check provider dashboard for payment status
  • Check CrediBill webhook logs for delivery attempts
  • Contact provider support if stuck > 30 minutes

Issue: Customer Charged Multiple Times

Cause: Duplicate payment initiation or missing idempotency Solution:
  • Implement idempotency keys (see code above)
  • Check webhook idempotency handling
  • Use database unique constraints on transaction IDs
  • Check for double form submissions in UI

Next Steps