Back to API Docs

Production Guide

Everything you need before deploying Invoice Navigator to production.

Go-Live Checklist

Complete these checks before switching from sk_test_ to sk_live_.

Security

  • API key stored in environment variable, never in client-side code or repos
  • Using sk_test_ for development, sk_live_ for production
  • API key rotation plan documented internally
  • Webhook endpoint uses HTTPS

Integration

  • Error handling covers all error codes (see Error Reference below)
  • Retry logic with exponential backoff implemented for 429 and 5xx
  • X-Request-Id header logged on every request for debugging
  • Timeout handling for requests > 30 seconds

Monitoring

  • X-Processing-Time-Ms tracked (alert if P95 > 2s)
  • Error rate tracked (alert if > 1% of requests are 5xx)
  • Quota usage monitored via X-Usage-Used / X-Usage-Included headers
  • Rate limit remaining tracked via X-RateLimit-Remaining

Testing

  • Full integration test suite passes against sk_test_ key
  • Tested with all invoice formats you'll encounter (UBL, CII, XRechnung, ZUGFeRD)
  • Error scenarios tested (invalid XML, oversized payloads, auth failures)
  • Rate limit behavior verified

Error Reference

Complete catalog of all error codes the API returns.

StatusCode
400INVALID_JSON
400MISSING_XML
400INVALID_XML
400XML_TOO_LARGE
401UNAUTHORIZED
402QUOTA_EXCEEDED
403INSUFFICIENT_TIER
429RATE_LIMIT_EXCEEDED
429TEST_RATE_LIMIT_EXCEEDED
500INTERNAL_ERROR

Error response shape (same for all errors):

{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Try again in 45 seconds.",
    "details": {
      "limit": 100,
      "remaining": 0,
      "resetAt": "2026-02-28T14:30:00Z"
    }
  }
}

4xx errors are client errors — fix the request, don't retry blindly. 429 and 5xx errors are retryable — see Retry Strategy below.

Response Headers

These headers are returned on every successful response.

HeaderDescription
X-Request-IdUnique request identifier. Log this. Include in support requests.
X-Processing-Time-MsServer processing time in milliseconds. Track for latency monitoring.
X-RateLimit-LimitYour hourly request limit
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp (seconds) when the window resets
Retry-After(Only on 429) Seconds to wait before retrying
X-Test-Mode(Only on test key requests) Confirms test mode is active

Usage headers (on responses that consume quota):

HeaderDescription
X-Usage-UsedValidations used this billing period
X-Usage-IncludedTotal validations included in your plan
X-Usage-OverageOverage validations beyond included amount
X-Usage-Period-EndWhen current billing period ends

Retry Strategy

Which errors to retry

  • 429 — Always retry. Use Retry-After header.
  • 500 — Retry with exponential backoff. Likely transient.
  • 502, 503, 504 — Retry with exponential backoff. Infrastructure issue.
  • 400, 401, 402, 403 — Do NOT retry. Fix the request.

Recommended backoff

  • Base delay: 1 second
  • Multiplier: 2x (exponential)
  • Jitter: random 0–500ms added to each delay
  • Max retries: 3
  • Max delay: 10 seconds
JavaScript
async function validateWithRetry(xml, apiKey, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(
      'https://api.invoicenavigator.eu/api/v2/validate-and-fix',
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ xml }),
      }
    );

    if (response.ok) return response.json();

    // Don't retry client errors (except rate limits)
    if (response.status >= 400 && response.status < 500
        && response.status !== 429) {
      const body = await response.json();
      throw new Error(`Client error ${response.status}: ${body.error.message}`);
    }

    if (attempt === maxRetries) {
      throw new Error(`Failed after ${maxRetries + 1} attempts`);
    }

    // Use Retry-After header if present, otherwise exponential backoff
    const retryAfter = response.headers.get('Retry-After');
    const delay = retryAfter
      ? parseInt(retryAfter) * 1000
      : Math.min(1000 * Math.pow(2, attempt), 10000)
        + Math.random() * 500;

    await new Promise(resolve => setTimeout(resolve, delay));
  }
}
Python
import requests
import time
import random

