Skip to content

Error Handling & Problem Details ​

Comprehensive guide to error types, RFC 7807 Problem Details, error dispatch, and programmatic handling patterns in the KSeF TypeScript client.


Overview ​

KSeF is Poland's National e-Invoice System -- a government tax system where errors have real consequences. A failed invoice submission may require re-sending before a legal deadline. A 403 may indicate a permissions misconfiguration that blocks an entire organization's invoicing. A rate limit hit during a batch export can stall downstream accounting processes.

The library provides a structured error hierarchy so that callers can react precisely to each failure mode:

  • Server errors (KSeFApiError, KSeFRateLimitError) carry the HTTP status code and parsed response body.
  • Auth errors (KSeFUnauthorizedError, KSeFForbiddenError) carry RFC 7807 Problem Details with machine-readable reason codes.
  • Retention errors (KSeFGoneError) signal that a server-side async operation status has aged out (HTTP 410).
  • Client-side errors (KSeFValidationError) catch invalid requests before they reach the network.
  • Workflow errors (KSeFAuthStatusError, KSeFSessionExpiredError, KSeFSessionFailedError, KSeFInvoiceRejectedError) represent higher-level failures in multi-step operations.

All error classes extend a common base KSeFError, so a single instanceof KSeFError catch covers every library error without catching unrelated exceptions.

This holds across the package's entry points (ksef-client-ts, ksef-client-ts/node, ksef-client-ts/pdf), which are bundled separately and therefore carry their own copies of the classes: KSeFError recognises its own kind by a registered symbol rather than by prototype. A subclass check is per entry point, so to tell one failure apart from another in the PDF module — an invalid template from a missing pdfmake, say — import KSeFValidationError and KSeFPdfError from ksef-client-ts/pdf.


Error Hierarchy ​

Error (built-in)
  └── KSeFError                          src/errors/ksef-error.ts
        ├── KSeFApiError                 src/errors/ksef-api-error.ts         (any non-2xx HTTP)
        │     ├── KSeFBadRequestError    src/errors/ksef-bad-request-error.ts (400 + RFC 7807)
        │     ├── KSeFUnauthorizedError  src/errors/ksef-unauthorized-error.ts (401 + RFC 7807)
        │     ├── KSeFForbiddenError     src/errors/ksef-forbidden-error.ts   (403 + RFC 7807)
        │     ├── KSeFGoneError          src/errors/ksef-gone-error.ts        (410 + RFC 7807, retention expired)
        │     ├── KSeFRateLimitError     src/errors/ksef-rate-limit-error.ts  (429 + RFC 7807)
        │     ├── KSeFBatchTimeoutError  src/errors/ksef-batch-timeout-error.ts (KSeF code 21208)
        │     ├── KSeFUnknownPublicKeyError
        │     │                          src/errors/ksef-unknown-public-key-error.ts (400, KSeF code 21470)
        │     └── KSeFSessionUnavailableError
        │                                src/errors/ksef-session-unavailable-error.ts (400, KSeF code 21184)
        ├── KSeFAuthStatusError          src/errors/ksef-auth-status-error.ts (auth ceremony failed)
        ├── KSeFSessionExpiredError      src/errors/ksef-session-expired-error.ts (stored session expired)
        ├── KSeFSessionFailedError       src/errors/ksef-session-failed-error.ts (session ended in a failed status)
        ├── KSeFInvoiceRejectedError     src/errors/ksef-invoice-rejected-error.ts (KSeF rejected a sent invoice)
        ├── KSeFCircuitOpenError         src/errors/ksef-circuit-open-error.ts (circuit breaker is open)
        ├── KSeFValidationError          src/errors/ksef-validation-error.ts  (client-side validation)
        ├── KSeFXsdValidationError       src/errors/ksef-xsd-validation-error.ts (XSD schema validation)
        ├── KSeFMetadataPaginationError  src/errors/ksef-metadata-pagination-error.ts (paging cannot advance)
        └── KSeFPaginationError          src/errors/ksef-pagination-error.ts (continuation-token paging cannot finish)

All server-returned HTTP errors extend KSeFApiError, so a single instanceof KSeFApiError catch handles every response-side failure. The KSeFApiProblem union type (see Exhaustive dispatch) narrows through the five RFC 7807 subclasses for exhaustive switch / assertNever patterns.

Three KSeFApiError subclasses sit outside that union, because they are selected by KSeF error code rather than by status code: KSeFBatchTimeoutError (code 21208), KSeFUnknownPublicKeyError (code 21470) and KSeFSessionUnavailableError (code 21184). Note that KSeFUnknownPublicKeyError and KSeFSessionUnavailableError are thrown instead of KSeFBadRequestError on a 400 carrying their code -- catching KSeFBadRequestError alone will not see them. Catch KSeFApiError if you want all of them.

All exports: src/errors/index.ts re-exports every error class, type, and interface. Import from the package root:

typescript
import {
  KSeFError,
  KSeFApiError,
  KSeFBadRequestError,
  KSeFRateLimitError,
  KSeFUnauthorizedError,
  KSeFForbiddenError,
  KSeFGoneError,
  KSeFAuthStatusError,
  KSeFSessionExpiredError,
  KSeFSessionFailedError,
  KSeFInvoiceRejectedError,
  KSeFValidationError,
  KSeFBatchTimeoutError,
  KSeFCircuitOpenError,
  KSeFUnknownPublicKeyError,
  KSeFSessionUnavailableError,
  KSeFXsdValidationError,
  KSeFMetadataPaginationError,
  KSeFPaginationError,
  KSeFErrorCode,
  type KSeFApiProblem,
  assertNever,
} from 'ksef-client-ts';

RFC 7807 Problem Details ​

KSeF returns structured error bodies for 400, 401, 403, 410, and 429 responses following RFC 7807 (Problem Details for HTTP APIs). These bodies provide machine-readable fields that go beyond a simple status code.

The client asks for Problem Details on every outgoing request via the X-Error-Format: problem-details header. Set errorFormat: 'legacy' in client options to suppress the header and receive legacy ApiErrorResponse bodies from older servers or proxies.

typescript
const client = new KSeFClient({
  environment: 'TEST',
  // errorFormat: 'legacy',  // opt out, rarely needed
});

400 Bad Request -- BadRequestProblemDetails ​

File: src/errors/types.ts

json
{
  "title": "Bad Request",
  "status": 400,
  "detail": "Request validation failed",
  "instance": "/v2/invoices/query",
  "errors": [
    { "code": 21200, "description": "Invalid date range", "details": ["dateFrom > dateTo"] },
    { "code": 21201, "description": "Unknown subject type", "details": [] }
  ],
  "traceId": "abc-123-def-456",
  "timestamp": "2026-04-18T10:15:30Z"
}
FieldTypeDescription
titlestringShort human-readable summary
statusnumberAlways 400
detailstring?Human-readable summary of what failed
instancestring?The API endpoint path that was called
errorsBadRequestErrorDetail[]Structured list of individual validation failures
traceIdstring?Server-side trace ID for correlating with KSeF support
timestampstring?UTC timestamp recorded by the server

Each entry in errors has code: number, description?: string, and details?: string[] | null — KSeF sometimes sends an entry with only its code, and such a body is still read as Problem Details. The client throws KSeFBadRequestError whose .errors field is ready for display or programmatic routing by code.

When the server returns a legacy 400 body (older server or proxy), the client falls back to generic KSeFApiError with statusCode === 400. Both cases are caught by instanceof KSeFApiError.

401 Unauthorized -- UnauthorizedProblemDetails ​

File: src/errors/types.ts

json
{
  "title": "Unauthorized",
  "status": 401,
  "detail": "Token has expired",
  "instance": "/v2/online/Session/Open",
  "traceId": "abc-123-def-456",
  "timestamp": "2026-04-12T10:15:30Z"
}
FieldTypeDescription
titlestringShort human-readable summary (e.g., "Unauthorized")
statusnumberAlways 401
detailstringSpecific explanation of why the request was rejected
instancestring?The API endpoint path that was called
traceIdstring?Server-side trace ID for correlating with KSeF support
timestampstring?UTC timestamp recorded by the server when the error was generated (KSeF API v2.4.0+)

403 Forbidden -- ForbiddenProblemDetails ​

File: src/errors/types.ts

json
{
  "title": "Forbidden",
  "status": 403,
  "detail": "Subject does not have required permissions for this operation",
  "instance": "/v2/online/Invoice/Send",
  "reasonCode": "missing-permissions",
  "security": {
    "requiredAnyOfPermissions": ["InvoiceWrite", "InvoiceRead"],
    "presentPermissions": ["SessionOwn"]
  },
  "traceId": "abc-123-def-456",
  "timestamp": "2026-04-12T10:15:30Z"
}
FieldTypeDescription
titlestringShort human-readable summary
statusnumberAlways 403
detailstringSpecific explanation of the denial
instancestring?The API endpoint path
reasonCodeForbiddenReasonCodeMachine-readable reason (see table below)
securityForbiddenSecurityInfo & Record<string, unknown>?Typed requiredAnyOfPermissions / presentPermissions lists plus any forward-compat fields
traceIdstring?Server-side trace ID
timestampstring?UTC timestamp recorded by the server when the error was generated (KSeF API v2.4.0+)

ForbiddenReasonCode values ​

Type: src/errors/types.ts

ValueMeaningHow to react
missing-permissionsSubject lacks the required KSeF permission for this operationGrant the missing permission via the KSeF portal or PermissionService
ip-not-allowedRequest originates from an IP not in the session's allowed rangeCheck your network configuration; re-open session from an allowed IP
insufficient-resource-accessSubject can access the endpoint but not this specific resourceVerify the NIP/invoice reference belongs to the authenticated subject
auth-method-not-allowedThe authentication method used is not permitted for this operationSwitch to a different auth method (e.g., certificate instead of token)
security-service-blockedKSeF security subsystem has blocked the requestContact KSeF support with the traceId
context-type-not-allowedThe session's context type does not allow this operationOpen a session with the correct context identifier type

The type also includes (string & {}) to allow for future reason codes not yet enumerated.

410 Gone -- GoneProblemDetails ​

File: src/errors/types.ts

Returned when polling an async operation status whose server-side record has aged out of KSeF retention windows. KSeF API v2.4.0 enforces 7-day retention for authentication and export operation status, and 30-day retention for certificate and permission enrollment status.

json
{
  "title": "Gone",
  "status": 410,
  "detail": "Operation status no longer available (retention expired)",
  "instance": "/v2/auth/ref-123",
  "traceId": "abc-123-def-456",
  "timestamp": "2026-04-12T10:00:00Z"
}
FieldTypeDescription
titlestringShort human-readable summary
statusnumberAlways 410
detailstringServer's explanation (typically references retention expiry)
instancestring?The API endpoint path
traceIdstring?Server-side trace ID
timestampstring?UTC timestamp recorded by the server

429 Too Many Requests -- TooManyRequestsProblemDetails ​

File: src/errors/types.ts

json
{
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Daily quota exceeded",
  "instance": "/v2/sessions",
  "traceId": "abc-123-def-456",
  "timestamp": "2026-04-18T10:00:00Z"
}

Surfaced on KSeFRateLimitError.problem. The existing retryAfterSeconds / retryAfterDate / recommendedDelay fields (parsed from the Retry-After header) remain populated — the Problem Details body is additive context for logging and support correlation.

typescript
catch (err) {
  if (err instanceof KSeFRateLimitError) {
    await sleep((err.recommendedDelay ?? 60) * 1000);
    if (err.problem?.traceId) {
      logger.warn({ traceId: err.problem.traceId }, 'rate limited');
    }
  }
}

Exhaustive dispatch with KSeFApiProblem ​

The union type KSeFApiProblem lets you switch over every Problem Details error with compile-time safety:

typescript
import { type KSeFApiProblem, assertNever, KSeFApiError } from 'ksef-client-ts';

function describe(err: KSeFApiProblem): string {
  switch (err.statusCode) {
    case 400: return `Validation: ${err.errors.length} error(s)`;
    case 401: return 'Authenticate and retry';
    case 403: return `Forbidden: ${err.reasonCode}`;
    case 410: return 'Operation status expired';
    case 429: return `Rate limited, retry in ${err.recommendedDelay}s`;
    default: return assertNever(err);
  }
}

try {
  await client.invoices.queryInvoiceMetadata(filters);
} catch (err) {
  if (err instanceof KSeFApiError && 'statusCode' in err) {
    // Narrow to KSeFApiProblem via instanceof checks before calling describe().
  }
}

Adding a new RFC 7807 subclass to the union causes assertNever to fail type-checking at every dispatch site until the new case is handled.


Error Dispatch Flow ​

File: src/http/rest-client.ts, method ensureSuccess()

After the retry loop is exhausted and a non-2xx response remains, ensureSuccess() reads the response body once as text and attempts to parse it as JSON. It then dispatches errors in a fixed priority order:

text
Response not OK?
  │
  ├── status 400 → try BadRequestProblemDetails guard
  │                → matches → errors[].code 21184 → throw KSeFSessionUnavailableError
  │                            errors[].code 21470 → throw KSeFUnknownPublicKeyError
  │                            otherwise → throw new KSeFBadRequestError(problem)
  │                → else legacy ApiErrorResponse; by KSeF exceptionCode
  │                  21184 → throw KSeFSessionUnavailableError
  │                  21470 → throw KSeFUnknownPublicKeyError
  │                  21208 → throw KSeFBatchTimeoutError
  │                  otherwise throw KSeFApiError.fromResponse()
  │
  ├── status 429 → try TooManyRequestsProblemDetails guard
  │                → if matches, pass to KSeFRateLimitError.fromRetryAfterHeader()
  │                  alongside the Retry-After header
  │                → else fall back to legacy TooManyRequestsResponse body
  │                → throw KSeFRateLimitError (always carries retry metadata)
  │
  ├── status 401 → parse as UnauthorizedProblemDetails
  │                → if body has .detail → throw new KSeFUnauthorizedError(body)
  │                  (if no .detail, falls through to generic)
  │
  ├── status 403 → parse as ForbiddenProblemDetails
  │                → if body has .reasonCode → throw new KSeFForbiddenError(body)
  │                  (if no .reasonCode, falls through to generic)
  │
  ├── status 410 → parse as GoneProblemDetails
  │                → throw new KSeFGoneError(body)
  │                  (if body is empty, throws with default retention-expired message)
  │
  └── any other status → parse as ApiErrorResponse
                         → if body carries KSeF exceptionCode 21208
                             → throw KSeFBatchTimeoutError.fromResponse()
                         → otherwise throw KSeFApiError.fromResponse()
                           (generic fallback for all unhandled status codes)

Why the order matters: The 429 check runs first because a rate-limited response might also contain fields like detail. It should always be treated as rate limiting, not as an auth error. Each check is exclusive -- once a specific error is thrown, no further checks run.

Body reading: The body is consumed exactly once via response.text(). The parseJson<T>() helper then attempts JSON.parse() on that text. If parsing fails (e.g., the server returned HTML), the raw text is silently discarded and the error is constructed without a parsed body.

Before ensureSuccess(): automatic recovery ​

The retry loop in sendRequest() (lines 71-131) handles certain errors transparently before they reach ensureSuccess():

  1. 429 / 5xx responses are retried up to maxRetries times with exponential backoff. POST requests are retried on 429 only.
  2. 401 on first attempt triggers an auth token refresh via AuthManager.onUnauthorized(). If refresh succeeds, the request is retried once with the new token.
  3. Network errors (ECONNRESET, ETIMEDOUT, etc.) are retried with backoff. POST requests are retried only if the connection was never established, because the server may already have processed them.

Only after all retries are exhausted does the response reach ensureSuccess(). This means callers only see errors that could not be recovered automatically.


Error Classes in Detail ​

KSeFError ​

File: src/errors/ksef-error.ts

Base class for all library errors. Extends Error with name = 'KSeFError'.

typescript
class KSeFError extends Error {
  constructor(message: string);
}

Use instanceof KSeFError to catch any error thrown by this library while letting unrelated errors propagate.


KSeFApiError ​

File: src/errors/ksef-api-error.ts

Generic error for any non-2xx HTTP response that is not handled by a more specific class.

typescript
class KSeFApiError extends KSeFError {
  readonly statusCode: number;
  readonly errorResponse?: ApiErrorResponse;

  static fromResponse(statusCode: number, body?: ApiErrorResponse): KSeFApiError;
}
FieldTypeDescription
statusCodenumberHTTP status code (e.g., 400, 404, 500)
errorResponseApiErrorResponse?Parsed KSeF error body (see below)
messagestringJoined exceptionDescription values, or "KSeF API error: HTTP {status}"

ApiErrorResponse structure ​

Type: src/errors/types.ts

typescript
interface ApiErrorResponse {
  exception?: {
    serviceCtx?: string;
    serviceCode?: string;
    serviceName?: string;
    timestamp?: string;
    referenceNumber?: string;
    exceptionDetailList?: ExceptionDetails[];
  };
}

interface ExceptionDetails {
  exceptionCode?: number;
  exceptionDescription?: string | null;
  details?: string[] | null;
}

Example KSeF error body:

json
{
  "exception": {
    "serviceCtx": "srvTXN",
    "serviceCode": "20230201-EX-B8FCA03125-E7",
    "serviceName": "online.invoice.send",
    "timestamp": "2026-03-28T10:30:00.000Z",
    "referenceNumber": "20230201-SE-ABC123",
    "exceptionDetailList": [
      {
        "exceptionCode": 21001,
        "exceptionDescription": "Invalid invoice XML schema",
        "details": ["Element 'P_1' is not valid"]
      }
    ]
  }
}

The fromResponse() factory joins all exceptionDescription values with "; " to build the error message. If no descriptions are present, it falls back to "KSeF API error: HTTP {statusCode}".


KSeFRateLimitError ​

File: src/errors/ksef-rate-limit-error.ts

Thrown when KSeF returns HTTP 429 (Too Many Requests). Extends KSeFApiError so it also carries statusCode and errorResponse.

typescript
class KSeFRateLimitError extends KSeFApiError {
  readonly retryAfterSeconds?: number;
  readonly retryAfterDate?: Date;
  readonly recommendedDelay: number;  // seconds, defaults to 60

  static fromRetryAfterHeader(
    statusCode: number,
    retryAfterHeader?: string | null,
    body?: ApiErrorResponse,
  ): KSeFRateLimitError;
}
FieldTypeDescription
retryAfterSecondsnumber?Parsed Retry-After value in seconds
retryAfterDateDate?If Retry-After was an HTTP-date, the parsed Date
recommendedDelaynumberretryAfterSeconds if available, otherwise 60 (seconds)

Retry-After header parsing ​

The fromRetryAfterHeader() factory parses the header in two formats:

  1. Seconds: Retry-After: 120 -- parsed as integer, stored in retryAfterSeconds.
  2. HTTP-date: Retry-After: Thu, 28 Mar 2026 12:00:00 GMT -- parsed as Date, delta from now stored in retryAfterSeconds.

A negative value is treated as 0. If the header is missing, unparseable or non-finite, retryAfterSeconds is undefined and recommendedDelay falls back to 60 seconds.

TIP

By the time you catch a KSeFRateLimitError, the built-in retry policy has already retried up to 3 times with backoff, or skipped retrying because Retry-After was longer than maxRetryAfterMs. Use recommendedDelay to schedule a later retry.


KSeFBatchTimeoutError ​

File: src/errors/ksef-batch-timeout-error.ts

Thrown when KSeF responds with error code 21208 — the server-side batch session timed out before processing finished. Extends KSeFApiError, so it also carries statusCode and errorResponse.

typescript
class KSeFBatchTimeoutError extends KSeFApiError {
  readonly errorCode: 21208;

  static fromResponse(statusCode: number, body?: ApiErrorResponse): KSeFBatchTimeoutError;
}

Use this class to distinguish server-side batch timeouts from other failure modes when orchestrating large batch uploads or finish operations. The numeric code registry for error detection is exposed as KSeFErrorCode in src/errors/error-codes.ts (currently BatchTimeout = 21208, DuplicateInvoice = 440, SessionTemporarilyUnavailable = 21184, InvalidContinuationToken = 21418, UnknownPublicKeyId = 21470, InvalidInput = 21405, UnreadableContent = 21001, CollectiveIdentifierDifferentSellers = 71004, CollectiveIdentifierDuplicateKsefNumber = 71005).

TIP

Pair with a retry-with-smaller-batch strategy rather than a tight loop — the timeout means KSeF is under load, not that the request was malformed.


KSeFUnknownPublicKeyError ​

File: src/errors/ksef-unknown-public-key-error.ts

Thrown when KSeF rejects an encryption request because the supplied publicKeyId is unknown or points to a revoked key — HTTP 400 with error code 21470 (KSeF API v2.5.0). Extends KSeFApiError, so it also carries statusCode and errorResponse.

typescript
class KSeFUnknownPublicKeyError extends KSeFApiError {
  readonly statusCode: 400;
  readonly errorCode: 21470;
  // Set only when built from an RFC 7807 body (same fields as KSeFBadRequestError):
  readonly detail?: string;
  readonly instance?: string;
  readonly errors: BadRequestErrorDetail[]; // [] for a legacy body
  readonly traceId?: string;
  readonly timestamp?: string;

  static fromProblem(problem: BadRequestProblemDetails): KSeFUnknownPublicKeyError;
  static fromLegacy(body?: ApiErrorResponse): KSeFUnknownPublicKeyError;
}

Two factories cover both response shapes KSeF may return: fromProblem reads the RFC 7807 errors[] array, fromLegacy reads the older exception.exceptionDetailList array. Both fall back to a fixed message when the server sends no description. Built from an RFC 7807 body, the error keeps the same Problem Details fields as KSeFBadRequestError and returns them from toProblemFields(), so the CLI and JSON output still show the full error list and trace ID; built from a legacy body, the details stay in errorResponse.

WARNING

On a 400 carrying code 21470 this class is thrown instead of KSeFBadRequestError, and it is not part of the KSeFApiProblem union. A handler that only catches KSeFBadRequestError will miss it — catch KSeFApiError to cover both.

The library already recovers from this on its own: encryption-bearing operations wrapped in the key-rotation helper (src/crypto/with-key-rotation-retry.ts) refresh the cached KSeF public certificates and retry once with a freshly selected key. You normally see this error only when the retry also fails.


KSeFSessionUnavailableError ​

File: src/errors/ksef-session-unavailable-error.ts

Thrown when KSeF temporarily stops accepting invoices in an existing online session, typically during maintenance — HTTP 400 with error code 21184 on POST /sessions/online/{referenceNumber}/invoices (KSeF API v2.8.0). Extends KSeFApiError, so it also carries statusCode and errorResponse.

typescript
class KSeFSessionUnavailableError extends KSeFApiError {
  readonly statusCode: 400;
  readonly errorCode: 21184;
  // Set only when built from an RFC 7807 body (same fields as KSeFBadRequestError):
  readonly detail?: string;
  readonly instance?: string;
  readonly errors: BadRequestErrorDetail[]; // [] for a legacy body
  readonly traceId?: string;
  readonly timestamp?: string;

  static fromProblem(problem: BadRequestProblemDetails): KSeFSessionUnavailableError;
  static fromLegacy(body?: ApiErrorResponse): KSeFSessionUnavailableError;
}

Like KSeFUnknownPublicKeyError, it is built from either body format, keeps the Problem Details fields (detail, instance, errors, traceId, timestamp) when built from an RFC 7807 body, and falls back to a fixed message that recommends opening a new session. If a 400 carries both 21184 and 21470, this class wins: rotating the key and retrying would land in the same unavailable session.

WARNING

On a 400 carrying code 21184 this class is thrown instead of KSeFBadRequestError, and it is not part of the KSeFApiProblem union. Catch it explicitly, or catch KSeFApiError.

The library does not reopen the session for you. KSeF's recommendation is to open a new session and send the remaining invoices there; invoices already accepted in the old session stay valid, so resend only the ones that were not accepted:

typescript
import { KSeFSessionUnavailableError, openOnlineSession } from 'ksef-client-ts';

try {
  await handle.sendInvoice(xml);
} catch (error) {
  if (error instanceof KSeFSessionUnavailableError) {
    handle = await openOnlineSession(client, sessionOptions); // new session
    await handle.sendInvoice(xml);                            // continue there
  } else {
    throw error;
  }
}

KSeFBadRequestError ​

File: src/errors/ksef-bad-request-error.ts

Thrown when KSeF returns HTTP 400 with an RFC 7807 body — the request was rejected before processing. Extends KSeFApiError. See 400 Bad Request for the wire format this class is built from.

typescript
class KSeFBadRequestError extends KSeFApiError {
  override readonly statusCode: 400 = 400;
  readonly detail?: string;
  readonly instance?: string;
  readonly errors: BadRequestErrorDetail[];
  readonly traceId?: string;
  readonly timestamp?: string;

  constructor(problemDetails: BadRequestProblemDetails);
  override toProblemFields(): ProblemFields;
}
FieldTypeDescription
statusCode400Always 400
detailstring?Server's explanation of what failed
instancestring?The endpoint path
errorsBadRequestErrorDetail[]Individual validation failures; defaults to [] when the server sends none
traceIdstring?Server-side trace ID
timestampstring?UTC timestamp recorded by the server

The top-level message falls back through detail → title → "Bad Request", so it is never empty. Each errors entry carries code: number, description?: string, and details?: string[] | null — route on code for programmatic handling.

WARNING

Not every 400 arrives as this class, and which class you get depends on the body format:

Catch KSeFApiError to cover every 400 regardless of format, and route on errors[].code rather than on the class when you care about a specific KSeF code.


KSeFUnauthorizedError ​

File: src/errors/ksef-unauthorized-error.ts

Thrown when KSeF returns HTTP 401 with an RFC 7807 Problem Details body. Extends KSeFApiError so a single instanceof KSeFApiError catch still matches.

typescript
class KSeFUnauthorizedError extends KSeFApiError {
  override readonly statusCode: 401 = 401;
  readonly detail: string;
  readonly traceId?: string;
  readonly instance?: string;
  readonly timestamp?: string;

  constructor(problemDetails: UnauthorizedProblemDetails);
}
FieldTypeDescription
statusCode401Always 401
detailstringServer's explanation (e.g., "Token has expired")
traceIdstring?Server-side trace ID for support tickets
instancestring?The endpoint path that was called
timestampstring?UTC timestamp recorded by the server (KSeF API v2.4.0+); useful for correlating with server-side logs

INFO

By the time you catch this error, the library has already attempted an automatic token refresh via AuthManager.onUnauthorized(). If you see this error, it means the refresh also failed or no AuthManager is configured.


KSeFForbiddenError ​

File: src/errors/ksef-forbidden-error.ts

Thrown when KSeF returns HTTP 403 with an RFC 7807 Problem Details body that includes a reasonCode. Extends KSeFApiError.

typescript
class KSeFForbiddenError extends KSeFApiError {
  override readonly statusCode: 403 = 403;
  readonly detail: string;
  readonly reasonCode: ForbiddenReasonCode;
  readonly instance?: string;
  readonly security?: Record<string, unknown>;
  readonly traceId?: string;
  readonly timestamp?: string;

  constructor(problemDetails: ForbiddenProblemDetails);
}
FieldTypeDescription
statusCode403Always 403
detailstringHuman-readable denial reason
reasonCodeForbiddenReasonCodeMachine-readable reason (see reason code table)
instancestring?The endpoint path
securityRecord<string, unknown>?Additional security context from KSeF
traceIdstring?Server-side trace ID
timestampstring?UTC timestamp recorded by the server (KSeF API v2.4.0+); useful for correlating with server-side logs

KSeFGoneError ​

File: src/errors/ksef-gone-error.ts

Thrown when KSeF returns HTTP 410 (Gone) — the server-side record for an async operation status has aged out of the retention window. Extends KSeFApiError (single out via instanceof KSeFGoneError rather than the base-class split).

typescript
class KSeFGoneError extends KSeFApiError {
  override readonly statusCode: 410 = 410;
  readonly detail: string;
  readonly instance?: string;
  readonly traceId?: string;
  readonly timestamp?: string;

  constructor(problemDetails: GoneProblemDetails);
}
FieldTypeDescription
statusCode410Always 410
detailstringServer's explanation, or "Operation status no longer available (retention expired)" if absent
instancestring?The endpoint path
traceIdstring?Server-side trace ID
timestampstring?UTC timestamp recorded by the server

KSeF v2.4.0 retention windows ​

Operation status typeRetention
Authentication operation status7 days
Invoice export operation status7 days
Certificate enrollment status30 days
Permission operation status30 days

TIP

A KSeFGoneError means the operation likely ran to completion long ago — KSeF only forgets the status record, not the underlying outcome (e.g., issued certificate, exported package). If you missed the result, repeat the action rather than re-poll the same reference number.


KSeFAuthStatusError ​

File: src/errors/ksef-auth-status-error.ts

Thrown when an authentication ceremony (challenge-redeem flow) fails or times out. This is a higher-level error that occurs during multi-step auth workflows, not a direct HTTP status mapping.

typescript
class KSeFAuthStatusError extends KSeFError {
  readonly referenceNumber?: string;
  readonly statusDescription?: string;

  constructor(message: string, referenceNumber?: string, statusDescription?: string);
}
FieldTypeDescription
referenceNumberstring?KSeF reference number for the failed auth operation
statusDescriptionstring?Server's description of the failure

Typical causes: the auth challenge expired before completion, the authorization token was invalid, or the certificate signature was rejected.


KSeFSessionExpiredError ​

File: src/errors/ksef-session-expired-error.ts

Thrown when attempting to use a stored session that has expired. This is a client-side check -- the error is raised before making a network request.

typescript
class KSeFSessionExpiredError extends KSeFError {
  constructor(message?: string);  // defaults to "KSeF session has expired"
}

Recovery: re-authenticate and open a new session.


KSeFSessionFailedError ​

File: src/errors/ksef-session-failed-error.ts

Thrown by waitForUpo() / waitForUpoParsed() on an online session handle and by the batch upload workflows when the session, polled for its UPO, ends in a failed status (code 400 or above). code is a KSeF session status, not an HTTP status. The message begins as before (Session failed: CODE — DESCRIPTION / Batch session failed: CODE — DESCRIPTION); when the status carries details, they are appended in parentheses, separated by semicolons. Matching on the start of the message therefore still works, while an exact comparison of the whole message fails when details are present — prefer code and details.

typescript
class KSeFSessionFailedError extends KSeFError {
  readonly referenceNumber: string;            // session reference number
  readonly code: number;                       // session status code
  readonly description: string;
  readonly details: string[];
  readonly sessionStatus: SessionStatusResponse; // full status, with invoice counts
}
SessionFailed status codes
Online415 key decryption error, 440 session cancelled (no invoices sent), 445 no valid invoices
Batch405 package verification error, 415 key decryption error, 420 invoice limit exceeded, 430 decompression error, 435 part decryption error, 440 session cancelled (upload timed out or no invoices sent), 445 no valid invoices, 500 unknown error

Recovery: list the session's failed invoices (client.sessionStatus.getSessionFailedInvoices(referenceNumber), or ksef session failed <ref>) to see why each one was rejected.


KSeFInvoiceRejectedError ​

File: src/errors/ksef-invoice-rejected-error.ts

Thrown by OnlineSessionHandle.waitForInvoice() when KSeF finishes processing a sent invoice and rejects it. code is a KSeF invoice status, not an HTTP status or a KSeF error code. The whole status response is kept, so the reason and the structured data KSeF attached reach the caller.

typescript
class KSeFInvoiceRejectedError extends KSeFError {
  readonly sessionReferenceNumber: string;
  readonly referenceNumber: string;            // invoice reference number from sendInvoice()
  readonly ordinalNumber: number;
  readonly invoiceNumber?: string;             // P_2, when KSeF could read it
  readonly code: number;                       // invoice status code
  readonly description: string;
  readonly details: string[];
  readonly extensions: Record<string, string | null>;
  readonly invoiceStatus: SessionInvoiceStatusResponse;

  get isDuplicate(): boolean;                          // code === 440
  get originalKsefNumber(): string | undefined;        // extensions.originalKsefNumber
  get originalSessionReferenceNumber(): string | undefined;
}
Invoice statusOutcome
100, 150Accepted / processing -- waitForInvoice() keeps polling
200Success -- resolves with the status, carrying ksefNumber
405Processing cancelled because of a session error
410Invalid permission scope
415Invoices with attachments cannot be sent
430Invoice file verification error
435File decryption error
440Duplicate invoice -- extensions carry originalKsefNumber and originalSessionReferenceNumber
450Semantic verification error -- details explain it
500Unknown error
550Cancelled by the system -- send the invoice again

A duplicate is how a lost send response is recovered: sending the same invoice again yields 440 together with the KSeF number the first send was given.

typescript
import { KSeFInvoiceRejectedError } from 'ksef-client-ts';

try {
  const { ksefNumber } = await handle.waitForInvoice(await handle.sendInvoice(xml));
  return ksefNumber;
} catch (error) {
  if (error instanceof KSeFInvoiceRejectedError && error.originalKsefNumber) {
    return error.originalKsefNumber;  // already in KSeF
  }
  throw error;
}

KSeFCircuitOpenError ​

File: src/errors/ksef-circuit-open-error.ts

Thrown by the opt-in circuit breaker when it is open and the cooldown has not yet elapsed. Extends KSeFError directly (not KSeFApiError) because no HTTP request was actually made — the breaker stopped it before the retry loop started.

typescript
class KSeFCircuitOpenError extends KSeFError {
  readonly endpoint: string;
  readonly openedAt: number;      // performance.now() at open time
  readonly retryAfterMs: number;  // remaining cooldown in ms

  constructor(endpoint: string, openedAt: number, retryAfterMs: number);
}
FieldTypeDescription
endpointstringThe request path that was short-circuited (useful when scope: 'endpoint')
openedAtnumberperformance.now() timestamp at the moment the breaker opened
retryAfterMsnumberRemaining cooldown in milliseconds before the next probe is allowed

Configure the breaker via the circuitBreaker client option — see Configuration — Circuit Breaker and HTTP Resilience — Circuit Breaker. 429 and 401 responses never open or extend the breaker, so rate limiting and auth problems are never confused with an upstream outage.

TIP

Catching this error is the right place to park work for later (persist to a queue, notify oncall) rather than retry. By definition, the upstream has been failing repeatedly within the configured window.


KSeFValidationError ​

File: src/errors/ksef-validation-error.ts

Thrown for client-side validation failures (e.g., invalid builder parameters, malformed presigned URLs). These errors are raised before any network request is made.

typescript
interface ValidationDetail {
  field?: string;
  message: string;
}

class KSeFValidationError extends KSeFError {
  readonly details: ValidationDetail[];

  constructor(message: string, details?: ValidationDetail[]);
  static fromField(field: string, message: string): KSeFValidationError;
  static fromMessages(messages: string[]): KSeFValidationError;
}
FieldTypeDescription
detailsValidationDetail[]List of individual validation failures
details[].fieldstring?The field that failed validation (if applicable)
details[].messagestringHuman-readable description of the validation failure

Factory methods:

  • fromField(field, message) -- creates an error with a single field-level validation detail.
  • fromMessages(messages) -- creates an error from multiple message strings, joining them with "; " for the top-level message.

KSeFXsdValidationError ​

File: src/errors/ksef-xsd-validation-error.ts

Thrown when an invoice document fails validation against an official KSeF XSD schema. Extends KSeFError directly — this is a local check, raised before any network request is made.

typescript
class KSeFXsdValidationError extends KSeFError {
  readonly schemaFile: string;
  readonly errors: string[];

  constructor(schemaFile: string, errors: string[]);
}
FieldTypeDescription
schemaFilestringPath of the XSD the document was validated against
errorsstring[]Every schema violation reported by the validator

The top-level message previews only the first three entries and appends (+N more); read errors for the complete list.

Raised by ksef invoice build --validate-xsd, which maps it to exit code 4 so scripts can branch on schema failures specifically. A missing optional libxmljs2 peer dependency is deliberately mapped to the same exit code, so a wrapper script only has to handle one "XSD did not pass" case.


KSeFMetadataPaginationError ​

File: src/errors/ksef-metadata-pagination-error.ts

Thrown by the metadata-query paging helper in two distinct situations, both of which end an otherwise unbounded loop. Extends KSeFError.

typescript
class KSeFMetadataPaginationError extends KSeFError {
  readonly boundaryValue: string;

  constructor(message: string, boundaryValue: string);
}
FieldTypeDescription
boundaryValuestringThe boundary value the helper stopped at
CauseWhat happenedRemedy
Stalled boundaryA truncated window did not advance past its boundary value — every row in the capped window shares the same date, so re-narrowing the range achieves nothingNarrow the query by a dimension other than the date
Crossing limit reachedEvery boundary advanced normally, but the query crossed more truncation boundaries than maxBoundaryCrossings allows (default 1000)Raise maxBoundaryCrossings, or split the query into smaller date ranges

Read the message to tell them apart: the first names the boundary it stalled on, the second names the limit it exceeded. Neither means the request was rejected — the server answered fine, the result set just cannot be walked under the current settings. Thrown from src/workflows/metadata-query-paging.ts.


KSeFPaginationError ​

File: src/errors/ksef-pagination-error.ts

Thrown by the continuation-token paging helpers (the collective identifier walks) when a walk cannot reach its last page. It is raised instead of looping forever, after every page read so far has been yielded. Extends KSeFError.

The metadata paging helper keeps its own KSeFMetadataPaginationError: that one pages by date boundaries and reports a boundary value, not a token.

typescript
class KSeFPaginationError extends KSeFError {
  readonly reason: 'repeated-token' | 'max-pages';
  readonly continuationToken: string;

  constructor(message: string, continuationToken: string, reason: 'repeated-token' | 'max-pages');
}
reasonWhat happenedcontinuationTokenRemedy
'repeated-token'KSeF returned a continuation token it had already returned, so following it would never endThe repeated tokenRestart the walk, or narrow the query
'max-pages'The walk needed more pages than maxPages allows (default 1000)The token of the first unread pageResume with it as continuationToken, or raise maxPages

The message names the token in both cases. Thrown from src/workflows/collective-identifier-paging.ts.

In the CLI, --all collects every page before printing, so the rows read before the stop are not shown. At the page cap the CLI prints a hint to run the command again with --continue <token>, which fetches the pages after them. For a repeated token it suggests narrowing the query instead. With --json the error object carries reason and continuationToken.


Programmatic Error Handling Patterns ​

Catch all library errors ​

typescript
import { KSeFError } from 'ksef-client-ts';

try {
  await client.invoices.sendInvoice(invoiceXml);
} catch (error) {
  if (error instanceof KSeFError) {
    console.error('KSeF library error:', error.message);
  } else {
    // Not from this library (network failure, coding bug, etc.)
    throw error;
  }
}

Handle specific error types with instanceof ​

typescript
import {
  KSeFError,
  KSeFApiError,
  KSeFRateLimitError,
  KSeFBatchTimeoutError,
  KSeFUnauthorizedError,
  KSeFForbiddenError,
  KSeFGoneError,
  KSeFValidationError,
} from 'ksef-client-ts';

try {
  await client.invoices.sendInvoice(invoiceXml);
} catch (error) {
  if (error instanceof KSeFRateLimitError) {
    // 429 — all retries exhausted
    console.warn(`Rate limited. Retry in ${error.recommendedDelay}s`);
    await scheduleRetry(error.recommendedDelay * 1000);

  } else if (error instanceof KSeFBatchTimeoutError) {
    // KSeF code 21208 — batch session timed out server-side
    console.warn('Batch timeout. Retry with a smaller batch or fewer parallel parts.');

  } else if (error instanceof KSeFUnauthorizedError) {
    // 401 — auth refresh also failed
    console.error(`Auth failed: ${error.detail} (trace: ${error.traceId})`);
    await reauthenticate();

  } else if (error instanceof KSeFForbiddenError) {
    // 403 — permission or policy denial
    console.error(`Forbidden [${error.reasonCode}]: ${error.detail}`);
    handleForbidden(error);

  } else if (error instanceof KSeFGoneError) {
    // 410 — operation status retention expired (KSeF v2.4.0+)
    console.error(`Operation status no longer available: ${error.detail}`);
    // Re-issue the underlying action; the previous result is unrecoverable from this endpoint.

  } else if (error instanceof KSeFValidationError) {
    // Client-side validation failed before sending
    for (const detail of error.details) {
      console.error(`Validation: ${detail.field ?? '(general)'} — ${detail.message}`);
    }

  } else if (error instanceof KSeFApiError) {
    // Generic server error (400, 404, 500, etc.)
    console.error(`HTTP ${error.statusCode}: ${error.message}`);
    if (error.errorResponse?.exception?.exceptionDetailList) {
      for (const detail of error.errorResponse.exception.exceptionDetailList) {
        console.error(`  [${detail.exceptionCode}] ${detail.exceptionDescription}`);
      }
    }

  } else if (error instanceof KSeFError) {
    // Other library errors (KSeFAuthStatusError, KSeFSessionExpiredError)
    console.error('KSeF error:', error.message);
  }
}

Check order matters

KSeFRateLimitError and KSeFBatchTimeoutError both extend KSeFApiError, so always check the specific subclasses before KSeFApiError. Otherwise, the KSeFApiError branch would catch them too.

React to ForbiddenReasonCode values ​

typescript
import { KSeFForbiddenError } from 'ksef-client-ts';

