Skip to content

Configuration ​

KSeFClient accepts configuration options that control environment selection, HTTP behavior, retry logic, rate limiting, and security policies.


Client Options ​

ts
import { KSeFClient } from 'ksef-client-ts';

const client = new KSeFClient({
  environment: 'PROD',         // 'TEST' | 'DEMO' | 'PROD' (default: 'TEST')
  timeout: 60_000,             // Request timeout in ms (default: 30000)
  customHeaders: { 'X-Request-Id': 'abc' },
  transport: customFetchFn,    // Replace native fetch
  retry: { maxRetries: 5 },    // Partial retry policy overrides
  rateLimit: { globalRps: 20 },// Partial rate limit config (or null to disable)
  circuitBreaker: { failureThreshold: 5 }, // Opt-in circuit breaker (undefined/null disables)
  presignedUrlHosts: ['*.my-storage.com'], // Additional allowed hosts
  authManager: customAuthMgr,  // Custom AuthManager implementation
  errorFormat: 'problem-details', // 'problem-details' (default) | 'legacy'
});

KSeFClientOptions ​

OptionTypeDefaultDescription
environment'TEST' | 'DEMO' | 'PROD''TEST'KSeF environment (sets base URLs)
baseUrlstringPer environmentOverride API base URL
baseQrUrlstringPer environmentOverride QR verification base URL
lighthouseUrlstringPer environmentOverride lighthouse status URL
apiVersionstring'v2'API version prefix
timeoutnumber30000Request timeout in milliseconds
customHeadersRecord<string, string>{}Additional headers on every request
transportTransportFnNative fetchCustom HTTP transport function
retryPartial<RetryPolicy>See belowRetry policy overrides
rateLimitPartial<RateLimitConfig> | null{ globalRps: 10 }Rate limit config, or null to disable
circuitBreakerPartial<CircuitBreakerConfig> | null—Opt-in HTTP circuit breaker. undefined / omitted / null disables the policy entirely; pass a partial config (even {}) to enable with defaults merged in
presignedUrlHostsstring[]—Additional allowed hosts for presigned URLs
authManagerAuthManagerDefaultAuthManagerCustom auth/token manager
errorFormat'problem-details' | 'legacy''problem-details'Request format for server error bodies. 'legacy' suppresses the X-Error-Format header for older servers/proxies (KSeF API v2.4.0+)

Retry Policy ​

Failed requests are automatically retried with exponential backoff. The retry policy is merged with defaults — you only need to specify the fields you want to override.

What gets retried ​

HTTP status codes (default): 429, 500, 502, 503, 504. POST: 429 only.

Network errors (when retryNetworkErrors: true): ECONNRESET, ECONNREFUSED, ETIMEDOUT, UND_ERR_CONNECT_TIMEOUT, and AbortError (fetch timeout).

POST requests are retried on a 429 response, because the server rejected them without acting on them. They are not retried on 5xx, because the server may already have processed a request that ends in 500, 502, 503 or 504. After a thrown network error or timeout they are retried only when the connection was never established (ECONNREFUSED, UND_ERR_CONNECT_TIMEOUT). After ECONNRESET, ETIMEDOUT or AbortError the server may already have processed the request, so a retry could repeat its effect (open a second session, generate a second token, or get a duplicate-invoice error for an invoice that was accepted). Those errors are thrown to the caller instead. GET, PUT and DELETE are retried on every listed network error.

The exception is a POST that only reads data and uses POST because it carries a filter body: invoice metadata queries, permission and certificate queries, collective-identifier queries, the auth challenge and access-token refresh. These are marked with RestRequest.retrySafe() and are retried like a GET.

Backoff formula ​

delay = min(baseDelayMs × 2^attempt + random(0, baseDelayMs), maxDelayMs)

For 429 and 503 responses with a Retry-After header, the server-specified delay is used instead of the calculated backoff (maxDelayMs does not cap it). Retry-After is parsed as seconds or HTTP-date; a negative value or a date in the past means retry at once, and a malformed value falls back to backoff.

If the server asks for a longer wait than maxRetryAfterMs (default 120 s), the client does not sleep and stops retrying: a 429 is thrown at once as KSeFRateLimitError with the server's value in retryAfterSeconds, so you can schedule the retry yourself; a 503 is thrown as KSeFApiError.

Configuration ​

FieldTypeDefaultDescription
maxRetriesnumber3Maximum retry attempts
baseDelayMsnumber500Base delay in milliseconds
maxDelayMsnumber30000Maximum delay cap in milliseconds
retryableStatusCodesnumber[][429, 500, 502, 503, 504]HTTP status codes to retry
retryNetworkErrorsbooleantrueRetry on network errors
maxRetryAfterMsnumber120000Longest Retry-After the client waits out; a longer one fails at once. Capped at 2147483647 (~24.8 days, the timer limit), so Infinity means "wait up to that"

Examples ​

ts
// More aggressive retries
const client = new KSeFClient({
  retry: {
    maxRetries: 5,
    baseDelayMs: 1000,
    maxDelayMs: 60_000,
  },
});

// Add 409 Conflict to retryable codes
const client = new KSeFClient({
  retry: {
    retryableStatusCodes: [409, 429, 500, 502, 503, 504],
  },
});

// Disable retries entirely
const client = new KSeFClient({
  retry: { maxRetries: 0 },
});

Rate Limiting ​

A token bucket rate limiter prevents overwhelming the KSeF API. Requests exceeding the limit are delayed (not rejected).

How it works ​

  • Global bucket: All requests share a global tokens-per-second limit (default: 10 RPS)
  • Per-endpoint buckets: Optional per-endpoint limits (created lazily on first use)
  • A request must pass both the global bucket and its endpoint-specific bucket (if configured)
  • Rate limit is acquired once before the retry loop; on 429 retries, a token is re-acquired
  • Concurrency-safe via sequential promise chain

Configuration ​

FieldTypeDefaultDescription
globalRpsnumber10Global requests per second
endpointLimitsRecord<string, number>{}Per-endpoint RPS limits

Examples ​

ts
// Higher global limit
const client = new KSeFClient({
  rateLimit: { globalRps: 20 },
});

// Per-endpoint limits
const client = new KSeFClient({
  rateLimit: {
    globalRps: 15,
    endpointLimits: {
      '/v2/online/Invoice/Send': 5,
    },
  },
});

// Disable rate limiting entirely
const client = new KSeFClient({
  rateLimit: null,
});

KSeF server-side limits (v2.4.0) ​

The KSeF API enforces its own per-endpoint caps. If you opt into client-side back-pressure via endpointLimits, use the values below to match the current server limits. The library does not apply these defaults automatically — the server enforces them regardless.

Endpointreq/sreq/min
POST /invoices/exports816
POST /invoices/query/metadata816

Prior to KSeF API v2.4.0, POST /invoices/exports was capped at 4 req/s and 8 req/min; v2.4.0 aligned it with query/metadata. The client-side token bucket only supports a per-second window — minute-level ceilings are enforced server-side only, so keep globalRps at or below the per-second cap to stay within the minute budget under sustained load.


Circuit Breaker ​

Opt-in HTTP circuit breaker that fails fast when the upstream is known to be unavailable. Sits above the retry loop: once open, every matching request raises KSeFCircuitOpenError without hitting the network, so a batch of requests during an outage consumes one retry budget instead of one-per-request. When the cooldown elapses, the breaker lets a single probe through; on success it clears, on failure it re-opens. See HTTP Resilience — Circuit Breaker for the state machine and retry-loop interaction.

The feature is off by default. Pass a (possibly empty) partial config to enable it; null and undefined both disable.

What counts as a failure ​

OutcomeCounted?
Network error (ECONNRESET, ETIMEDOUT, etc.)Yes
5xx response after retries exhaustedYes
429 Too Many RequestsNo (throttling, not an outage)
401 UnauthorizedNo (auth problem, not availability)
2xx / 4xx (other than 401/429)No (records success)

Configuration ​

FieldTypeDefaultDescription
failureThresholdnumber5Consecutive failures within openMs before opening. Must be > 0.
openMsnumber30000Cooldown window in ms. Requests fail fast while open; one probe is allowed after it elapses. Must be > 0.
scope'global' | 'endpoint''global''global' uses one breaker for the whole client. 'endpoint' keeps per-endpoint state so an outage on one route leaves the others alone.

Examples ​

ts
// Enable with defaults (threshold 5, cooldown 30s, global scope)
const client = new KSeFClient({
  circuitBreaker: {},
});

// Aggressive: open sooner, stay open longer, per-endpoint scope
const client = new KSeFClient({
  circuitBreaker: {
    failureThreshold: 3,
    openMs: 60_000,
    scope: 'endpoint',
  },
});

// Explicitly disable (same as omitting the option)
const client = new KSeFClient({
  circuitBreaker: null,
});

Handle the new error class where it makes sense:

ts
import { KSeFCircuitOpenError } from 'ksef-client-ts';

try {
  await client.invoices.sendInvoice(xml);
} catch (err) {
  if (err instanceof KSeFCircuitOpenError) {
    // Park the work for later; don't retry-storm a known-down upstream
    await parkForLater(xml, err.retryAfterMs);
    return;
  }
  throw err;
}

Presigned URL Policy ​

When downloading files from presigned URLs returned by the KSeF API (e.g., batch exports, UPO downloads), the library validates URLs against a security policy to prevent SSRF attacks.

Security checks (in order) ​

  1. HTTPS enforcement — HTTP URLs are rejected (default: enabled)
  2. Host whitelist — Only URLs matching allowed host patterns are permitted. Wildcard patterns (*.domain.com) match any subdomain. Default: ['*.ksef.mf.gov.pl']
  3. Redirect parameter blocking — URLs containing redirect, callback, return_url, or next query parameters are rejected (case-insensitive)
  4. Private IP rejection — URLs targeting private/reserved IPs are rejected:
    • IPv4: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16
    • IPv6: ::1, fc00::/7, fe80::/10

The first failing check throws KSeFValidationError.

Adding allowed hosts ​

Use presignedUrlHosts in client options to add hosts beyond the default *.ksef.mf.gov.pl:

ts
const client = new KSeFClient({
  presignedUrlHosts: ['*.my-corporate-storage.com', 'cdn.example.com'],
});

The additional hosts are merged with the defaults — you don't need to repeat *.ksef.mf.gov.pl.


Custom Transport ​

Replace the default fetch with a custom transport function for testing, logging, or proxying.

Type signature ​

ts
type TransportFn = (url: string, init: RequestInit) => Promise<Response>;

The transport receives the fully-constructed URL and RequestInit (including method, headers, body, and AbortSignal for timeouts).

Examples ​

ts
// Logging transport
const client = new KSeFClient({
  transport: async (url, init) => {
    console.log(`${init.method} ${url}`);
    const start = Date.now();
    const res = await fetch(url, init);
    console.log(`${res.status} in ${Date.now() - start}ms`);
    return res;
  },
});

// Mock transport for tests
const client = new KSeFClient({
  transport: async (url, init) => {
    return new Response(JSON.stringify({ ok: true }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    });
  },
});

// Proxy transport
import { ProxyAgent } from 'undici';

const agent = new ProxyAgent('http://proxy.corp.com:8080');
const client = new KSeFClient({
  transport: (url, init) => fetch(url, { ...init, dispatcher: agent }),
});

Released under the MIT License.