def validate_with_retry(xml: str, api_key: str, max_retries: int = 3):
    for attempt in range(max_retries + 1):
        response = requests.post(
            "https://api.invoicenavigator.eu/api/v2/validate-and-fix",
            headers={"Authorization": f"Bearer {api_key}"},
            json={"xml": xml},
        )

        if response.ok:
            return response.json()

        # Don't retry client errors (except rate limits)
        if 400 <= response.status_code < 500 and response.status_code != 429:
            raise Exception(
                f"Client error {response.status_code}: "
                f"{response.json()['error']['message']}"
            )

        if attempt == max_retries:
            raise Exception(f"Failed after {max_retries + 1} attempts")

        # Use Retry-After header if present, otherwise exponential backoff
        retry_after = response.headers.get("Retry-After")
        if retry_after:
            delay = int(retry_after)
        else:
            delay = min(2 ** attempt, 10) + random.uniform(0, 0.5)

        time.sleep(delay)

Rate Limits

How rate limits work

  • Sliding window algorithm (not fixed windows — requests don't all reset at once)
  • Window: 1 hour
  • Limits are per API key, not per IP
TierRequests/hour
Free trial10
Starter (€249/mo)100
Pro (€499/mo)1,000
Scale (€999/mo)10,000
Test mode (sk_test_)100 (fixed)

Reading the rate limit headers

X-RateLimit-Limit: 100        ← Your hourly limit
X-RateLimit-Remaining: 23     ← Requests left in this window
X-RateLimit-Reset: 1709125200 ← When the window resets (Unix seconds)

Time until reset

const resetAt = parseInt(response.headers.get('X-RateLimit-Reset')) * 1000;
const msUntilReset = resetAt - Date.now();

Rate limit vs. quota

  • Rate limit = requests per hour (prevents bursts). Resets every hour.
  • Quota = validations per billing period (prevents overuse). Resets monthly.
  • You can hit a rate limit without exceeding quota, and vice versa.

Monitoring Recommendations

Key metrics

  • X-Processing-Time-Ms — P50 should be <500ms, P95 <2s, P99 <5s. Alert if P95 exceeds 3s.
  • Error rate (5xx responses / total) — Should be <0.1%. Alert if >1%.
  • X-RateLimit-Remaining — Alert when <10% of limit remaining.
  • X-Usage-Used / X-Usage-Included — Alert at 80% quota consumed.

Logging best practice

Log X-Request-Id on every API call. If something goes wrong, include it in your support request — it lets us trace the exact request through our systems.

JavaScript
const response = await fetch(url, options);
const requestId = response.headers.get('X-Request-Id');
const processingTime = response.headers.get('X-Processing-Time-Ms');

logger.info('Invoice Navigator API call', {
  requestId,
  processingTimeMs: processingTime,
  status: response.status,
  endpoint: '/v2/validate-and-fix',
});

Versioning & Stability

  • v1 endpoints (/api/v1/validate) — Stable. Validation only.
  • v2 endpoints (/api/v2/validate-and-fix) — Stable. Validation + remediation + evidence packs.
  • No breaking changes without 90 days notice and a migration guide.
  • Deprecation policy: v1 is not deprecated. Both versions run in production.

Data & Security

  • All data processed and stored in the EU (Vercel EU region)
  • Invoice XML is processed in memory, stored encrypted at rest
  • Evidence packs retained for 12 months (configurable on Scale tier)
  • HTTPS enforced on all endpoints
  • API keys are hashed (SHA-256) before storage — we can't see your key
  • CORS enabled (Access-Control-Allow-Origin: *) for browser-based tools
  • For GDPR data requests or DPA agreements: contact team@invoicenavigator.eu

Ready to go live?

Get your production API key and start processing invoices.

Get Your Production Key

Need higher limits or a custom SLA? Contact our team