Configuration
KSeFClient accepts configuration options that control environment selection, HTTP behavior, retry logic, rate limiting, and security policies.
Client Options
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
| Option | Type | Default | Description |
|---|---|---|---|
environment | 'TEST' | 'DEMO' | 'PROD' | 'TEST' | KSeF environment (sets base URLs) |
baseUrl | string | Per environment | Override API base URL |
baseQrUrl | string | Per environment | Override QR verification base URL |
lighthouseUrl | string | Per environment | Override lighthouse status URL |
apiVersion | string | 'v2' | API version prefix |
timeout | number | 30000 | Request timeout in milliseconds |
customHeaders | Record<string, string> | {} | Additional headers on every request |
transport | TransportFn | Native fetch | Custom HTTP transport function |
retry | Partial<RetryPolicy> | See below | Retry policy overrides |
rateLimit | Partial<RateLimitConfig> | null | { globalRps: 10 } | Rate limit config, or null to disable |
circuitBreaker | Partial<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 |
presignedUrlHosts | string[] | — | Additional allowed hosts for presigned URLs |
authManager | AuthManager | DefaultAuthManager | Custom 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
Network errors (when retryNetworkErrors: true): ECONNRESET, ECONNREFUSED, ETIMEDOUT, UND_ERR_CONNECT_TIMEOUT, and AbortError (fetch timeout).
All HTTP methods are retried, including POST. This is safe because KSeF API operations are idempotent by design — for example, submitting the same invoice returns the same KSeF number.
Backoff formula
delay = min(baseDelayMs × 2^attempt + random(0, baseDelayMs), maxDelayMs)For 429 responses with a Retry-After header, the server-specified delay is used instead of the calculated backoff. Retry-After is parsed as seconds (integer) or HTTP-date.
Configuration
| Field | Type | Default | Description |
|---|---|---|---|
maxRetries | number | 3 | Maximum retry attempts |
baseDelayMs | number | 500 | Base delay in milliseconds |
maxDelayMs | number | 30000 | Maximum delay cap in milliseconds |
retryableStatusCodes | number[] | [429, 500, 502, 503, 504] | HTTP status codes to retry |
retryNetworkErrors | boolean | true | Retry on network errors |
Examples
// 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
| Field | Type | Default | Description |
|---|---|---|---|
globalRps | number | 10 | Global requests per second |
endpointLimits | Record<string, number> | {} | Per-endpoint RPS limits |
Examples
// 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.
| Endpoint | req/s | req/min |
|---|---|---|
POST /invoices/exports | 8 | 16 |
POST /invoices/query/metadata | 8 | 16 |
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
| Outcome | Counted? |
|---|---|
Network error (ECONNRESET, ETIMEDOUT, etc.) | Yes |
| 5xx response after retries exhausted | Yes |
| 429 Too Many Requests | No (throttling, not an outage) |
| 401 Unauthorized | No (auth problem, not availability) |
| 2xx / 4xx (other than 401/429) | No (records success) |
Configuration
| Field | Type | Default | Description |
|---|---|---|---|
failureThreshold | number | 5 | Consecutive failures within openMs before opening. Must be > 0. |
openMs | number | 30000 | Cooldown 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
// 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:
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)
- HTTPS enforcement — HTTP URLs are rejected (default: enabled)
- Host whitelist — Only URLs matching allowed host patterns are permitted. Wildcard patterns (
*.domain.com) match any subdomain. Default:['*.ksef.mf.gov.pl'] - Redirect parameter blocking — URLs containing
redirect,callback,return_url, ornextquery parameters are rejected (case-insensitive) - 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
- IPv4:
The first failing check throws KSeFValidationError.
Adding allowed hosts
Use presignedUrlHosts in client options to add hosts beyond the default *.ksef.mf.gov.pl:
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
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
// 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 }),
});