ksef-rb

Ruby client for the Polish KSeF 2.0 (Krajowy System e-Faktur) National e-Invoicing System.

Targets the FA(3) schema (mandatory since February 2026). Built against the official OpenAPI spec at https://api-test.ksef.mf.gov.pl/docs/v2/openapi.json and the CIRFMF reference clients for C# and Java.

Status

Pre-1.0. The public API is small on purpose and stable across the v0.1 line, but additions are expected as more KSeF features land.

Installation

gem "ksef-rb"

Quick start

require "ksef"

Ksef.configure do |c|
  c.environment = :test          # :test, :demo, or :production
  c.user_agent  = "MyApp / ksef-rb #{Ksef::VERSION}"
end

client = Ksef::Client.new(
  nip:         "1234567890",
  credentials: Ksef::Credentials::Token.new(ENV.fetch("KSEF_TOKEN"))
)

client.sessions.with_interactive do |_session|
  headers = client.invoices.query(
    subject_type: :recipient,
    date_from:    Time.now.utc - (7 * 24 * 3600),
    date_to:      Time.now.utc,
    date_type:    :issue  # :permanent_storage (default), :invoicing, or :issue
  )

  headers.each do |h|
    puts "#{h.ksef_reference_number}  #{h.issuer_nip}  #{h.gross_amount} #{h.currency}"
  end

  xml = client.invoices.fetch_xml(headers.first.ksef_reference_number)
  File.write("invoice.xml", xml)
end

#query returns at most page_size headers per call (default 100, max 250). Paginate by bumping page_offset until a short page comes back:

all = []
offset = 0
loop do
  page = client.invoices.query(
    subject_type: :recipient,
    date_from:    Date.new(2026, 4, 1),
    date_to:      Date.new(2026, 5, 31),
    date_type:    :issue,
    page_size:    250,
    page_offset:  offset
  )
  all.concat(page)
  break if page.size < 250

  offset += 1
end

Ksef::InvoiceHeader exposes (among others): ksef_reference_number, invoice_number, issuer_nip, issuer_name, recipient_nip, recipient_name, issued_on, gross_amount, net_amount, vat_amount, currency, invoicing_mode, invoice_type, form_code, form_schema_version, permanently_stored_at, has_attachment?, self_invoicing?, and the original payload via raw.

Authentication

v0.1 ships with token-based auth using the long-lived integration tokens minted in the KSeF portal after a Profil Zaufany / qualified-seal login.

The full handshake — /auth/challenge, /auth/ksef-token, status polling at /auth/{ref}, and /auth/token/redeem — is performed automatically by Ksef::Sessions#with_interactive. The integration token is encrypted with RSA-OAEP (SHA-256) using the public key returned by /security/public-key-certificates.

with_interactive always tears the session down by calling DELETE /auth/sessions/current, even when the block raises.

Errors

All KSeF-specific errors inherit from Ksef::Error:

Class When
Ksef::AuthError 401, 403, or auth-status failure (code: 450, etc.)
Ksef::NotFoundError 404
Ksef::RateLimitError 429 (exposes #retry_after in seconds when sent)
Ksef::ServerError 5xx
Ksef::ClientError other 4xx
Ksef::ConfigurationError bad config

Every error captures status, body, and the KSeF-supplied code.

What's not in v0.1.0

Feature Status
Token-based auth shipped
Interactive sessions shipped
Inbound invoice metadata query shipped
Inbound invoice XML fetch shipped
Inbound invoice PDF visualisation stubbed — KSeF 2.0 has no server-side PDF endpoint; render client-side from the XML using the official XSLT (wizualizacja-faktury_v3-0.xsl)
Certificate-based auth (qualified seal) stubbed (Ksef::Credentials::Certificate)
Batch sessions not yet
Outbound invoice issuance not yet
UPO download stubbed (Ksef::Invoices#fetch_upo)
Offline / QR-code modes not yet

NotImplementedError is raised from the stubs.

Configuration reference

Ksef.configure do |c|
  c.environment  = :test        # :test, :demo, :production
  c.user_agent   = "..."        # appended to every request
  c.timeout      = 30           # seconds
  c.open_timeout = 10           # seconds
  c.api_version  = "v2"         # path segment; defaults to v2
  c.base_url     = nil          # override entirely (useful for tests)
  c.logger       = Logger.new($stdout)  # wires Faraday's logger middleware
end

Ksef::Client.new(configuration:) accepts a per-client Configuration, which is the duplicated global configuration by default.

Development

bundle install
bundle exec rspec

The suite uses VCR (opt-in, via the :vcr metadata tag) and WebMock. Live re-recording against the sandbox is gated behind KSEF_RECORD=true and a real token in KSEF_TOKEN.

License

MIT — see LICENSE.txt.