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
This guide walks through integrating CrediBill payment infrastructure into your SaaS application. We’ll cover:
- Setting up payment providers
- Creating subscriptions
- Handling webhooks
- Managing customer payments
- 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
Step 1: Get Provider Credentials
Follow the Provider Setup Guide to obtain credentials for your chosen provider.
Step 2: Add to CrediBill Dashboard
- Go to Settings → Payment Providers
- Click Add Provider
- Select provider (Flutterwave, PawaPay, Pesapal, or DPO)
- Enter credentials obtained from provider
- Set environment (Test or Live)
- 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
}
Multi-Provider Setup (Recommended)
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": "customer@example.com",
"name": "Acme Corp",
"metadata": {
"customerId": "12345",
"industry": "software"
}
}'
Response:
{
"id": "cust_abc123def456",
"email": "customer@example.com",
"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": "customer@example.com",
"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.
Next.js (App Router)
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 });
}
# 1. Configure provider with test credentials
curl -X POST https://api.credibill.io/v1/payment-providers \
-H "Authorization: Bearer sk_test_abc" \
-H "Content-Type: application/json" \
-d '{"provider":"flutterwave","publicKey":"pk_test_...","secretKey":"sk_test_...","environment":"test","isPrimary":true}'
# 2. Create test customer
curl -X POST https://api.credibill.io/v1/customers \
-H "Authorization: Bearer sk_test_abc" \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","name":"Test Customer"}'
# 3. Create test subscription
curl -X POST https://api.credibill.io/v1/subscriptions \
-H "Authorization: Bearer sk_test_abc" \
-H "Content-Type: application/json" \
-d '{"customerId":"cust_test","planId":"plan_pro_monthly"}'
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:
- Go to Settings → Webhooks
- Update URL to:
https://your-ngrok-url.ngrok.io/webhooks/credibill
- 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
CrediBill Configuration
Code Review
Testing
Monitoring
Deployment
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:
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