Developers · Production
Before the live key goes in.
What the API guarantees, what it returns on every call, and what to do when a call does not succeed. No promises beyond what the code enforces.
- The key lives in a server-side environment variable;
sk_test_in development,sk_live_in production. - Every non-2xx is handled by
error.code, not by status alone. 4xx (except 429) is a request problem: fix, do not retry. 429waits forRetry-After;5xxretries with exponential backoff and a cap.X-Request-Idis logged for every call.- Requests time out on your side after 60 seconds; patching plus re-validation usually takes seconds, large files longer.
fixSummary.needsInput > 0is routed to a person or a form; the API will not invent business data.remainingIssuesafter patching means the file is re-exported from the source system, not delivered.- Tested with your own invoices from your main formats (UBL, CII, XRechnung, Peppol BIS), not only the samples.
- Quota is watched: GET /v1/usage costs nothing and returns the same counter as the dashboard.
| Status | Code | When | Retry |
|---|---|---|---|
| 400 | INVALID_JSON | The body is not valid JSON. | no |
| 400 | MISSING_XML | The "xml" field is missing. | no |
| 400 | INVALID_XML | The "xml" field is not a string. | no |
| 400 | XML_TOO_LARGE | The XML is larger than 10 MB. | no |
| 401 | UNAUTHORIZED | No Bearer header, or the key is unknown. | no |
| 401 | KEY_EXPIRED | An instant test key past its hour. | no |
| 402 | QUOTA_EXCEEDED | Monthly quota used up, or the 10 instant-key requests. details.upgradeUrl points to the Team checkout. | no |
| 403 | KEY_INACTIVE | The key was revoked. | no |
| 403 | INSUFFICIENT_TIER | A Team-only endpoint called with a Free key. | no |
| 429 | RATE_LIMITED | Too many requests in the sliding hour. Retry-After is set. | after Retry-After |
| 500 | INTERNAL_ERROR | The engine failed. X-Request-Id identifies the call. | with backoff |
| 503 | SERVICE_UNAVAILABLE | API authentication is not configured on the server. | with backoff |
The body is always { success: false, error: { code, message, details? } }. Validation findings are not errors: a failing invoice returns 200.
| Header | On | Meaning |
|---|---|---|
| X-Request-Id | every response | Identifier of this call. Log it; quote it when you write to us. |
| X-Processing-Time-Ms | every response | Server time spent on the call, in milliseconds. |
| X-RateLimit-Limit | every authenticated response | Requests allowed per sliding hour for this key. |
| X-RateLimit-Remaining | every authenticated response | Requests left in the current hour. |
| X-RateLimit-Reset | every authenticated response | Unix seconds at which the window resets. |
| Retry-After | 429 only | Seconds to wait before the next attempt. |
| X-Test-Mode | sk_test_ keys only | Set to "true"; the body carries meta.testMode as well. |
| X-Usage-Used | metered live responses | Requests counted this month, after this call. |
| X-Usage-Included | metered live responses | Requests included in the plan for the month. |
| X-Usage-Overage | metered live responses | Requests beyond the included amount (0 while quotas are hard caps). |
| X-Usage-Period-End | metered live responses | Date the month counter resets. |
The usage headers appear only on metered calls (live key, quota-tracking endpoint). Test keys and GET /v1/usage carry none; the counter itself is the response of GET /v1/usage.
const res = await fetch(url, init)
log.info('invoice-navigator', {
requestId: res.headers.get('X-Request-Id'),
ms: res.headers.get('X-Processing-Time-Ms'),
remaining: res.headers.get('X-RateLimit-Remaining'),
status: res.status,
})Retry 429 after Retry-After; retry 500, 502, 503 and 504 with backoff (start at one second, double, add jitter, stop at ten seconds and three attempts). Never retry 400, 401, 402 or 403: the same request will fail the same way.
async function validateWithRetry(xml, apiKey, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch('https://www.invoicenavigator.eu/api/v2/validate-and-fix', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ xml }),
})
if (res.ok) return res.json()
// 4xx other than 429 is a request problem: do not retry.
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
const { error } = await res.json()
throw new Error(`${error.code}: ${error.message} (${res.headers.get('X-Request-Id')})`)
}
if (attempt === maxRetries) throw new Error(`gave up after ${attempt + 1} attempts`)
const retryAfter = Number(res.headers.get('Retry-After'))
const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * 2 ** attempt, 10_000) + Math.random() * 500
await new Promise((r) => setTimeout(r, delay))
}
}| Key | Requests / hour | Requests / month |
|---|---|---|
| Free · sk_live_ | 60 | 100 |
| Team · sk_live_ | 100 | 5,000 |
| sk_test_ | 100 | not metered |
The hourly limit is a sliding window per key, not per IP: a burst is answered with 429 and resets continuously. The month counter answers 402 QUOTA_EXCEEDED with the counter and the Team checkout URL in error.details. POST /v2/validate-and-fix counts as 2; GET /v1/usage as 0.
/v1 and /v2 both run; nothing is deprecated. /v2 adds the composite call and a fixability-enriched validate. Pin rulesets per call with options.ruleset_versions on POST /v1/validate; the response reports the version that actually ran and warns when a pinned version is deprecated.
- Keys are stored as SHA-256 hashes. The plaintext is shown once; nobody at Invoice Navigator can read it back.
- Live calls store the invoice XML with the validation or flow record so that evidence packs and
fixWithInputwork. Test-key calls store no flow. - Evidence packs are signed; anyone with the id can check them at GET /v1/verify/{'{id}'} or /verify without a key.
- HTTPS only. There are no CORS headers on the API; call it from a server, not from a browser page.
- Questions on data handling or a processing agreement: hello@invoicenavigator.eu, answered within one working day.
Free keys are created on sign-up; Team is €49 a month for 5,000 requests, batch, evidence packs and conversion.