Error Codes
Understanding API errors and validation messages.
On This Page
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. |
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
detailsarray for specific validation issues - Use the
pathto identify which field caused the error - Display user-friendly messages from the
messagefield
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.