> For the complete documentation index, see [llms.txt](https://docs.docs-dispatcher.io/docs-dispatcher/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.docs-dispatcher.io/docs-dispatcher/troubleshooting-and-errors/provider-issues.md).

# Provider Issues

Comprehensive guide to debugging provider-specific problems across all 12 Docs-Dispatcher providers.

## Overview

This guide covers:

* Provider-specific error patterns for all 12 providers
* Configuration issues by provider
* Credential validation
* Provider API errors
* Sandbox vs production issues
* Rate limits per provider
* Fallback strategies
* Provider health checks

## Provider Troubleshooting Matrix

Quick reference for common issues:

| Provider     | Common Issues                          | Config Check     | Status Page        |
| ------------ | -------------------------------------- | ---------------- | ------------------ |
| iPaidThat    | Duplicate invoice, invalid API key     | `/admin/configs` | No public status   |
| PennyLane    | SIRET requirement, tax validation      | `/admin/configs` | No public status   |
| Qonto        | Org slug, sandbox mode                 | `/admin/configs` | status.qonto.com   |
| SuperPDP     | Certification, archiving               | `/admin/configs` | No public status   |
| Signaturit   | Signer validation, webhooks            | `/admin/configs` | No public status   |
| Universign   | Qualified signature, French compliance | `/admin/configs` | No public status   |
| Yousign      | Multi-signer, sequential flow          | `/admin/configs` | status.yousign.com |
| MySendingBox | Address validation, tracking           | `/admin/configs` | No public status   |
| Brevo        | SMS credits, sender ID                 | `/admin/configs` | status.brevo.com   |
| OVH          | API credentials, account credits       | `/admin/configs` | status.ovh.com     |
| SMS Factor   | Phone format, delivery                 | `/admin/configs` | No public status   |
| SMS Magic    | International routing                  | `/admin/configs` | No public status   |

## Invoicing Providers (4)

### iPaidThat

#### Configuration Structure

```json
{
  "service": "invoicing",
  "providerName": "ipaidthat",
  "isDefault": true,
  "isActive": true,
  "config": {
    "apiKey": "ipaidthat_live_abc123",
    "useSandbox": false
  }
}
```

#### Common Errors

**1. Invalid API Key**

```json
{
  "error": "provider_error",
  "message": "Authentication failed with iPaidThat",
  "details": {
    "providerError": {
      "code": "INVALID_API_KEY",
      "message": "API key is invalid or expired"
    }
  }
}
```

**Solution:**

```javascript
// Verify API key in iPaidThat dashboard
async function validateIpaidThatKey(apiKey) {
  const response = await fetch('https://api.ipaidthat.io/v1/companies', {
    headers: { 'Authorization': `Bearer ${apiKey}` }
  });

  if (!response.ok) {
    throw new Error(
      'Invalid iPaidThat API key. ' +
      'Get new key at https://app.ipaidthat.io/settings/api'
    );
  }

  console.log('✓ iPaidThat API key valid');
  return true;
}
```

**2. Duplicate Invoice Number**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "DUPLICATE_INVOICE",
      "message": "Invoice number already exists",
      "invoiceNumber": "INV-2026-001"
    }
  }
}
```

**Solution:**

```javascript
// Generate unique invoice numbers with timestamp
function generateUniqueInvoiceNumber(prefix = 'INV') {
  const date = new Date();
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  const timestamp = Date.now().toString().slice(-6);

  return `${prefix}-${year}${month}${day}-${timestamp}`;
}
```

**3. Missing Customer Information**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "MISSING_CUSTOMER_DATA",
      "message": "Customer email and name are required"
    }
  }
}
```

**Solution:**

```javascript
// Validate customer data
function validateCustomerData(customer) {
  const required = ['name', 'email', 'address'];
  const missing = required.filter(field => !customer[field]);

  if (missing.length > 0) {
    throw new Error(
      `Missing required customer fields for iPaidThat: ${missing.join(', ')}`
    );
  }

  // Validate email format
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(customer.email)) {
    throw new Error('Invalid customer email format');
  }
}
```

### PennyLane

#### Configuration Structure

```json
{
  "service": "invoicing",
  "providerName": "pennylane",
  "config": {
    "apiKey": "pennylane_live_xyz789",
    "companyId": "comp_abc123",
    "useSandbox": false
  }
}
```

#### Common Errors

**1. SIRET Required for French B2B**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "MISSING_SIRET",
      "message": "SIRET number required for French B2B e-invoices"
    }
  }
}
```

**Solution:**

```javascript
// Validate French e-invoice requirements
function validateFrenchEInvoice(invoiceData) {
  if (invoiceData.documentType !== 'E_INVOICE') return;
  if (invoiceData.customer.country !== 'FR') return;

  // B2B requires SIRET
  if (invoiceData.customer.type === 'COMPANY') {
    if (!invoiceData.customer.siret) {
      throw new Error(
        'SIRET required for French B2B e-invoices. ' +
        'Add customer.siret (14 digits) to template data.'
      );
    }

    // Validate SIRET format
    if (!/^\d{14}$/.test(invoiceData.customer.siret)) {
      throw new Error('SIRET must be exactly 14 digits');
    }
  }

  // B2C requires different validation
  if (invoiceData.customer.type === 'INDIVIDUAL') {
    console.log('✓ B2C e-invoice, no SIRET required');
  }
}
```

**2. Invalid Tax Rate**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "INVALID_TAX_RATE",
      "message": "Tax rate 25% is not valid for France",
      "validRates": [0, 2.1, 5.5, 10, 20]
    }
  }
}
```

**Solution:**

```javascript
// Use valid French VAT rates
const FRENCH_VAT_RATES = {
  STANDARD: 20.0,
  REDUCED: 10.0,
  SUPER_REDUCED: 5.5,
  SPECIAL: 2.1,
  ZERO: 0.0
};

function validateFrenchVAT(taxRate) {
  const validRates = Object.values(FRENCH_VAT_RATES);

  if (!validRates.includes(taxRate)) {
    throw new Error(
      `Invalid VAT rate: ${taxRate}%. ` +
      `Valid rates for France: ${validRates.join(', ')}%`
    );
  }
}
```

**3. Invalid Company Configuration**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "INVALID_COMPANY",
      "message": "Company ID not found or not accessible"
    }
  }
}
```

**Solution:**

```javascript
// Test PennyLane configuration
async function testPennyLaneConfig(apiKey, companyId) {
  try {
    const response = await fetch(
      `https://api.pennylane.com/api/v1/companies/${companyId}`,
      { headers: { 'Authorization': `Bearer ${apiKey}` } }
    );

    if (!response.ok) {
      throw new Error('Company not found or API key invalid');
    }

    const company = await response.json();
    console.log('✓ PennyLane config valid for:', company.name);
    return true;

  } catch (error) {
    throw new Error(
      'PennyLane configuration test failed. ' +
      'Verify API key and company ID at https://app.pennylane.com/settings/api'
    );
  }
}
```

### Qonto

#### Configuration Structure

```json
{
  "service": "invoicing",
  "providerName": "qonto",
  "config": {
    "apiKey": "qonto_live_def456",
    "organizationSlug": "acme-corp",
    "useSandbox": true
  }
}
```

#### Common Errors

**1. Invalid Organization Slug**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "INVALID_ORG",
      "message": "Organization 'acme-corp' not found"
    }
  }
}
```

**Solution:**

```javascript
// Get organization slug from Qonto
async function getQontoOrgSlug(apiKey) {
  const response = await fetch('https://api.qonto.com/v2/organizations', {
    headers: { 'Authorization': `Bearer ${apiKey}` }
  });

  const data = await response.json();
  const orgs = data.organizations;

  if (orgs.length === 0) {
    throw new Error('No organizations found for this API key');
  }

  console.log('Available organizations:');
  orgs.forEach(org => {
    console.log(`- ${org.slug} (${org.name})`);
  });

  return orgs[0].slug;
}
```

**2. Sandbox vs Production Mismatch**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "SANDBOX_REQUIRED",
      "message": "This API key is for sandbox only"
    }
  }
}
```

**Solution:**

```javascript
// Validate sandbox/production consistency
function validateQontoEnvironment(apiKey, useSandbox) {
  const isSandboxKey = apiKey.includes('sandbox') || apiKey.includes('test');

  if (isSandboxKey && !useSandbox) {
    throw new Error(
      'Sandbox API key detected but useSandbox is false. ' +
      'Set useSandbox: true or use production API key.'
    );
  }

  if (!isSandboxKey && useSandbox) {
    console.warn(
      'Warning: Production API key with sandbox mode enabled. ' +
      'Requests may fail.'
    );
  }
}
```

**3. Rate Limit Exceeded**

**Rate limits:**

* Sandbox: 10 requests/minute
* Production: 100 requests/minute

```javascript
// Rate-limited queue for Qonto
class QontoRateLimiter {
  constructor(isSandbox = false) {
    this.limit = isSandbox ? 10 : 100;
    this.queue = [];
    this.requests = 0;
    this.resetTime = Date.now() + 60000;
  }

  async enqueue(fn) {
    // Reset counter every minute
    if (Date.now() >= this.resetTime) {
      this.requests = 0;
      this.resetTime = Date.now() + 60000;
    }

    // Wait if at limit
    if (this.requests >= this.limit) {
      const waitMs = this.resetTime - Date.now();
      console.log(`Qonto rate limit reached. Waiting ${waitMs}ms`);
      await new Promise(resolve => setTimeout(resolve, waitMs));
      return this.enqueue(fn);
    }

    this.requests++;
    return await fn();
  }
}
```

### SuperPDP

#### Configuration Structure

```json
{
  "service": "invoicing",
  "providerName": "superpdp",
  "config": {
    "apiKey": "superpdp_live_ghi789",
    "entityCode": "ENTITY_001",
    "useSandbox": false
  }
}
```

#### Common Errors

**1. Missing Entity Code**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "MISSING_ENTITY",
      "message": "Entity code required for SuperPDP"
    }
  }
}
```

**Solution:**

```javascript
// Validate SuperPDP entity code
function validateSuperPDPEntity(entityCode) {
  if (!entityCode) {
    throw new Error(
      'Entity code required for SuperPDP. ' +
      'Find your entity code at https://app.superpdp.com/settings'
    );
  }

  // Entity code format: ENTITY_XXX
  if (!/^ENTITY_\w+$/.test(entityCode)) {
    throw new Error(
      'Invalid entity code format. Expected: ENTITY_XXX'
    );
  }
}
```

**2. Government Invoice Requirements**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "MISSING_PO_NUMBER",
      "message": "Purchase order number required for government invoices"
    }
  }
}
```

**Solution:**

```javascript
// Validate government invoice data
function validateGovernmentInvoice(invoiceData) {
  // Government invoices need PO number
  if (invoiceData.customer.type === 'GOVERNMENT') {
    if (!invoiceData.purchaseOrderNumber) {
      throw new Error(
        'Purchase order number required for government invoices. ' +
        'Add purchaseOrderNumber to template data.'
      );
    }
  }

  // Certified archiving required
  if (invoiceData.documentType === 'E_INVOICE') {
    invoiceData.requiresArchiving = true;
  }
}
```

## eSign Providers (3)

### Signaturit

#### Configuration Structure

```json
{
  "service": "esign",
  "providerName": "signaturit",
  "config": {
    "apiKey": "signaturit_live_jkl012",
    "webhookSecret": "whsec_mno345",
    "useSandbox": false
  }
}
```

#### Common Errors

**1. Invalid Signer Email**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "INVALID_SIGNER",
      "message": "Signer email 'invalid-email' is not valid"
    }
  }
}
```

**Solution:**

```javascript
// Validate all signers
function validateSigners(signers) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

  signers.forEach((signer, index) => {
    // Email required
    if (!signer.email || !emailRegex.test(signer.email)) {
      throw new Error(
        `Signer ${index + 1}: Invalid email '${signer.email}'`
      );
    }

    // Name required
    if (!signer.firstName || !signer.lastName) {
      throw new Error(
        `Signer ${index + 1}: firstName and lastName required`
      );
    }

    // Order required for sequential
    if (signer.order !== undefined && typeof signer.order !== 'number') {
      throw new Error(
        `Signer ${index + 1}: order must be a number`
      );
    }
  });
}
```

**2. Webhook Verification Failed**

```json
{
  "error": "webhook_verification_failed",
  "message": "Webhook signature is invalid"
}
```

**Solution:**

```javascript
// Verify Signaturit webhook signature
const crypto = require('crypto');

function verifySignaturitWebhook(payload, signature, secret) {
  const computedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  if (computedSignature !== signature) {
    throw new Error('Webhook signature verification failed');
  }

  console.log('✓ Webhook signature valid');
  return true;
}

// Express webhook handler
app.post('/webhooks/signaturit', (req, res) => {
  const signature = req.headers['x-signaturit-signature'];
  const secret = process.env.SIGNATURIT_WEBHOOK_SECRET;

  try {
    verifySignaturitWebhook(req.body, signature, secret);

    // Process webhook
    const event = req.body;
    console.log('Signature event:', event.type);

    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook verification failed:', error);
    res.status(401).send('Unauthorized');
  }
});
```

**3. Document Already Signed**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "ALREADY_SIGNED",
      "message": "Document has already been signed"
    }
  }
}
```

**Prevention:**

```javascript
// Check signature status before sending reminder
async function checkSignatureStatus(transactionId) {
  const response = await fetch(
    `https://api.signaturit.com/v3/signatures/${transactionId}`,
    { headers: { 'Authorization': `Bearer ${apiKey}` } }
  );

  const signature = await response.json();

  return {
    status: signature.status, // 'pending', 'completed', 'expired'
    signedBy: signature.signers.filter(s => s.status === 'signed'),
    pendingSigners: signature.signers.filter(s => s.status === 'pending')
  };
}
```

### Universign

#### Configuration Structure

```json
{
  "service": "esign",
  "providerName": "universign",
  "config": {
    "username": "user@company.com",
    "password": "secure-password",
    "apiUrl": "https://api.universign.com/v1",
    "webhookSecret": "whsec_pqr678"
  }
}
```

#### Common Errors

**1. Qualified Signature Not Available**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "QUALIFIED_UNAVAILABLE",
      "message": "Qualified signature not available for this signer"
    }
  }
}
```

**Solution:**

```javascript
// Fallback to advanced signature
async function requestSignature(documentData, preferQualified = true) {
  try {
    // Try qualified first
    if (preferQualified) {
      return await requestQualifiedSignature(documentData);
    }
  } catch (error) {
    if (error.code === 'QUALIFIED_UNAVAILABLE') {
      console.warn('Qualified signature unavailable, using advanced');
      return await requestAdvancedSignature(documentData);
    }
    throw error;
  }
}

function requestAdvancedSignature(documentData) {
  return fetch('https://api.docs-dispatcher.io/api/esign', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${jwt}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      providerName: 'universign',
      signatureType: 'ADVANCED', // Not QUALIFIED
      ...documentData
    })
  });
}
```

**2. French Compliance Requirements**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "COMPLIANCE_FAILED",
      "message": "Document must include legal notice for eIDAS compliance"
    }
  }
}
```

**Solution:**

```javascript
// Add required legal notices for French compliance
function addFrenchLegalNotice(templateData) {
  const legalNotice = `
    Signature électronique qualifiée conforme au règlement eIDAS.
    Valeur juridique équivalente à une signature manuscrite.
  `;

  templateData.legalNotice = legalNotice;
  return templateData;
}
```

### Yousign

#### Configuration Structure

```json
{
  "service": "esign",
  "providerName": "yousign",
  "config": {
    "apiKey": "yousign_live_stu901",
    "useSandbox": false
  }
}
```

#### Common Errors

**1. Sequential Signing Order Error**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "INVALID_ORDER",
      "message": "Signing order must start at 1 and be consecutive"
    }
  }
}
```

**Solution:**

```javascript
// Validate and fix signing order
function validateSigningOrder(signers) {
  const orders = signers
    .filter(s => s.order !== undefined)
    .map(s => s.order)
    .sort((a, b) => a - b);

  if (orders.length === 0) {
    // No order specified = parallel signing
    return signers;
  }

  // Check starts at 1
  if (orders[0] !== 1) {
    throw new Error('Signing order must start at 1');
  }

  // Check consecutive
  for (let i = 0; i < orders.length - 1; i++) {
    if (orders[i + 1] !== orders[i] + 1) {
      throw new Error(
        `Non-consecutive signing order: ${orders[i]} -> ${orders[i + 1]}`
      );
    }
  }

  return signers;
}
```

**2. Document Size Limit Exceeded**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "FILE_TOO_LARGE",
      "message": "Document size exceeds 10MB limit",
      "size": 12582912,
      "limit": 10485760
    }
  }
}
```

**Solution:**

```javascript
// Optimize document before sending
async function optimizeForYousign(templateId, templateData) {
  // Compress images in template data
  if (templateData.images) {
    templateData.images = await Promise.all(
      templateData.images.map(img => compressImage(img, { maxSize: 800 }))
    );
  }

  // Request compressed PDF
  const response = await fetch('https://api.docs-dispatcher.io/api/file', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${jwt}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      template: { id: templateId, data: templateData },
      outputFormat: 'PDF',
      optimization: {
        compressImages: true,
        imageQuality: 85,
        compressPDF: true
      }
    })
  });

  const result = await response.json();

  // Check size
  if (result.generatedDocument.size > 10 * 1024 * 1024) {
    throw new Error('Document still too large after optimization');
  }

  return result.generatedDocument.url;
}
```

## Postal Provider (1)

### MySendingBox

#### Configuration Structure

```json
{
  "service": "postal",
  "providerName": "mysendingbox",
  "config": {
    "apiKey": "msb_live_vwx234",
    "webhookSecret": "whsec_yza567",
    "useSandbox": false
  }
}
```

#### Common Errors

**1. Invalid Address**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "INVALID_ADDRESS",
      "message": "Address validation failed",
      "field": "postalCode",
      "reason": "Invalid postal code format for France"
    }
  }
}
```

**Solution:**

```javascript
// Validate address by country
function validateAddress(address) {
  const validators = {
    FR: validateFrenchAddress,
    US: validateUSAddress,
    GB: validateUKAddress
  };

  const validator = validators[address.country];

  if (!validator) {
    console.warn(`No validator for country: ${address.country}`);
    return; // Skip validation
  }

  validator(address);
}

function validateFrenchAddress(address) {
  // Postal code: 5 digits
  if (!/^\d{5}$/.test(address.postalCode)) {
    throw new Error(
      'Invalid French postal code. Expected 5 digits (e.g., 75001)'
    );
  }

  // Required fields
  const required = ['name', 'address', 'city', 'postalCode', 'country'];
  const missing = required.filter(field => !address[field]);

  if (missing.length > 0) {
    throw new Error(`Missing address fields: ${missing.join(', ')}`);
  }
}

function validateUSAddress(address) {
  // ZIP code: 5 digits or ZIP+4
  if (!/^\d{5}(-\d{4})?$/.test(address.postalCode)) {
    throw new Error(
      'Invalid US ZIP code. Expected 12345 or 12345-6789'
    );
  }

  // State code: 2 letters
  if (!/^[A-Z]{2}$/.test(address.state)) {
    throw new Error('Invalid US state code. Expected 2 letters (e.g., CA)');
  }
}
```

**2. International Shipping Restrictions**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "COUNTRY_RESTRICTED",
      "message": "Registered mail not available for destination country"
    }
  }
}
```

**Solution:**

```javascript
// Check mail type availability by country
const MAIL_TYPE_AVAILABILITY = {
  SIMPLE: ['FR', 'EU', 'INTERNATIONAL'],
  REGISTERED: ['FR', 'EU'],
  EXPRESS: ['FR']
};

function validateMailTypeForCountry(mailType, country) {
  const isEU = ['DE', 'BE', 'IT', 'ES', 'NL', 'PT'].includes(country);
  const region = country === 'FR' ? 'FR' : (isEU ? 'EU' : 'INTERNATIONAL');

  const availability = MAIL_TYPE_AVAILABILITY[mailType];

  if (!availability.includes(region)) {
    throw new Error(
      `${mailType} mail not available for ${country}. ` +
      `Available types: ${Object.keys(MAIL_TYPE_AVAILABILITY)
        .filter(type => MAIL_TYPE_AVAILABILITY[type].includes(region))
        .join(', ')}`
    );
  }
}
```

**3. Tracking Not Available**

```json
{
  "error": "provider_error",
  "details": {
    "providerError": {
      "code": "NO_TRACKING",
      "message": "Tracking not available for simple mail type"
    }
  }
}
```

**Solution:**

```javascript
// Choose appropriate mail type for tracking
function selectMailType(requiresTracking, requiresSignature) {
  if (requiresSignature) {
    return 'REGISTERED'; // Includes tracking + signature
  }

  if (requiresTracking) {
    return 'REGISTERED'; // Minimum for tracking
  }

  return 'SIMPLE'; // No tracking
}
```

## SMS Providers (4)

### Common SMS Issues (All Providers)

**1. Invalid Phone Number Format**

All SMS providers require E.164 format: `+[country code][number]`

```javascript
// Validate and format phone numbers
function formatPhoneNumber(phone, defaultCountryCode = '33') {
  // Remove spaces, dashes, parentheses
  let cleaned = phone.replace(/[\s\-\(\)]/g, '');

  // Add + if missing
  if (!cleaned.startsWith('+')) {
    // Add country code if not present
    if (!cleaned.startsWith('00')) {
      cleaned = `+${defaultCountryCode}${cleaned}`;
    } else {
      cleaned = '+' + cleaned.substring(2);
    }
  }

  // Validate format: +[1-3 digits country code][4-14 digits number]
  if (!/^\+\d{1,3}\d{4,14}$/.test(cleaned)) {
    throw new Error(
      `Invalid phone number: ${phone}. ` +
      'Expected format: +33612345678 (E.164)'
    );
  }

  return cleaned;
}

// Examples
console.log(formatPhoneNumber('06 12 34 56 78', '33')); // +33612345678
console.log(formatPhoneNumber('+1 (555) 123-4567')); // +15551234567
```

**2. Message Length Exceeded**

```javascript
// Check SMS length and calculate segments
function calculateSMSSegments(message) {
  const hasUnicode = /[^\x00-\x7F]/.test(message);

  const singleSMSLimit = hasUnicode ? 70 : 160;
  const concatenatedLimit = hasUnicode ? 67 : 153;

  const length = message.length;

  if (length <= singleSMSLimit) {
    return {
      segments: 1,
      length: length,
      remaining: singleSMSLimit - length,
      encoding: hasUnicode ? 'Unicode' : 'GSM-7'
    };
  }

  const segments = Math.ceil(length / concatenatedLimit);
  const maxLength = segments * concatenatedLimit;

  return {
    segments: segments,
    length: length,
    remaining: maxLength - length,
    encoding: hasUnicode ? 'Unicode' : 'GSM-7',
    warning: segments > 3 ? 'Message is very long (3+ segments)' : null
  };
}

// Usage
const info = calculateSMSSegments(message);
console.log(`Message: ${info.segments} segments, ${info.length} chars`);

if (info.warning) {
  console.warn(info.warning);
}
```

### Provider-Specific Rate Limits

| Provider   | Requests/min | Burst | Best For                 |
| ---------- | ------------ | ----- | ------------------------ |
| Brevo      | 100          | 200   | High volume, marketing   |
| OVH        | 50           | 100   | European coverage        |
| SMS Factor | 100          | 150   | French market, analytics |
| SMS Magic  | 200          | 300   | Global, high throughput  |

```javascript
// Rate limiter with provider-specific limits
class SMSRateLimiter {
  constructor(provider) {
    const limits = {
      brevo: { perMinute: 100, burst: 200 },
      ovh: { perMinute: 50, burst: 100 },
      sms_factor: { perMinute: 100, burst: 150 },
      sms_magic: { perMinute: 200, burst: 300 }
    };

    this.limits = limits[provider] || { perMinute: 50, burst: 100 };
    this.requests = [];
  }

  async throttle() {
    const now = Date.now();
    const oneMinuteAgo = now - 60000;

    // Clean old requests
    this.requests = this.requests.filter(time => time > oneMinuteAgo);

    // Check burst limit
    if (this.requests.length >= this.limits.burst) {
      throw new Error(
        `Burst limit exceeded (${this.limits.burst} requests)`
      );
    }

    // Check per-minute limit
    if (this.requests.length >= this.limits.perMinute) {
      const waitMs = this.requests[0] - oneMinuteAgo;
      console.log(`Rate limit reached. Waiting ${waitMs}ms`);
      await new Promise(resolve => setTimeout(resolve, waitMs));
      return this.throttle();
    }

    this.requests.push(now);
  }
}
```

## Configuration Debugging

### Check Provider Configuration

```javascript
// Complete provider configuration checker
async function debugProviderConfig(providerName, service) {
  console.log(`\n=== Debugging ${providerName} Configuration ===\n`);

  try {
    // 1. Check user config
    console.log('1. Checking user configuration...');
    const userConfigs = await fetch('/api/v1/users/me/configs', {
      headers: { 'Authorization': `Bearer ${jwt}` }
    }).then(r => r.json());

    const userConfig = userConfigs.find(c =>
      c.providerName === providerName && c.service === service
    );

    if (userConfig) {
      console.log('✓ User config found');
      console.log('  - Active:', userConfig.isActive);
      console.log('  - Default:', userConfig.isDefault);
      return { found: true, level: 'user', config: userConfig };
    }

    // 2. Check company config
    console.log('✗ No user config, checking company...');
    const companyConfigs = await fetch('/api/v1/companies/me/configs', {
      headers: { 'Authorization': `Bearer ${jwt}` }
    }).then(r => r.json());

    const companyConfig = companyConfigs.find(c =>
      c.providerName === providerName && c.service === service
    );

    if (companyConfig) {
      console.log('✓ Company config found');
      console.log('  - Active:', companyConfig.isActive);
      console.log('  - Default:', companyConfig.isDefault);
      return { found: true, level: 'company', config: companyConfig };
    }

    // 3. Not found
    console.log('✗ No company config found');
    console.error(
      `\nProvider ${providerName} not configured.\n` +
      'Configure at: https://app.docs-dispatcher.io/admin/configs\n'
    );

    return { found: false };

  } catch (error) {
    console.error('Configuration check failed:', error.message);
    throw error;
  }
}