function handleForbidden(error: KSeFForbiddenError): void {
  switch (error.reasonCode) {
    case 'missing-permissions':
      // The authenticated subject lacks a required KSeF permission.
      // Action: grant the permission via PermissionService or KSeF portal.
      console.error('Missing permission. Grant access and retry.');
      break;

    case 'ip-not-allowed':
      // Request came from an IP outside the session's allowed range.
      // Action: check network config or re-open session from allowed IP.
      console.error('IP address not allowed for this session.');
      break;

    case 'insufficient-resource-access':
      // Subject is authenticated but cannot access this specific resource.
      // Action: verify the NIP or invoice reference belongs to the subject.
      console.error('Cannot access this resource. Check NIP/invoice ownership.');
      break;

    case 'auth-method-not-allowed':
      // The auth method (token vs certificate) is not permitted here.
      // Action: switch authentication method.
      console.error('Auth method not allowed. Try certificate-based auth.');
      break;

    case 'security-service-blocked':
      // KSeF security subsystem blocked the request.
      // Action: contact KSeF support with the traceId.
      console.error(`Blocked by security. Contact support (trace: ${error.traceId})`);
      break;

    case 'context-type-not-allowed':
      // Session context type does not permit this operation.
      // Action: re-open session with correct context identifier type.
      console.error('Context type not allowed. Re-open session with correct context.');
      break;

    default:
      // Unknown reason code (future API additions).
      console.error(`Forbidden: ${error.reasonCode} — ${error.detail}`);
  }
}

Handle rate limiting gracefully ​

typescript
import { KSeFRateLimitError } from 'ksef-client-ts';

async function sendWithRateLimitRetry(
  client: KSeFClient,
  invoiceXml: string,
  maxRetries = 3,
): Promise<string> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await client.invoices.sendInvoice(invoiceXml);
    } catch (error) {
      if (error instanceof KSeFRateLimitError && i < maxRetries - 1) {
        const delayMs = error.recommendedDelay * 1000;
        console.warn(`Rate limited, waiting ${error.recommendedDelay}s before retry ${i + 1}`);
        await new Promise((r) => setTimeout(r, delayMs));
        continue;
      }
      throw error;
    }
  }
  throw new Error('Unreachable');
}

Distinguish server errors from validation errors ​

typescript
import { KSeFApiError, KSeFValidationError } from 'ksef-client-ts';

try {
  await client.invoices.sendInvoice(invoiceXml);
} catch (error) {
  if (error instanceof KSeFValidationError) {
    // Client-side: fix the input, no need to contact KSeF
    console.error('Fix these validation errors before sending:');
    error.details.forEach((d) => console.error(`  - ${d.message}`));
  } else if (error instanceof KSeFApiError) {
    // Server-side: the request reached KSeF and was rejected
    console.error(`Server rejected with HTTP ${error.statusCode}`);
  }
}

Automatic Recovery ​

The RestClient handles several error types transparently before they reach your code. Understanding what happens internally helps you write correct error handling.

File: src/http/rest-client.ts, method sendRequest() (lines 71-131)

Retry on 429 / 5xx / network errors ​

Retryable responses (429, 500, 502, 503, 504; for POST only 429) and network errors (ECONNRESET, ECONNREFUSED, ETIMEDOUT, AbortError; for POST only ECONNREFUSED and UND_ERR_CONNECT_TIMEOUT) are retried up to maxRetries times (default: 3) with exponential backoff and jitter.

For 429 and 503 responses, the Retry-After header is respected -- if present, its value overrides the calculated backoff delay. A Retry-After longer than maxRetryAfterMs (default 120 s) is not waited out: the request fails at once, a 429 as KSeFRateLimitError carrying the server's value.

Your code calls client.invoices.sendInvoice()
  │
  ├── Attempt 0: 429 → sleep(Retry-After or backoff) → retry
  ├── Attempt 1: 502 → sleep(backoff) → retry
  ├── Attempt 2: 200 → success, your code gets the result
  │
  └── (if attempt 3 also fails: ensureSuccess() throws the error to your code)

What you see: Either a successful result (if any retry succeeded) or a KSeFRateLimitError / KSeFApiError (if all retries failed).

Auto-refresh on 401 ​

When the first attempt returns 401, the RestClient calls AuthManager.onUnauthorized() to refresh the access token. If the refresh succeeds, the request is retried once with the new token.

Your code calls client.invoices.getInvoice()
  │
  ├── Attempt 0: 401 → AuthManager.onUnauthorized()
  │   ├── Refresh succeeded → retry with new token → 200 → success
  │   └── Refresh failed → ensureSuccess() → throw KSeFUnauthorizedError
  │
  └── (auth refresh only attempted on first attempt, not during retries)

Guard conditions on auto-refresh:

  • Only on attempt === 0 (first attempt, not during retries)
  • Only if AuthManager is configured (i.e., user has logged in)
  • Skipped for requests marked with .skipAuthRetry() (auth endpoints themselves, to prevent infinite refresh loops)

What you see: Either a successful result (if refresh worked) or a KSeFUnauthorizedError (if refresh failed or was not possible).

Dedup refresh ​

When multiple concurrent requests all receive 401 simultaneously, DefaultAuthManager deduplicates the refresh calls -- only the first triggers the actual refresh; all others await the same promise.

File: src/http/auth-manager.ts


Error Logging for Support Tickets ​

When contacting KSeF support (Ministerstwo Finansow), the traceId is the most important piece of information. Both KSeFUnauthorizedError and KSeFForbiddenError carry it.

typescript
import { KSeFError, KSeFUnauthorizedError, KSeFForbiddenError, KSeFApiError } from 'ksef-client-ts';

function logKSeFError(error: KSeFError): void {
  const entry: Record<string, unknown> = {
    name: error.name,
    message: error.message,
  };

  // Extract traceId for RFC 7807 errors
  if (error instanceof KSeFUnauthorizedError || error instanceof KSeFForbiddenError) {
    entry.traceId = error.traceId;
    entry.detail = error.detail;
    entry.instance = error.instance;
  }

  // Extract reasonCode for 403
  if (error instanceof KSeFForbiddenError) {
    entry.reasonCode = error.reasonCode;
    entry.security = error.security;
  }

  // Extract exception details for generic API errors
  if (error instanceof KSeFApiError) {
    entry.statusCode = error.statusCode;
    const details = error.errorResponse?.exception;
    if (details) {
      entry.serviceCode = details.serviceCode;
      entry.referenceNumber = details.referenceNumber;
      entry.timestamp = details.timestamp;
      entry.exceptions = details.exceptionDetailList;
    }
  }

  console.error('[KSeF Error]', JSON.stringify(entry, null, 2));
}

Example output for a support ticket:

json
{
  "name": "KSeFForbiddenError",
  "message": "Subject does not have required permissions",
  "traceId": "abc-123-def-456",
  "detail": "Subject does not have required permissions",
  "instance": "/v2/online/Invoice/Send",
  "reasonCode": "missing-permissions",
  "security": { "requiredPermission": "InvoiceWrite" }
}

When filing a KSeF support ticket, include: the traceId, the instance (endpoint path), the timestamp (from your logs), and the reasonCode or exceptionCode.


Integration with Workflows ​

Workflow functions in src/workflows/ orchestrate multi-step operations (export, batch upload, session management). Errors propagate through them with additional context.

Polling timeout ​

File: src/workflows/polling.ts

The pollUntil() utility polls an async action until a condition is met or a maximum number of attempts is reached. On timeout, it throws a plain Error (not a KSeFError):

typescript
throw new Error(
  `Polling timeout: ${description} after ${maxAttempts} attempts`
);

Default polling: 2000ms interval, 60 attempts (2 minutes total). Configure via PollOptions:

