Skip to content

alapierre/go-ksef-client

Sonarcloud Status Renovate enabled Go Go Report Card Go Reference

GO KSeF 2.0 API client library

API structure generated by ogen-go with AuthFacade, EncryptionService, Client and in memory TokenProvider.

This is not a full-featured SDK, but a production-ready, battle-tested client for the most critical KSeF operations, especially authentication and signing workflows.

This project is actively used in production environments (70+ commercial deployments) and is considered stable for its core use cases.

The current implementation focuses primarily on authentication and QR code generation for KSeF 2.0:

  • Authentication using:
  • KSeF Token
  • KSeF Certificate
  • QR code (one and two) generation compliant with KSeF requirements

Additionally, thanks to integration with github.com/alapierre/godss, the client supports:

  • Qualified electronic signature
  • Qualified electronic seal

These features are fully production-proven and widely used in real-world integrations.

At this stage, the library does not aim to provide full coverage of the KSeF API.

  • Many API operations are not yet exposed through high-level abstractions (facades)
  • The focus has been on reliability and correctness of critical paths rather than breadth of features

Currently supported (beyond authentication):

  • Opening interactive sessions
  • Sending invoices in interactive mode
  • Batch invoice submission

Other areas of the KSeF API may be added gradually based on demand and real-world usage.

Ready futures

  • Sending invoices — interactively and in batch

Design assumptions

Context-bound NIP

  • The client reads the taxpayer identifier (NIP) from context.Context.
  • Rationale:
    • Consistency: a single, implicit source of truth for the current processing scope.
    • Propagation: NIP travels with the request lifetime across layers without changing function signatures.
    • Safety: avoids accidental mix-ups when multiple NIPs may be processed concurrently.

How it works

Set NIP into context once, near the request boundary:

ctx := ksef.Context(ctx, nipString)

Components that require NIP retrieve it from context:

nip, ok := ksef.NipFromContext(ctx)

Authorization flows (e.g., AuthWithToken) expect NIP to be present in the provided context. If missing, an error is returned.

Guidelines

  • Always derive child contexts from the NIP-bearing parent (use the same ctx for subsequent calls).
  • Do not pass NIP as a separate function parameter; rely on context for clarity and consistency.
  • Validate NIP before injecting it into context if your application requires strict input checks.
  • When spawning goroutines or timeouts, carry the same context forward (e.g., context.WithTimeout(ctx, ...)) so NIP remains available.

Example

At startup or per request:

ctx := context.Background()
ctx = ksef.Context(ctx, "")

Use ctx for all API calls that require NIP

TokenProvider Multi-NIP token cache

TokenProvider is a simple in-memory cache for KSeF tokens. It is thread-safe and can be used concurrently from multiple goroutines. In the next implementation, locks will be contextual — currently, the mutex is locked regardless of the NIP

EncryptionService

EncryptionService is responsible for:

  • fetching and caching KSeF public certificates,
  • encryption for two distinct usages:
    • KsefTokenEncryption (auth token + timestamp),
    • SymmetricKeyEncryption (encrypting the AES key used for invoices),
  • optional initialization without contacting the API (when you already have certificates/keys).

Keys are cached separately for each usage and automatically refreshed before expiration using a safety margin (refreshSkew).

Key points:

  • Two independent caches: tokenPub (KsefTokenEncryption) and symKeyPub (SymmetricKeyEncryption).
  • Automatic on-demand fetch from the API only when a key is missing or close to expiration.
  • ForceRefresh() refreshes both caches (does not return keys).
  • Optional preload of certificates/keys in the constructor to avoid API calls.

Initialization

Standard (keys fetched on demand from the API):

// Go
env := ksef.Test
httpClient := &http.Client{ Timeout: 15 * time.Second }

enc, err := cipher.NewEncryptionService(env, httpClient)
if err != nil {
    // handle error
}

No API calls — preload with existing certs or keys

// Go
enc, err := cipher.NewEncryptionService(
    env,
    httpClient,
    cipher.WithPreloadedKeys(cipher.PreloadedKeys{
        // Option A: certificates in DER Base64 form
        TokenCertBase64:     "<base64-der-token>",
        SymmetricCertBase64: "<base64-der-symmetric>",

        // Option B: ready *rsa.PublicKey instances
        // TokenRSAPub:     tokenPub,
        // SymmetricRSAPub: symPub,

        // Optional validity dates (if omitted, they’ll be read from certs)
        // TokenValidTo:     time.Time{},
        // SymmetricValidTo: time.Time{},
    }),
)
if err != nil {
    // handle error
}

Encryption

Encrypt KSeF token (token + timestamp in ms), RSA-OAEP(SHA-256)

encryptedTokenBytes, err := enc.EncryptKsefToken(ctx, token, challenge.Timestamp)
if err != nil {
    // handle error
}