// Usage
await debugProviderConfig('qonto', 'invoicing');
```

### Test Provider Credentials

```javascript
// Test provider API credentials
async function testProviderCredentials(providerName, config) {
  const tests = {
    qonto: testQontoCredentials,
    pennylane: testPennyLaneCredentials,
    brevo: testBrevoCredentials,
    universign: testUniversignCredentials
  };

  const test = tests[providerName];

  if (!test) {
    console.log(`No credential test available for ${providerName}`);
    return null;
  }

  try {
    console.log(`Testing ${providerName} credentials...`);
    const result = await test(config);
    console.log('✓ Credentials valid');
    return result;
  } catch (error) {
    console.error(`✗ Credentials invalid: ${error.message}`);
    throw error;
  }
}

async function testQontoCredentials(config) {
  const response = await fetch('https://api.qonto.com/v2/organizations', {
    headers: { 'Authorization': `Bearer ${config.apiKey}` }
  });

  if (!response.ok) {
    throw new Error('API key invalid or expired');
  }

  const data = await response.json();
  return data.organizations;
}
```

## Provider Status Checks

```javascript
// Check provider API status
async function checkProviderStatus(providerName) {
  const statusPages = {
    qonto: 'https://status.qonto.com/api/v2/status.json',
    brevo: 'https://status.brevo.com/api/v2/status.json',
    yousign: 'https://status.yousign.com/api/v2/status.json',
    ovh: 'https://status.ovh.com/api/status'
  };

  const statusUrl = statusPages[providerName];

  if (!statusUrl) {
    return { available: true, note: 'No public status page' };
  }

  try {
    const response = await fetch(statusUrl, { timeout: 5000 });
    const data = await response.json();

    return {
      available: data.status?.indicator === 'none',
      status: data.status?.description || 'operational',
      updated: data.page?.updated_at
    };
  } catch (error) {
    return {
      available: null,
      error: 'Could not check status page'
    };
  }
}
```

## Fallback Strategies

```javascript
// Fallback to alternative provider
async function dispatchWithFallback(requestData, providers) {
  const errors = [];

  for (const provider of providers) {
    try {
      console.log(`Trying provider: ${provider}`);

      const result = await dispatch({
        ...requestData,
        providerName: provider
      });

      console.log(`✓ Success with ${provider}`);
      return result;

    } catch (error) {
      console.warn(`✗ ${provider} failed: ${error.message}`);
      errors.push({ provider, error: error.message });
    }
  }

  throw new Error(
    'All providers failed:\n' +
    errors.map(e => `- ${e.provider}: ${e.error}`).join('\n')
  );
}

// Usage
const smsProviders = ['brevo', 'ovh', 'sms_factor', 'sms_magic'];
const result = await dispatchWithFallback(smsRequest, smsProviders);
```

## Related Documentation

* [Common Errors](/docs-dispatcher/troubleshooting-and-errors/errors.md) - HTTP errors and solutions
* [Configuration Problems](/docs-dispatcher/troubleshooting-and-errors/configuration.md) - Config resolution debugging
* [Provider Configurations](/docs-dispatcher/core-concepts/providers-configurations.md) - Setup guide
* [Providers Reference](/docs-dispatcher/providers/providers.md) - Detailed provider docs

## Summary

Key troubleshooting steps:

1. **Check configuration** - Verify provider is configured and active
2. **Validate credentials** - Test API keys and credentials
3. **Check provider status** - Verify provider API is operational
4. **Validate data** - Ensure data meets provider requirements
5. **Test in sandbox** - Use sandbox mode for testing
6. **Implement fallbacks** - Have backup providers configured
7. **Monitor rate limits** - Respect provider limits
8. **Log provider errors** - Track provider-specific errors

**Provider-specific requirements:**

* **PennyLane:** SIRET for French B2B e-invoices
* **Qonto:** Organization slug required
* **SuperPDP:** Entity code and PO numbers
* **Universign:** French compliance notices
* **MySendingBox:** Address validation per country
* **All SMS:** E.164 phone format

Always test with validation endpoint before production dispatch.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.docs-dispatcher.io/docs-dispatcher/troubleshooting-and-errors/provider-issues.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