typescript
interface PollOptions {
  intervalMs?: number;   // default: 2000
  maxAttempts?: number;  // default: 60
  onProgress?: (attempt: number, maxAttempts: number) => void;
}

Export workflow errors ​

File: src/workflows/invoice-export-workflow.ts

The exportInvoices() and exportAndDownload() functions can throw:

  1. Any KSeFError subclass from the underlying API calls (client.invoices.exportInvoices(), client.invoices.getInvoiceExportStatus()).
  2. Error("Export failed: {code} -- {description}") when the export status poll completes with a non-200 status code (the server accepted the export request but the operation itself failed).
  3. Error("Export completed but no package available") when the export succeeds but returns no downloadable package.
  4. Error("Download failed for part N: HTTP {status}") when downloading an encrypted export part fails.
  5. Polling timeout from pollUntil().

Error handling in workflows ​

typescript
import { KSeFError, KSeFRateLimitError } from 'ksef-client-ts';
import { exportAndDownload } from 'ksef-client-ts/workflows';

try {
  const result = await exportAndDownload(client, filters, { extract: true });
} catch (error) {
  if (error instanceof KSeFRateLimitError) {
    // Rate limited during export initiation or status polling
    console.warn('Export rate limited, retry later');
  } else if (error instanceof KSeFError) {
    // Other KSeF API error during the workflow
    console.error('KSeF error during export:', error.message);
  } else if (error instanceof Error && error.message.startsWith('Polling timeout')) {
    // Export is taking too long -- increase maxAttempts or check KSeF status
    console.error('Export polling timed out');
  } else if (error instanceof Error && error.message.startsWith('Export failed')) {
    // KSeF accepted the export but the operation failed server-side
    console.error('Export operation failed:', error.message);
  } else {
    throw error;
  }
}

Summary Table ​

Error classHTTP statusBody formatKey fieldsAutomatic recovery
KSeFApiErrorany non-2xxApiErrorResponsestatusCode, errorResponseRetry on 5xx
KSeFBadRequestError400RFC 7807 BadRequestProblemDetailserrors[], detail, traceId, instance, timestampNone (fix the request)
KSeFRateLimitError429RFC 7807 TooManyRequestsProblemDetails or legacy TooManyRequestsResponseretryAfterSeconds, recommendedDelay, problem?Retry with Retry-After
KSeFBatchTimeoutErrorany non-2xx (KSeF code 21208)ApiErrorResponseerrorCode (21208), statusCode, errorResponseNone (retry with smaller batch)
KSeFUnknownPublicKeyError400 (KSeF code 21470)RFC 7807 BadRequestProblemDetails or ApiErrorResponseerrorCode (21470), statusCode, errors[], traceId (RFC 7807) or errorResponse (legacy)Certificate cache refresh, then retry once
KSeFSessionUnavailableError400 (KSeF code 21184)RFC 7807 BadRequestProblemDetails or ApiErrorResponseerrorCode (21184), statusCode, errors[], traceId (RFC 7807) or errorResponse (legacy)None (open a new session and continue)
KSeFUnauthorizedError401RFC 7807 UnauthorizedProblemDetailsdetail, traceId, instance, timestampToken refresh, then retry once
KSeFForbiddenError403RFC 7807 ForbiddenProblemDetailsreasonCode, detail, traceId, security, timestampNone (not retryable)
KSeFGoneError410RFC 7807 GoneProblemDetailsdetail, traceId, instance, timestampNone (re-issue the underlying action)
KSeFAuthStatusError----referenceNumber, statusDescriptionNone
KSeFSessionExpiredError----messageNone
KSeFSessionFailedError-- (session status)--referenceNumber, code, description, details, sessionStatusNone (inspect the failed invoices)
KSeFInvoiceRejectedError-- (invoice status)--referenceNumber, code, details, extensions, originalKsefNumberNone (fix the invoice; for 440 use originalKsefNumber)
KSeFCircuitOpenError----endpoint, openedAt, retryAfterMsNone (wait out retryAfterMs before retrying)
KSeFValidationError----details[] with field and messageNone (client-side)
KSeFXsdValidationError----schemaFile, errors[]None (client-side)
KSeFMetadataPaginationError----boundaryValueNone (narrow the query or raise the crossing cap)
KSeFPaginationError----reason, continuationTokenNone (resume from continuationToken or raise maxPages)

Files Reference ​

FileContents
src/errors/types.tsApiErrorResponse, ExceptionDetails, TooManyRequestsResponse, UnauthorizedProblemDetails, ForbiddenProblemDetails, ForbiddenReasonCode, GoneProblemDetails
src/errors/ksef-error.tsKSeFError base class
src/errors/ksef-api-error.tsKSeFApiError with fromResponse() factory
src/errors/ksef-bad-request-error.tsKSeFBadRequestError (HTTP 400, RFC 7807)
src/errors/ksef-rate-limit-error.tsKSeFRateLimitError with fromRetryAfterHeader() factory
src/errors/ksef-batch-timeout-error.tsKSeFBatchTimeoutError with fromResponse() factory (KSeF code 21208)
src/errors/ksef-unknown-public-key-error.tsKSeFUnknownPublicKeyError with fromProblem() / fromLegacy() factories (KSeF code 21470)
src/errors/ksef-session-unavailable-error.tsKSeFSessionUnavailableError with fromProblem() / fromLegacy() factories (KSeF code 21184)
src/errors/error-codes.tsKSeFErrorCode numeric code registry + hasErrorCode() helper
src/errors/ksef-unauthorized-error.tsKSeFUnauthorizedError
src/errors/ksef-forbidden-error.tsKSeFForbiddenError
src/errors/ksef-gone-error.tsKSeFGoneError (HTTP 410, retention expired)
src/errors/ksef-auth-status-error.tsKSeFAuthStatusError
src/errors/ksef-session-expired-error.tsKSeFSessionExpiredError
src/errors/ksef-session-failed-error.tsKSeFSessionFailedError (session ended in a failed status)
src/errors/ksef-invoice-rejected-error.tsKSeFInvoiceRejectedError (sent invoice rejected; duplicate's original KSeF number)
src/errors/ksef-circuit-open-error.tsKSeFCircuitOpenError (opt-in circuit breaker)
src/errors/ksef-validation-error.tsKSeFValidationError, ValidationDetail
src/errors/ksef-xsd-validation-error.tsKSeFXsdValidationError (XSD schema validation)
src/errors/ksef-metadata-pagination-error.tsKSeFMetadataPaginationError (paging cannot advance)
src/errors/ksef-pagination-error.tsKSeFPaginationError (continuation-token paging cannot finish)
src/errors/index.tsBarrel re-exports for all error types
src/http/rest-client.tsensureSuccess() dispatch, sendRequest() retry + auth refresh
src/http/retry-policy.tsRetryPolicy, parseRetryAfter(), calculateBackoff()
src/http/auth-manager.tsAuthManager interface, dedup refresh logic
src/workflows/metadata-query-paging.tsMetadata paging helper; raises KSeFMetadataPaginationError
src/workflows/collective-identifier-paging.tsCollective identifier paging helpers; raise KSeFPaginationError
src/workflows/polling.tspollUntil() with timeout
src/workflows/invoice-export-workflow.tsExport workflow error propagation

Released under the MIT License.