Encrypt the invoice symmetric key (Usage=SymmetricKeyEncryption)

encryptedSymKey, err := enc.EncryptSymmetricKey(ctx, aesKey)

If a required key is missing or expired, EncryptionService will fetch/refresh the proper certificate and update its cache automatically.

if err := enc.ForceRefresh(ctx); err != nil {
    // handle error
}

After ForceRefresh, use the regular methods (EncryptKsefToken, EncryptSymmetricKey, or GetPublicKeyFor). ForceRefresh itself does not return keys.

Implementation notes

  • RSA-OAEP with SHA-256 is used for all RSA encryptions.
  • Each key usage has its own key and validity tracked independently.
  • refreshSkew is a safety margin checked on-demand: when a key is requested and its ValidTo − now ≤ refreshSkew (default 2 minutes), the service refreshes it; there is no periodic timer.
  • GetPublicKeyFor(ctx, usage) returns the current key for the given usage and will fetch/cache it if needed.

Authentication with KSeF Token

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/alapierre/go-ksef-client/ksef"
	"github.com/alapierre/go-ksef-client/ksef/util"
	"github.com/sirupsen/logrus"
)

func main() {

	logrus.SetLevel(logrus.DebugLevel)

	nip := util.GetEnvOrFailed("KSEF_NIP")
	token := util.GetEnvOrFailed("KSEF_TOKEN")

	httpClient := &http.Client{
		Timeout: 15 * time.Second,
	}

	env := ksef.Test
	authFacade, err := ksef.NewAuthFacade(env, httpClient)
	if err != nil {
		panic(err)
	}

	encryptor, err := ksef.NewEncryptionService(env, httpClient)
	if err != nil {
		panic(err)
	}

	ctx := context.Background()
	ctx = ksef.Context(ctx, nip)
	tokens, err := ksef.WithKsefToken(ctx, authFacade, encryptor, token)

	if err != nil {
		panic(err)
	}

	fmt.Println(tokens.AccessToken.Token)
	fmt.Println(tokens.RefreshToken.Token)

	refreshToken, err := authFacade.RefreshToken(ctx, tokens.RefreshToken.Token)
	if err != nil {
		panic(err)
	}

	fmt.Println(refreshToken.GetToken())
	fmt.Println("Refreshed")
}

Opening an interactive session and sending invoices

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
    
    "github.com/alapierre/go-ksef-client/ksef"
    "github.com/alapierre/go-ksef-client/ksef/api"
    "github.com/alapierre/go-ksef-client/ksef/util"
    "github.com/sirupsen/logrus"
)

func openSession() {

	logrus.SetLevel(logrus.DebugLevel)

	nip := util.GetEnvOrFailed("KSEF_NIP")
	token := util.GetEnvOrFailed("KSEF_TOKEN")
	buer := util.GetEnvOrFailed("KSEF_BUYER_NIP")

	httpClient := &http.Client{
		Timeout: 15 * time.Second,
	}

	env := ksef.Test

	authFacade, err := ksef.NewAuthFacade(env, httpClient)

	if err != nil {
		panic(err)
	}

	encryptor, err := ksef.NewEncryptionService(env, httpClient)
	if err != nil {
		panic(err)
	}

	ctx := context.Background()
	ctx = ksef.Context(ctx, nip)

	provider := ksef.NewTokenProvider(authFacade, func(ctx context.Context) (*api.AuthenticationTokensResponse, error) {
		return ksef.WithKsefToken(ctx, authFacade, encryptor, token)
	})

	client, err := ksef.NewClient(env, httpClient, provider)

	form := api.FormCode{
		SystemCode:    "FA (3)",
		SchemaVersion: "1-0E",
		Value:         "FA",
	}

	key, err := ksef.GenerateRandom256BitsKey()
	iv, err := ksef.GenerateRandom16BytesIv()
	encryptedKey, err := encryptor.EncryptSymmetricKey(ctx, key)

	enc := api.EncryptionInfo{
		EncryptedSymmetricKey: encryptedKey,
		InitializationVector:  iv,
	}

	session, err := client.OpenInteractiveSession(ctx, form, enc)
	if err != nil {
		panic(err)
	}

	fmt.Println(session)
	
	// send invoices	
	
	invoice, err := util.ReplacePlaceholdersInXML("../invoice_fa_3_type.xml", map[string]any{
		"NIP":        nip,
		"ISSUE_DATE": time.Now(),
		"BUYER_NIP":  buer,
	})
	if err != nil {
		panic(err)
	}

	ir, err := client.SendInvoice(ctx, string(session.ReferenceNumber), api.OptBool{}, invoice, key, iv)
	if err != nil {
		panic(err)
	}

	fmt.Println(ir)
}

