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-Idheader logged on every request for debugging - ☐Timeout handling for requests > 30 seconds
Monitoring
- ☐
X-Processing-Time-Mstracked (alert if P95 > 2s) - ☐Error rate tracked (alert if > 1% of requests are 5xx)
- ☐Quota usage monitored via
X-Usage-Used/X-Usage-Includedheaders - ☐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.
| Status | Code |
|---|---|
| 400 | INVALID_JSON |
| 400 | MISSING_XML |
| 400 | INVALID_XML |
| 400 | XML_TOO_LARGE |
| 401 | UNAUTHORIZED |
| 402 | QUOTA_EXCEEDED |
| 403 | INSUFFICIENT_TIER |
| 429 | RATE_LIMIT_EXCEEDED |
| 429 | TEST_RATE_LIMIT_EXCEEDED |
| 500 | INTERNAL_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.
| Header | Description |
|---|---|
X-Request-Id | Unique request identifier. Log this. Include in support requests. |
X-Processing-Time-Ms | Server processing time in milliseconds. Track for latency monitoring. |
X-RateLimit-Limit | Your hourly request limit |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix 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):
| Header | Description |
|---|---|
X-Usage-Used | Validations used this billing period |
X-Usage-Included | Total validations included in your plan |
X-Usage-Overage | Overage validations beyond included amount |
X-Usage-Period-End | When current billing period ends |
Retry Strategy
Which errors to retry
- ✓
429— Always retry. UseRetry-Afterheader. - ✓
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
| Tier | Requests/hour |
|---|---|
| Free trial | 10 |
| 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
Need higher limits or a custom SLA? Contact our team