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.
Troubleshooting
Payment Issues
Payment Stuck in “Pending”
Symptoms: Transaction created but status never changes to success/failed
Common causes:
- Mobile money delays: MMOs can take 1-5 minutes
- Webhook delivery failure: Provider sent webhook but we didn’t receive it
- Provider API error: Provider accepted payment but network error on response
Solution:
Next.js (App Router)
cURL
// app/api/payments/[id]/route.ts
import { NextResponse } from "next/server";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const id = params.id;
const res = await fetch(
`https://giant-goldfish-922.convex.site/api/payments/${id}`,
{
headers: { Authorization: `Bearer ${process.env.CREDIBILL_SECRET_KEY}` },
}
);
return NextResponse.json(await res.json(), { status: res.status });
}
// app/api/webhook-logs/route.ts (example)
export async function POST(request: Request) {
const body = await request.json();
const res = await fetch(
"https://giant-goldfish-922.convex.site/api/webhook-logs",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CREDIBILL_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}
);
return NextResponse.json(await res.json(), { status: res.status });
}
# Check transaction status
curl -X GET https://giant-goldfish-922.convex.site/api/payments/txn_abc123 \
-H "Authorization: Bearer ${CREDIBILL_SECRET_KEY}"
# List webhook logs filtered by payment transaction
curl -X GET "https://giant-goldfish-922.convex.site/api/webhook-logs?paymentTransactionId=txn_abc123" \
-H "Authorization: Bearer ${CREDIBILL_SECRET_KEY}"
If webhook never delivered: 1) Check provider dashboard for successful payment 2) Contact provider support 3) Manually update transaction status as workaround.
Next steps:
- Wait 5-10 minutes for MMO confirmation
- Check provider dashboard for payment status
- Check CrediBill webhook logs for delivery attempts
- If payment confirmed in provider but not in CrediBill, contact support
Payment Declined
Symptoms: Payment fails immediately with “declined” status
Common causes:
- Insufficient funds: Customer doesn’t have balance
- Invalid payment method: Phone number or card incorrect
- Account blocked: Account frozen by provider
- Daily limit reached: Customer exceeded daily transaction limit
Solution:
Next.js (App Router)
cURL
// app/api/payments/[id]/route.ts
// (GET handler shown earlier)
// Use that endpoint to inspect failure fields from the payment object
# Inspect failure details
curl -X GET https://giant-goldfish-922.convex.site/api/payments/txn_abc123 \
-H "Authorization: Bearer ${CREDIBILL_SECRET_KEY}" | jq .failureReason,.failureCode,.providerResponse
Common failure codes include: INSUFFICIENT_FUNDS, INVALID_ACCOUNT, DECLINED, DAILY_LIMIT, ACCOUNT_BLOCKED.
Customer actions:
- Ensure sufficient funds in mobile money wallet
- Verify correct phone number
- Check account not blocked by provider
- Retry with different payment method
Multiple Charges for Single Payment
Symptoms: Customer charged multiple times for one subscription
Causes:
- Missing idempotency: Payment initiated multiple times
- Client-side double submission: Form submitted twice
- Webhook retry processing: Same webhook processed multiple times
Solution:
// 1. Use an Idempotency-Key header for server-side payment creation
const idempotencyKey = crypto.randomUUID();
// Server-side (Next.js App Router)
// app/api/payments/route.ts (POST)
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({ subscriptionId: "sub_abc123", amount: 50000 }),
});
// 2. Prevent double-submission in UI
let isSubmitting = false;
async function submitPayment() {
if (isSubmitting) return;
isSubmitting = true;
try {
await fetch("/api/payments", {
method: "POST",
body: JSON.stringify({
/* ... */
}),
});
} finally {
isSubmitting = false;
}
}
// 3. Webhook idempotency (database sketch)
const eventId = `${webhook.event}:${webhook.data.id}:${webhook.timestamp}`;
const existing = await db.processedEvents.findOne({ where: { eventId } });
if (existing) return; // already processed
await db.processedEvents.create({ data: { eventId, processedAt: new Date() } });
// Process webhook safely here
Webhook Issues
Webhooks Not Received
Symptoms: Webhook endpoint configured but never receives calls
Checklist:
Solution:
// 1. Test endpoint manually
curl -X POST https://your-app.com/webhooks/credibill \
-H "Content-Type: application/json" \
-d '{"event":"test","data":{}}'
// Should return 200
// 2. Check webhook logs in CrediBill dashboard
// Settings → Webhooks → Logs
// 3. Enable verbose logging
app.post('/webhooks/credibill', (req, res) => {
console.log('Webhook received:');
console.log(' Headers:', req.headers);
console.log(' Body size:', req.body?.length);
console.log(' Signature:', req.headers['x-credibill-signature']?.substring(0, 10) + '...');
res.status(200).json({ success: true });
});
// 4. Check firewall/WAF logs
// Look for CrediBill IPs being blocked
// 5. If still not receiving, contact support with:
// - Webhook URL
// - Logs from your endpoint
// - Logs from CrediBill dashboard
Webhook Signature Verification Fails
Symptoms: All webhooks rejected with “Invalid signature” error
Checklist:
Solution:
// ❌ Common mistake: Using parsed body
app.use(express.json()); // Parses body, loses original bytes
app.post("/webhook", (req, res) => {
const body = JSON.stringify(req.body); // Different bytes!
// Signature verification will fail
});
// ✅ Correct: Using raw body
app.post(
"/webhook",
express.raw({ type: "application/json" }), // Get raw bytes
(req, res) => {
const rawBody = req.body.toString("utf-8"); // Original bytes
const timestamp = req.headers["x-credibill-timestamp"];
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac("sha256", process.env.CREDIBILL_WEBHOOK_SECRET)
.update(payload)
.digest("hex");
// Timing-safe comparison
let isValid = false;
try {
isValid = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(req.headers["x-credibill-signature"])
);
} catch (err) {
isValid = false;
}
if (!isValid) {
console.log("Expected:", expected);
console.log("Received:", req.headers["x-credibill-signature"]);
console.log("Payload length:", payload.length);
console.log(
"Webhook secret:",
process.env.CREDIBILL_WEBHOOK_SECRET?.substring(0, 10) + "..."
);
}
}
);
Debug signature mismatch:
// Log signature details
console.log("Timestamp:", timestamp);
console.log("Body length:", rawBody.length);
console.log("Body (first 100 chars):", rawBody.substring(0, 100));
console.log("Webhook secret:", secret.substring(0, 10) + "...");
console.log("Expected signature:", expectedSignature);
console.log("Received signature:", receivedSignature);
// Regenerate signature step-by-step
const payload = `${timestamp}.${rawBody}`;
console.log("Payload to hash:", payload.substring(0, 100) + "...");
const hmac = crypto.createHmac("sha256", secret);
hmac.update(payload);
const computed = hmac.digest("hex");
console.log("Computed signature:", computed);
console.log("Do they match?", computed === receivedSignature);
Duplicate Webhooks
Symptoms: Same event processed multiple times
Cause: Network timeout causing CrediBill to retry webhook
Solution:
// Implement idempotency checking
async function handleWebhook(webhook) {
// Create unique event ID
const eventId = `${webhook.event}:${webhook.data.id}:${webhook.timestamp}`;
// Check if already processed
const existing = await db.processedWebhooks.findOne({ eventId });
if (existing) {
console.log("Duplicate webhook, ignoring:", eventId);
return; // Silently ignore
}
// Process webhook
try {
await processEvent(webhook);
// Mark as processed AFTER successful processing
await db.processedWebhooks.create({
eventId,
processedAt: new Date(),
});
} catch (error) {
console.error("Error processing webhook:", error);
// Don't mark as processed if it failed
throw error; // Let CrediBill retry
}
}
Credential & Configuration Issues
”Invalid Credentials” Error
Symptoms: Provider connection test fails immediately
Checklist:
Solution:
# 1. Verify credentials in provider dashboard
# Each provider has different credential format
# Flutterwave:
# Public Key: FLWPUBK_TEST-xxx or FLWPUBK-xxx
# Secret Key: FLWSECK_TEST-xxx or FLWSECK-xxx
# PawaPay:
# API Token: Bearer test_xxx or Bearer prod_xxx
# Pesapal:
# Consumer Key: xxx
# Consumer Secret: xxx
# DPO:
# Company Token: xxx
# Service Type: xxx
# 2. Check for extra spaces
echo "Credential: |${CREDENTIAL}|" # Pipes show any spaces
# 3. Try credential in provider's test endpoint
curl -H "Authorization: Bearer ${API_TOKEN}" https://api.provider.com/test
# 4. In CrediBill, click "Test Connection" after updating
Provider Connection Keeps Failing
Symptoms: Test connection fails repeatedly
Checklist:
Solution:
# 1. Check provider status via CrediBill API
curl -X GET https://api.credibill.io/v1/payment-providers/provider_id \
-H "Authorization: Bearer ${CREDIBILL_SECRET_KEY}" | jq '{connectionStatus: .connectionStatus, errorMessage: .errorMessage, lastTestedAt: .lastTestedAt}'
# 2. Test provider health directly (isolate)
curl -I https://api.provider.com/health
// Server-side check using fetch
const res = await fetch(
`https://api.credibill.io/v1/payment-providers/provider_id`,
{
headers: { Authorization: `Bearer ${process.env.CREDIBILL_SECRET_KEY}` },
}
);
const provider = await res.json();
console.log("Status:", provider.connectionStatus);
// 3. Check provider status page
// Flutterwave: https://status.flutterwave.com/
// PawaPay: Check their status
// Pesapal: https://status.pesapal.com/
// DPO: Check their status
// 4. Contact provider support with:
// - Error message
// - Credentials (masked)
// - Time of failure
// - Your account ID
### API & Integration Issues
#### "Unauthorized" Error on API Calls
**Symptoms:** API requests return 401 Unauthorized
**Causes:**
1. **Missing or invalid API key**
2. **Incorrect API key** (using publishable instead of secret)
3. **API key rotated** but app not updated
4. **Invalid Bearer token format**
**Solution:**
```bash
# ✅ Correct: Use your Secret Key in server-side requests
curl -X GET https://api.credibill.io/v1/customers \
-H "Authorization: Bearer ${CREDIBILL_SECRET_KEY}" \
-H "Content-Type: application/json"
# If you get 401 Unauthorized, check:
# 1. You're using a secret key (prefix sk_)
# 2. The key has not been rotated or revoked
# 3. The key is correctly copied into your environment
Server-side check (Next.js App Router example)
// app/api/health/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const res = await fetch("https://api.credibill.io/v1/customers", {
headers: { Authorization: `Bearer ${process.env.CREDIBILL_SECRET_KEY}` },
});
if (res.status === 401) {
return NextResponse.json(
{ ok: false, error: "Unauthorized" },
{ status: 401 }
);
}
return NextResponse.json({ ok: true });
}
Troubleshooting tips
-
Ensure the secret key is set in server environment variables, not in frontend code.
-
Use cURL or a server-side route to test keys.
-
Verify the key prefix is
sk_ (secret keys) not pk_ (publishable keys).
console.error(“3. Key matches environment”);
}
}
### Database & Data Issues
#### Missing or Mismatched Data
**Symptoms:** Subscription created but not appearing in API
**Causes:**
1. **Using wrong environment** (test vs. live)
2. **Multi-tenant isolation**: Data in different app
3. **Database sync delay**: Eventual consistency
4. **Wrong pagination**: Data on different page
**Solution:**
```bash
# 1. Verify environment (check subscription)
curl -X GET https://api.credibill.io/v1/subscriptions/sub_abc123 \
-H "Authorization: Bearer ${CREDIBILL_SECRET_KEY}" | jq .createdAt
# 2. Check multi-tenant isolation (list apps)
curl -X GET https://api.credibill.io/v1/apps \
-H "Authorization: Bearer ${CREDIBILL_SECRET_KEY}" | jq '.data[] | {id, name}'
# 3. List subscriptions and find customer
curl -X GET "https://api.credibill.io/v1/subscriptions?limit=100" \
-H "Authorization: Bearer ${CREDIBILL_SECRET_KEY}" | jq '.data[] | select(.customerId=="cust_xyz")'
# 4. Handle eventual consistency: retry after a short delay
sleep 5
curl -X GET https://api.credibill.io/v1/subscriptions/sub_abc123 \
-H "Authorization: Bearer ${CREDIBILL_SECRET_KEY}"
// Example server-side check (App Router) using fetch
async function checkSubscription(id) {
const res = await fetch(`https://api.credibill.io/v1/subscriptions/${id}`, {
headers: { Authorization: `Bearer ${process.env.CREDIBILL_SECRET_KEY}` },
});
return await res.json();
}
Frequently Asked Questions
Billing & Pricing
Q: How much does CrediBill cost?
A: See pricing page. Typically a percentage of transaction amount plus per-transaction fee.
Q: Do you charge for failed payments?
A: No. We only charge for successful transactions.
Q: Can you help with invoicing?
A: Yes, CrediBill generates and sends invoices automatically. You can customize templates in dashboard.
Technical
Q: What happens if a provider API is down?
A: CrediBill failsover to backup provider if configured. Payment marked as pending and retried later.
Q: How long do payment retries continue?
A: Up to 3 automatic retries with exponential backoff. After 3 failures, subscription marked PAST_DUE.
Q: Can I use multiple providers simultaneously?
A: Yes. Configure primary and backup. Can also route by customer country/payment method.
Q: What happens if my webhook endpoint is down?
A: CrediBill retries webhooks up to 3 times with backoff. After 3 failures, marked as failed. Check webhook logs to manually retry.
Q: How long does a mobile money payment take?
A: Instant to 5 minutes typical. Some edge cases up to 30 minutes. Check provider dashboard.
Security
Q: Are my credentials encrypted?
A: Yes. AES-256-GCM encryption with per-app keys. In transit uses HTTPS/TLS 1.2+.
Q: Do you store card numbers?
A: No. We never handle raw card data. Providers handle PCI compliance.
Q: How do you prevent fraud?
A: Signature verification, replay attack prevention, rate limiting, anomaly detection. For high-risk transactions, may request additional verification.
Q: Can I get an audit trail?
A: Yes. View all transactions, webhooks, and configuration changes in dashboard. Export available for compliance.
Integration
Q: How long does integration take?
A: 2-4 hours with CrediBill docs. Additional time for provider setup.
Q: Do you have SDKs?
A: Yes, for Node.js, Python, Go, Ruby. REST API for any language.
Q: Can I test before going live?
A: Yes. All providers have test mode. Use test credentials and payment numbers.
Q: How do I handle different currencies?
A: Each provider supports specific currencies. Configure per provider. Automatic currency conversion available.
Q: Can I customize invoice templates?
A: Yes. Dashboard has template editor. Can add logo, colors, custom fields.
Support & Troubleshooting
Q: How do I contact support?
A: Email support@credibill.tech or use in-app chat. Response time < 2 hours for critical issues.
Q: What’s your SLA?
A: 99.95% uptime SLA. Check our status page for incidents.
Q: Can you help debug payment issues?
A: Yes. We can check logs on our side. Provide transaction ID and time.
Q: What if I lose webhook logs?
A: Logs retained for 90 days. Older logs available by contacting support.
Q: How do I cancel my account?
A: Go to Settings → Account → Delete Account. All data deleted within 30 days per GDPR.
Still Need Help?