Client validation

Cause by ogen issue: ogen-go/ogen#1570 validation of SHA-256 Base64 string is not working corectly. So in the generated code, the validation is commented out. Alternative is to change the following definition in the openapi.json:

from:

      "Sha256HashBase64": {
        "maxLength": 44,
        "minLength": 44,
        "type": "string",
        "description": "SHA-256 w Base64.",
        "format": "byte"
      },

to:

      "Sha256HashBase64": {
        "maxLength": 32,
        "minLength": 32,
        "type": "string",
        "description": "SHA-256 w Base64.",
        "format": "byte"
      },

Too many requests support

KSeF OpenAPI specification now defines support for HTTP 429 (Too Many Requests) responses.

However, the current version of this client does not yet handle 429 responses at the facade level:

ogen-generated low-level client is capable of receiving 429 responses but higher-level abstractions (facades) do not yet implement automatic retries, Retry-After header handling and backoff strategies.

As a result, handling of rate limiting must currently be implemented on the application side.

Support for proper 429 handling is planned for future releases.

Reference: CIRFMF/ksef-docs#347

TokenProvider design and benchmarks

The TokenProvider component is responsible for providing and transparently refreshing KSeF access tokens. It implements api.SecuritySource and is used by the generated client to attach Authorization: Bearer headers to all protected requests.

Design rationale

The provider is designed with the following goals:

  • Correctness under concurrency
    Multiple goroutines may request tokens for the same or different NIPs at the same time. TokenProvider uses an internal cache protected by a mutex to ensure:

    • exactly one full authentication per NIP when tokens are missing or expired,
    • serialized refresh of access tokens per NIP,
    • race-free reads from the cache (verified with go test -race).
  • Steady‑state performance
    In typical usage, most calls should reuse a cached access token that is still valid. This “fast path” avoids network calls and does not allocate memory on the heap.

  • Simplicity vs. complexity
    The current implementation uses a single mutex and a per‑NIP cache. Given the expected load (usually 1–2, up to ~40 NIPs per application instance), more complex patterns (like per‑NIP locks or singleflight groups) were evaluated and considered unnecessary for now. The benchmarks below confirm that the cost of the provider itself is negligible compared to KSeF network latency.

What we benchmark and why

Benchmarks focus on the Bearer method of TokenProvider in several scenarios:

  • Sequential, warm cache
    Multiple calls for the same NIP with tokens already cached and valid.
    This measures the absolute overhead of the provider in the happy‑path case.

  • Parallel, same NIP, warm cache
    Many goroutines requesting a token for the same NIP simultaneously, with tokens already cached.
    This stresses the locking strategy and measures contention on the mutex.

  • Parallel, many NIPs, cold cache
    Many goroutines requesting tokens for many different NIPs when the cache is empty.
    This simulates the worst case (initial warm‑up), where each NIP requires a full authentication. The benchmark is dominated by the simulated authentication delay; it is useful mainly to estimate upper bounds and allocations during warm‑up, not to compare micro‑optimizations.

  • Parallel, many NIPs, warm cache
    Many goroutines requesting tokens for multiple NIPs after the cache has been pre‑filled.
    This is close to real‑world steady‑state with multiple tenants and concurrent traffic.

Sample benchmark results

Go 1.25

go test ./ksef -bench 'BenchmarkTokenProvider_' -benchmem
goos: linux
goarch: amd64
pkg: github.com/alapierre/go-ksef-client/ksef
cpu: Intel(R) Core(TM) i9-9940X CPU @ 3.30GHz
BenchmarkTokenProvider_Sequential-28                    13828598                73.83 ns/op            0 B/op          0 allocs/op
BenchmarkTokenProvider_ParallelSameNip-28                2263152               516.3 ns/op             0 B/op          0 allocs/op
BenchmarkTokenProvider_ParallelManyNips-28                507884              2033 ns/op              64 B/op          2 allocs/op
BenchmarkTokenProvider_ParallelManyNipsWarmCache-28      1261300               970.5 ns/op            64 B/op          2 allocs/op
PASS
ok      github.com/alapierre/go-ksef-client/ksef        21.349s

Regenerating client form OpenAPI

Because of how ogen handles size validation for string fields with the byte format, the maxLength and minLength constraints must be removed from the Sha256HashBase64 type or set to a value of 31. Ogen validates the length of the underlying byte array rather than the length of the Base64-encoded string.

      "Sha256HashBase64": {
        "type": "string",
        "description": "SHA-256 w Base64.",
        "format": "byte"
      },

Prevent failure when parsing JSON with unknown fields:

replace:

  • return errors.Errorf("unexpected field %q", k)

to:

  • return d.Skip()

on generated parser oas_json_gen.go

About

Go KSeF 2.0 API Client library

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors