Troubleshooting
Troubleshooting
This guide covers common issues you might encounter when using QZPay and how to resolve them.
Installation Issues
Symptom: “Cannot find module ‘@qazuor/qzpay-core’”
Cause: The package isn’t installed or there’s a dependency resolution issue.
Solution:
# Clear node_modules and reinstallrm -rf node_modules pnpm-lock.yamlpnpm install
# Or install the specific packagepnpm add @qazuor/qzpay-coreSymptom: TypeScript errors after installation
Cause: TypeScript version mismatch or missing type definitions.
Solution:
# Ensure TypeScript 5.0+ is installedpnpm add -D typescript@latest
# Check tsconfig.json settings{ "compilerOptions": { "moduleResolution": "bundler", "strict": true }}Provider Configuration
Symptom: “Invalid API key” error with Stripe
Cause: Using the wrong API key or mixing test/live keys.
Solution:
- Verify you’re using the correct key type (test vs live)
- Check the key format starts with
sk_test_orsk_live_ - Ensure the key is properly set in environment variables
// Correctconst stripe = createQZPayStripeAdapter({ secretKey: process.env.STRIPE_SECRET_KEY! // sk_test_xxx or sk_live_xxx});
// Wrong - using publishable keyconst stripe = createQZPayStripeAdapter({ secretKey: process.env.STRIPE_PUBLISHABLE_KEY! // pk_test_xxx - won't work!});Symptom: “Webhook signature verification failed”
Cause: Webhook secret mismatch or payload manipulation.
Solution:
- Verify the webhook secret matches your Stripe dashboard
- Ensure you’re passing the raw body (not parsed JSON)
- Check that the signature header is being passed correctly
// Correct - raw bodyapp.post('/webhooks/stripe', express.raw({ type: 'application/json' }), handler);
// Wrong - parsed bodyapp.post('/webhooks/stripe', express.json(), handler); // Signature will fail!Symptom: MercadoPago “access_denied” error
Cause: Invalid credentials or insufficient permissions.
Solution:
- Regenerate credentials in MercadoPago developer dashboard
- Ensure the application has the required permissions
- Check you’re using production credentials in production
Database Issues
Symptom: “Table does not exist” error
Cause: Database migrations haven’t been run.
Solution:
# Run migrations using Drizzlepnpm drizzle-kit pushSymptom: “Foreign key constraint failed”
Cause: Trying to create a subscription for a non-existent customer.
Solution:
// Always create customer firstconst customer = await billing.customers.create({ email: 'user@example.com'});
// Then create subscription with customer IDconst subscription = await billing.subscriptions.create({ customerId: customer.id, planId: 'pro_monthly'});Payment Error Codes
Stripe Card Errors
| Error Code | Cause | User Message |
|---|---|---|
card_declined | Card was declined | Please try a different card |
insufficient_funds | Not enough funds | Please use a different payment method |
expired_card | Card has expired | Please update your card details |
incorrect_cvc | CVC is incorrect | Please check your security code |
processing_error | Processing failed | Please try again |
import { QZPayPaymentError } from '@qazuor/qzpay-core';
try { await billing.payments.process({ customerId, amount: 9900, currency: 'usd', paymentMethodId: 'pm_xxx' });} catch (error) { if (error instanceof QZPayPaymentError) { switch (error.code) { case 'card_declined': return { error: 'Your card was declined. Please try another card.' }; case 'insufficient_funds': return { error: 'Insufficient funds. Please try a different payment method.' }; case 'expired_card': return { error: 'Your card has expired. Please update your payment method.' }; default: return { error: 'Payment failed. Please try again.' }; } }}MercadoPago Rejection Codes
| Code | Cause | Solution |
|---|---|---|
cc_rejected_bad_filled_card_number | Invalid card number | Ask user to re-enter card |
cc_rejected_bad_filled_date | Invalid expiration date | Ask user to check date |
cc_rejected_bad_filled_security_code | Invalid CVV | Ask user to verify CVV |
cc_rejected_card_disabled | Card is disabled | User must contact bank |
cc_rejected_call_for_authorize | Requires authorization | User must call bank |
cc_rejected_duplicated_payment | Duplicate payment | Wait, payment may have succeeded |
cc_rejected_high_risk | High risk transaction | Try different card |
cc_rejected_insufficient_amount | Insufficient funds | Use different card |
cc_rejected_max_attempts | Too many attempts | Wait and retry later |
Subscription Issues
Symptom: “Plan not found” when creating subscription
Cause: Plan ID doesn’t exist in storage.
Solution:
// Verify plan exists before creating subscriptionconst plan = await billing.plans.get('pro_monthly');if (!plan) { throw new Error('Plan not found');}
// Create with valid planawait billing.subscriptions.create({ customerId, planId: plan.id});Symptom: Subscription status not updating
Cause: Webhooks not configured or not being processed.
Solution:
- Verify webhook endpoint is publicly accessible
- Check webhook logs in provider dashboard
- Ensure webhook handler is processing events
import { createWebhookRouter } from '@qazuor/qzpay-hono';
const webhookRouter = createWebhookRouter({ billing, paymentAdapter: stripeAdapter, handlers: { 'customer.subscription.updated': async (c, event) => { console.log('Subscription updated:', event.data.object.id); } }});Symptom: Cannot pause subscription
Cause: Subscription is not in an active state.
Solution:
const sub = await billing.subscriptions.get(subscriptionId);
// Only active subscriptions can be pausedif (sub.isActive()) { await billing.subscriptions.pause(subscriptionId);} else { console.log('Cannot pause subscription in status:', sub.status);}Symptom: Cannot cancel subscription
Cause: Subscription is already canceled.
Solution:
const sub = await billing.subscriptions.get(subscriptionId);
if (sub.isCanceled()) { return { error: 'Subscription is already canceled' };}
await billing.subscriptions.cancel(subscriptionId, { atPeriodEnd: true });Invoice Issues
Symptom: “Invoice cannot be finalized”
Cause: Invoice is not in draft status or has no line items.
Solution:
const invoice = await billing.invoices.get(invoiceId);
// Check statusif (invoice.status !== 'draft') { throw new Error(`Cannot finalize invoice in status: ${invoice.status}`);}
// Ensure it has line itemsif (!invoice.lines || invoice.lines.length === 0) { throw new Error('Invoice must have at least one line item');}
await billing.invoices.finalize(invoiceId);Symptom: “Cannot void paid invoice”
Cause: Trying to void an invoice that has already been paid.
Solution:
const invoice = await billing.invoices.get(invoiceId);
if (invoice.status === 'paid') { // Use refund instead await billing.payments.refund({ paymentId: invoice.paymentId, reason: 'requested_by_customer' });} else if (invoice.status === 'open') { await billing.invoices.void(invoiceId);}Promo Code Issues
Symptom: “Promo code is not active”
Cause: Code has been deactivated.
Solution:
const code = await billing.promoCodes.get('SUMMER50');
if (!code.active) { return { error: 'This promo code is no longer active' };}Symptom: “Promo code has expired”
Cause: Code’s validUntil date has passed.
Solution:
const code = await billing.promoCodes.get('SUMMER50');
if (code.validUntil && code.validUntil < new Date()) { return { error: 'This promo code has expired' };}Symptom: “Promo code has reached max redemptions”
Cause: Maximum usage limit reached.
Solution:
const code = await billing.promoCodes.get('SUMMER50');
if (code.maxRedemptions && code.timesRedeemed >= code.maxRedemptions) { return { error: 'This promo code is no longer available' };}Symptom: “Currency does not match”
Cause: Fixed-amount discount with different currency than the purchase.
Solution:
// Fixed amount discounts must match currencyconst code = await billing.promoCodes.get('FLAT10');
if (code.discountType === 'fixed_amount' && code.currency !== purchaseCurrency) { return { error: 'This promo code cannot be used with your currency' };}Validation Errors
Symptom: “Invalid amount” error
Cause: Amount is negative, not an integer, or exceeds maximum.
Solution:
// Amounts must be positive integers in cents// Maximum: 99,999,999 cents ($999,999.99)
// Wrongawait billing.payments.process({ amount: 99.99 }); // Not in cents!await billing.payments.process({ amount: -100 }); // Negative!
// Correctawait billing.payments.process({ amount: 9999 }); // $99.99 in centsSymptom: “Metadata validation failed”
Cause: Metadata exceeds limits.
Solution:
// Metadata limits:// - Maximum 50 keys// - Key length: 1-40 characters// - Value length: max 500 characters// - Only string, number, boolean values
// Wrongawait billing.customers.create({ email: 'user@example.com', metadata: { veryLongKeyNameThatExceedsFortyCharactersLimit: 'value', // Too long nested: { foo: 'bar' } // Objects not allowed }});
// Correctawait billing.customers.create({ email: 'user@example.com', metadata: { userId: 'usr_123', source: 'marketing_campaign' }});Webhook Issues
Symptom: Webhook signature invalid (Stripe)
Causes and solutions:
// Ensure raw body is passed, not parsed JSONimport { Hono } from 'hono';
const app = new Hono();
// Don't use bodyParser before webhook routeapp.post('/webhooks/stripe', async (c) => { const rawBody = await c.req.text(); // Get raw body const signature = c.req.header('stripe-signature');
const event = stripeAdapter.webhooks.constructEvent(rawBody, signature);});// Stripe signature format: t=timestamp,v1=hash,v0=hash// Common issues:// - Missing timestamp: signature too old (>5 min)// - Altered payload: hash mismatch// - Wrong secret: hash mismatch
const isValid = stripeAdapter.webhooks.verifySignature(rawBody, signature);if (!isValid) { return c.json({ error: 'Invalid signature' }, 400);}Symptom: Webhook signature invalid (MercadoPago)
Cause: MercadoPago uses HMAC-SHA256 with a specific format.
Solution:
// MercadoPago signature format: ts=timestamp,v1=hmac// The signature is computed from: id:{payment_id};request-id:{timestamp};ts:{timestamp};
// Ensure webhook secret is configuredconst mpAdapter = createQZPayMercadoPagoAdapter({ accessToken: process.env.MP_ACCESS_TOKEN!, webhookSecret: process.env.MP_WEBHOOK_SECRET! // Required for verification});Symptom: Webhook events not being received
Causes:
- Endpoint not publicly accessible
- Firewall blocking requests
- Wrong endpoint URL configured
Solution:
# Test with Stripe CLI locallystripe listen --forward-to localhost:3000/webhooks/stripe
# Check webhook logs in dashboard# Stripe: https://dashboard.stripe.com/webhooks# MercadoPago: https://www.mercadopago.com/developers/panelAdd-on Issues
Symptom: “Add-on is not compatible with plan”
Cause: Add-on has plan restrictions.
Solution:
const addOn = await billing.addons.get('extra_storage');const subscription = await billing.subscriptions.get(subscriptionId);
// Check compatibilityif (addOn.applicablePlans && !addOn.applicablePlans.includes(subscription.planId)) { return { error: 'This add-on is not available for your plan' };}Symptom: “Quantity exceeds maximum”
Cause: Add-on has a maximum quantity limit.
Solution:
const addOn = await billing.addons.get('extra_seats');
// Check quantity limitsif (addOn.maxQuantity && requestedQuantity > addOn.maxQuantity) { return { error: `Maximum quantity is ${addOn.maxQuantity}` };}React Component Issues
Symptom: “useQZPayContext must be used within a QZPayProvider”
Cause: Component using hooks outside provider context.
Solution:
// Wrong - hook outside providerfunction App() { const billing = useQZPayContext(); // Error! return <QZPayProvider>...</QZPayProvider>;}
// Correct - hook inside providerfunction App() { return ( <QZPayProvider billing={billing}> <CustomerInfo /> {/* useQZPayContext works here */} </QZPayProvider> );}Rate Limiting
Symptom: “Too many requests” (HTTP 429)
Cause: Exceeded API rate limits.
Solution:
// QZPay presets:// API_STANDARD: 100 requests/minute// API_STRICT: 10 requests/minute (sensitive operations)// AUTH: 5 attempts/5 minutes// WEBHOOK: 1000/minute
// Implement exponential backoffasync function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (error.status === 429 && i < maxRetries - 1) { const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s await new Promise(resolve => setTimeout(resolve, delay)); continue; } throw error; } } throw new Error('Max retries exceeded');}Debug Mode
Enable debug logging to troubleshoot issues:
import { createQZPayBilling } from '@qazuor/qzpay-core';
const billing = createQZPayBilling({ storage, paymentAdapter, logger: { debug: (msg, data) => console.log('[DEBUG]', msg, data), info: (msg, data) => console.log('[INFO]', msg, data), warn: (msg, data) => console.warn('[WARN]', msg, data), error: (msg, data) => console.error('[ERROR]', msg, data) }});Health Checks
Monitor system health:
// Check component healthconst health = await billing.health.check();
console.log('Storage:', health.storage.status);// 'healthy' | 'degraded' | 'unhealthy'
console.log('Payment adapter:', health.paymentAdapter.status);// 'healthy' if API responds within 3s// 'degraded' if slow (>3s but <5s)// 'unhealthy' if connection fails