Error Codes

Understanding API errors and validation messages.

HTTP Status Codes

The API uses standard HTTP status codes to indicate success or failure:

Code Meaning Description
200 OK Request successful. Response contains pdf_base64 or validation results.
400 Bad Request Invalid JSON syntax or Content-Type header missing.
401 Unauthorized Missing or invalid X-API-Key header.
402 Payment Required Monthly quota exceeded. Upgrade plan or wait for reset.
422 Unprocessable Entity Validation errors in invoice data (see details array).
429 Too Many Requests Rate limit exceeded. Check Retry-After header (seconds).
500 Internal Server Error Unexpected server error. Contact support if persists.
Pro Tip: Always check response.ok (or status code) before parsing JSON. Errors return JSON with error and details fields.

Error Response Format

All errors return a JSON object with details:

{
  "error": "validation_failed",
  "details": [
    {
      "path": "$.invoice.seller.vat_id",
      "code": "INVALID_VAT_FORMAT",
      "message": "VAT ID 'DE12345' invalid. Expected: country code + numbers (e.g., DE123456789)",
      "severity": "error"
    },
    {
      "path": "$.invoice.items[0].unit",
      "code": "UNKNOWN_UNIT_CODE",
      "message": "Unit 'STK' unknown. Use UN/ECE codes (e.g., 'C62' for piece)",
      "severity": "error"
    }
  ]
}

Validation Error Codes

When the API returns 422 Unprocessable Entity, the response includes specific error codes for each invalid field:

Code Description Example
REQUIRED_FIELD A required field is missing $.invoice.seller.name
INVALID_VALUE Value is out of valid range or wrong type $.invoice.items[0].quantity
INVALID_DATE_FORMAT Date must be YYYY-MM-DD (ISO 8601) $.invoice.issue_date
INVALID_VAT_FORMAT VAT ID format is incorrect for country $.invoice.seller.vat_id
INVALID_IBAN IBAN checksum or format invalid $.invoice.payment.iban
INVALID_BIC BIC/SWIFT code format invalid $.invoice.payment.bic
INVALID_CURRENCY Currency code not ISO 4217 compliant $.invoice.currency
INVALID_COUNTRY_CODE Country code not ISO 3166-1 alpha-2 $.invoice.buyer.country
INVALID_TEMPLATE Unknown template name $.template
INVALID_FORMAT Unknown format (must be zugferd, facturx, etc.) $.format
UNKNOWN_UNIT_CODE Unit code not in UN/ECE Rec. 20 $.invoice.items[0].unit
AMOUNT_MISMATCH Calculated total doesn't match provided value $.invoice.total
INVALID_TAX_RATE Tax rate must be between 0-100 $.invoice.items[0].tax_rate

Handling Errors

Best practices for error handling:

  • Check the status code before parsing the response body
  • Parse the details array for specific validation issues
  • Use the path to identify which field caused the error
  • Display user-friendly messages from the message field

Code Examples

JavaScript (with Retry Logic)

async function generateInvoice(invoiceData, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch('https://api.thelawin.dev/v1/generate', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': process.env.THELAWIN_API_KEY
        },
        body: JSON.stringify(invoiceData)
      });

      if (response.ok) {
        return await response.json();
      }

      const error = await response.json();

      // Handle rate limiting with Retry-After header
      if (response.status === 429) {
        const retryAfter = parseInt(response.headers.get('Retry-After') || '1');
        console.log(`Rate limited. Retrying after ${retryAfter}s...`);
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
        continue;
      }

      // Handle validation errors
      if (response.status === 422) {
        console.error('Validation errors:');
        error.details.forEach(detail => {
          console.error(`  ${detail.path}: ${detail.message} [${detail.code}]`);
        });
        throw new Error('Validation failed');
      }

      // Handle quota exceeded
      if (response.status === 402) {
        throw new Error('Monthly quota exceeded. Upgrade your plan.');
      }

      // Handle auth errors
      if (response.status === 401) {
        throw new Error('Invalid API key');
      }

      throw new Error(`API error: ${error.error}`);
    } catch (err) {
      if (attempt === maxRetries - 1) throw err;
      const waitMs = Math.pow(2, attempt) * 1000; // Exponential backoff
      await new Promise(resolve => setTimeout(resolve, waitMs));
    }
  }
}

Python (with Error Handling)

import requests
import os
import time

def generate_invoice(invoice_data, max_retries=3):
    url = 'https://api.thelawin.dev/v1/generate'
    headers = {
        'Content-Type': 'application/json',
        'X-API-Key': os.environ['THELAWIN_API_KEY']
    }

    for attempt in range(max_retries):
        try:
            response = requests.post(url, headers=headers, json=invoice_data)

            if response.status_code == 200:
                return response.json()

            error = response.json()

            # Handle rate limiting
            if response.status_code == 429:
                retry_after = int(response.headers.get('Retry-After', 1))
                print(f"Rate limited. Retrying after {retry_after}s...")
                time.sleep(retry_after)
                continue

            # Handle validation errors
            if response.status_code == 422:
                print("Validation errors:")
                for detail in error.get('details', []):
                    print(f"  {detail['path']}: {detail['message']} [{detail['code']}]")
                raise ValueError("Validation failed")

            # Handle quota exceeded
            if response.status_code == 402:
                raise Exception("Monthly quota exceeded. Upgrade your plan.")

            # Handle auth errors
            if response.status_code == 401:
                raise Exception("Invalid API key")

            raise Exception(f"API error: {error.get('error')}")

        except requests.exceptions.RequestException as e:
            if attempt == max_retries - 1:
                raise
            wait_seconds = 2 ** attempt  # Exponential backoff
            time.sleep(wait_seconds)

Ruby (with thelawin gem)

require 'thelawin'

client = Thelawin::Client.new(api_key: ENV['THELAWIN_API_KEY'])

begin
  result = client.generate(
    invoice: invoice_data,
    template: 'minimal',
    format: 'zugferd'
  )

  File.write('invoice.pdf', Base64.decode64(result['pdf_base64']))
  puts "Invoice generated successfully!"

rescue Thelawin::ValidationError => e
  puts "Validation errors:"
  e.details.each do |detail|
    puts "  #{detail['path']}: #{detail['message']} [#{detail['code']}]"
  end

rescue Thelawin::RateLimitError => e
  puts "Rate limit exceeded. Retry after #{e.retry_after} seconds."
  sleep e.retry_after
  retry

rescue Thelawin::QuotaExceededError => e
  puts "Monthly quota exceeded. Upgrade your plan."

rescue Thelawin::AuthenticationError => e
  puts "Invalid API key. Check your credentials."

rescue Thelawin::Error => e
  puts "API error: #{e.message}"
end

Best Practices

1. Use /v1/validate for Pre-Validation

Call /v1/validate before /v1/generate to catch errors early without consuming quota. This is especially useful in user-facing forms.

2. Implement Exponential Backoff

On 429 or transient 500 errors, wait progressively longer (1s, 2s, 4s, 8s...) between retries. Always respect the Retry-After header.

3. Use JSON-Path for Field-Level Errors

The path field (e.g., $.invoice.items[0].unit) pinpoints the exact location of the error. Use this to highlight invalid fields in your UI.

4. Log Full Error Details

Log the entire error response (including details array) for debugging. The message field contains human-readable descriptions.

5. Monitor Quota Usage

Set up alerts when approaching monthly quota limits. Check the Dashboard regularly or build a monitoring system that tracks 402 errors.

Need Help? See the Getting Started guide or Authentication Troubleshooting for more solutions.