Skip to main content

Troubleshooting

Payment Issues

Payment Stuck in “Pending”

Symptoms: Transaction created but status never changes to success/failed Common causes:
  1. Mobile money delays: MMOs can take 1-5 minutes
  2. Webhook delivery failure: Provider sent webhook but we didn’t receive it
  3. Provider API error: Provider accepted payment but network error on response
Solution:
// 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 });
}
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:
  1. Insufficient funds: Customer doesn’t have balance
  2. Invalid payment method: Phone number or card incorrect
  3. Account blocked: Account frozen by provider
  4. Daily limit reached: Customer exceeded daily transaction limit
Solution:
// app/api/payments/[id]/route.ts
// (GET handler shown earlier)
// Use that endpoint to inspect failure fields from the payment object
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:
  1. Missing idempotency: Payment initiated multiple times
  2. Client-side double submission: Form submitted twice
  3. 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:
  • Webhook URL is public (not localhost)
  • HTTPS certificate is valid
  • Firewall allows CrediBill IPs
  • Endpoint returns 200 OK
  • Endpoint responds within 10 seconds
  • Correct provider webhook secret configured
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:
  • Using raw body (not parsed JSON)
  • Webhook secret is correct
  • Using timing-safe comparison
  • Timestamp header present
  • Not modifying body before verification
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:
  • Credentials copied correctly (no extra spaces)
  • Using correct credentials (test vs. live)
  • Credentials still valid in provider dashboard
  • Not using URL-encoded credentials
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:
  • Credentials are current (not expired)
  • Provider account active (not suspended)
  • Network connectivity working
  • Firewall allows outbound to provider
  • Provider API not down
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 [email protected] 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?