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.

Go-live list
  • 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.
  • 429 waits for Retry-After; 5xx retries with exponential backoff and a cap.
  • X-Request-Id is 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 > 0 is routed to a person or a form; the API will not invent business data.
  • remainingIssues after 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.
Error reference
StatusCodeWhenRetry
400INVALID_JSONThe body is not valid JSON.no
400MISSING_XMLThe "xml" field is missing.no
400INVALID_XMLThe "xml" field is not a string.no
400XML_TOO_LARGEThe XML is larger than 10 MB.no
401UNAUTHORIZEDNo Bearer header, or the key is unknown.no
401KEY_EXPIREDAn instant test key past its hour.no
402QUOTA_EXCEEDEDMonthly quota used up, or the 10 instant-key requests. details.upgradeUrl points to the Team checkout.no
403KEY_INACTIVEThe key was revoked.no
403INSUFFICIENT_TIERA Team-only endpoint called with a Free key.no
429RATE_LIMITEDToo many requests in the sliding hour. Retry-After is set.after Retry-After
500INTERNAL_ERRORThe engine failed. X-Request-Id identifies the call.with backoff
503SERVICE_UNAVAILABLEAPI 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.

Response headers
HeaderOnMeaning
X-Request-Idevery responseIdentifier of this call. Log it; quote it when you write to us.
X-Processing-Time-Msevery responseServer time spent on the call, in milliseconds.
X-RateLimit-Limitevery authenticated responseRequests allowed per sliding hour for this key.
X-RateLimit-Remainingevery authenticated responseRequests left in the current hour.
X-RateLimit-Resetevery authenticated responseUnix seconds at which the window resets.
Retry-After429 onlySeconds to wait before the next attempt.
X-Test-Modesk_test_ keys onlySet to "true"; the body carries meta.testMode as well.
X-Usage-Usedmetered live responsesRequests counted this month, after this call.
X-Usage-Includedmetered live responsesRequests included in the plan for the month.
X-Usage-Overagemetered live responsesRequests beyond the included amount (0 while quotas are hard caps).
X-Usage-Period-Endmetered live responsesDate 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.

Log the three that matter
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,
})
Retries

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.

JavaScript
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))
  }
}
Rate limits and quotas
KeyRequests / hourRequests / month
Free · sk_live_60100
Team · sk_live_1005,000
sk_test_100not 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.

Versions

/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.

What the API stores
  • 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 fixWithInput work. 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.
Live key

Free keys are created on sign-up; Team is €49 a month for 5,000 requests, batch, evidence packs and conversion.