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.
  • Client-side errors (KSeFValidationError) catch invalid requests before they reach the network.
  • Workflow errors (KSeFAuthStatusError, KSeFSessionExpiredError) 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.


Error Hierarchy

Error (built-in)
  └── KSeFError                          src/errors/ksef-error.ts
        ├── KSeFApiError                 src/errors/ksef-api-error.ts         (any non-2xx HTTP)
        │     └── KSeFRateLimitError     src/errors/ksef-rate-limit-error.ts  (429)
        ├── KSeFUnauthorizedError        src/errors/ksef-unauthorized-error.ts (401 + RFC 7807)
        ├── KSeFForbiddenError           src/errors/ksef-forbidden-error.ts   (403 + RFC 7807)
        ├── KSeFAuthStatusError          src/errors/ksef-auth-status-error.ts (auth ceremony failed)
        ├── KSeFSessionExpiredError      src/errors/ksef-session-expired-error.ts (stored session expired)
        └── KSeFValidationError          src/errors/ksef-validation-error.ts  (client-side validation)

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

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

RFC 7807 Problem Details

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

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"
}
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

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": { "requiredPermission": "InvoiceWrite" },
  "traceId": "abc-123-def-456"
}
FieldTypeDescription
titlestringShort human-readable summary
statusnumberAlways 403
detailstringSpecific explanation of the denial
instancestring?The API endpoint path
reasonCodeForbiddenReasonCodeMachine-readable reason (see table below)
securityRecord<string, unknown>?Additional security context from the server
traceIdstring?Server-side trace ID

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.


Error Dispatch Flow

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

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:

Response not OK?

  ├── status 429 → parse as TooManyRequestsResponse + ApiErrorResponse
  │                → throw KSeFRateLimitError.fromRetryAfterHeader()
  │                  (includes Retry-After header parsing)

  ├── 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)

  └── any other status → parse as ApiErrorResponse
                         → 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.
  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.

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.

If the header is missing or unparseable, 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. This error means all retries were exhausted. Use recommendedDelay to schedule a later retry.


KSeFUnauthorizedError

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

Thrown when KSeF returns HTTP 401 with an RFC 7807 Problem Details body. Extends KSeFError directly (not KSeFApiError) because the body structure is different.

typescript
class KSeFUnauthorizedError extends KSeFError {
  readonly statusCode = 401;
  readonly detail: string;
  readonly traceId?: string;
  readonly instance?: 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

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 KSeFError directly.

typescript
class KSeFForbiddenError extends KSeFError {
  readonly statusCode = 403;
  readonly detail: string;
  readonly reasonCode: ForbiddenReasonCode;
  readonly instance?: string;
  readonly security?: Record<string, unknown>;
  readonly traceId?: 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

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.


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.

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,
  KSeFUnauthorizedError,
  KSeFForbiddenError,
  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 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 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 extends KSeFApiError, so always check for KSeFRateLimitError before KSeFApiError. Otherwise, the KSeFApiError branch catches rate limit errors 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) and network errors (ECONNRESET, ECONNREFUSED, ETIMEDOUT, AbortError) are retried up to maxRetries times (default: 3) with exponential backoff and jitter.

For 429 responses, the Retry-After header is respected -- if present, its value overrides the calculated backoff delay.

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
KSeFRateLimitError429TooManyRequestsResponseretryAfterSeconds, recommendedDelayRetry with Retry-After
KSeFUnauthorizedError401RFC 7807 UnauthorizedProblemDetailsdetail, traceId, instanceToken refresh, then retry once
KSeFForbiddenError403RFC 7807 ForbiddenProblemDetailsreasonCode, detail, traceId, securityNone (not retryable)
KSeFAuthStatusError----referenceNumber, statusDescriptionNone
KSeFSessionExpiredError----messageNone
KSeFValidationError----details[] with field and messageNone (client-side)

Files Reference

FileContents
src/errors/types.tsApiErrorResponse, ExceptionDetails, TooManyRequestsResponse, UnauthorizedProblemDetails, ForbiddenProblemDetails, ForbiddenReasonCode
src/errors/ksef-error.tsKSeFError base class
src/errors/ksef-api-error.tsKSeFApiError with fromResponse() factory
src/errors/ksef-rate-limit-error.tsKSeFRateLimitError with fromRetryAfterHeader() factory
src/errors/ksef-unauthorized-error.tsKSeFUnauthorizedError
src/errors/ksef-forbidden-error.tsKSeFForbiddenError
src/errors/ksef-auth-status-error.tsKSeFAuthStatusError
src/errors/ksef-session-expired-error.tsKSeFSessionExpiredError
src/errors/ksef-validation-error.tsKSeFValidationError, ValidationDetail
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/polling.tspollUntil() with timeout
src/workflows/invoice-export-workflow.tsExport workflow error propagation

Released under the MIT License.