TBJ.Integrations.MF.KSeF 1.0.1

dotnet add package TBJ.Integrations.MF.KSeF --version 1.0.1
                    
NuGet\Install-Package TBJ.Integrations.MF.KSeF -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TBJ.Integrations.MF.KSeF" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TBJ.Integrations.MF.KSeF" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="TBJ.Integrations.MF.KSeF" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TBJ.Integrations.MF.KSeF --version 1.0.1
                    
#r "nuget: TBJ.Integrations.MF.KSeF, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TBJ.Integrations.MF.KSeF@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TBJ.Integrations.MF.KSeF&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=TBJ.Integrations.MF.KSeF&version=1.0.1
                    
Install as a Cake Tool

TBJ.Integrations.MF.KSeF

build NuGet

Biblioteka klienta .NET 8, 9, 10 dla KSeF API v2 (Krajowy System e-Faktur, Ministerstwo Finansów).

Projekt jest wzorowany na TBJ.Integrations.NFZ.UmwApi i dostarcza typowanych abstrakcji do komunikacji z KSeF: uwierzytelnianie, sesje wystawiania faktur (online i batch), wyszukiwanie/pobieranie faktur oraz eksporty.


Spis treści


Wymagania

  • .NET 10
  • Konto/podmiot zarejestrowany w KSeF (TEST / DEMO / PROD)
  • Token KSeF lub kwalifikowany podpis elektroniczny (XAdES) do uwierzytelniania

Rejestracja w DI

Biblioteka rejestruje tylko infrastrukturę HTTP i klienty.

builder.Services.AddKSeFApi(opt =>
{
    opt.BaseUrl = "https://api-test.ksef.mf.gov.pl/v2";
    opt.Timeout = TimeSpan.FromSeconds(60);
});

lub z appsettings.json:

builder.Services.AddKSeFApi(builder.Configuration);
{
  "KSeFApi": {
    "BaseUrl": "https://api-test.ksef.mf.gov.pl/v2",
    "Timeout": "00:01:00",
    "UseProblemDetailsErrorFormat": true
  }
}

Model uwierzytelniania (wielotenantowość)

Biblioteka obsługuje dwa scenariusze dostarczania access tokenu:

Scenariusz A — konto tenanta (token per-request)

Token dostępowy (accessToken) pobierany z kontekstu tenanta i przekazywany jako parametr do każdej metody. To preferowany tryb dla aplikacji wielotenantowych.

// Token pobrany z bazy danych tenanta
string accessToken = await tokenStore.GetTokenAsync(tenantId);

var sessions = serviceProvider.GetRequiredService<ISessionsClient>();
var session = await sessions.OpenOnlineSessionAsync(request, accessToken: accessToken);

Scenariusz B — nasze konto (domyślny token z konfiguracji)

Jeśli accessToken nie jest przekazany (parametr null), klienty automatycznie użyją DefaultAccessToken z KSeFApiOptions.

// appsettings.json
{
  "KSeFApi": {
    "BaseUrl": "https://api.ksef.mf.gov.pl/v2",
    "DefaultAccessToken": "eyJ..."  // token naszego konta
  }
}

// Rejestracja
builder.Services.AddKSeFApi(builder.Configuration);

// Wywołanie — accessToken=null → używany DefaultAccessToken z opcji
var sessions = serviceProvider.GetRequiredService<ISessionsClient>();
var session = await sessions.OpenOnlineSessionAsync(request);  // bez accessToken

Uwaga: Token KSeF ma ograniczony czas życia. W środowiskach produkcyjnych rozważ dynamiczne odświeżanie tokenu (RefreshTokensAsync) zamiast statycznej wartości w konfiguracji.


Zakres funkcjonalności

Obszar Funkcje
Uwierzytelnianie GetChallengeAsync, AuthenticateWithXadesAsync, AuthenticateWithTokenAsync, WaitForAuthenticationCompletedAsync, RedeemTokensAsync, RefreshTokensAsync, zarządzanie sesjami auth
Sesje online otwieranie, zamykanie, wysyłka pojedynczych faktur, status, UPO, lista faktur, faktury odrzucone
Sesje batch otwieranie sesji wsadowej, zamykanie, wysyłka paczki faktur
Faktury wyszukiwanie, pobieranie szczegółów, status, UPO, pobieranie pliku, eksport

Uwierzytelnianie i tokeny

Jak uzyskać token dostępowy

KSeF API używa dwuetapowego uwierzytelniania:

  1. Rozpoczęcie uwierzytelniania — metodą XAdES lub tokenem KSeF:
var authClient = serviceProvider.GetRequiredService<IAuthenticationClient>();

// Metoda 1: XAdES
// Krok A: pobierz challenge (ważny ~10 min)
var challenge = await authClient.GetChallengeAsync();

// Krok B: w aplikacji nadrzędnej zbuduj i podpisz dokument AuthTokenRequest,
//         w którym challenge jest wpisany wewnątrz XML
string signedXml = BuildAndSignAuthTokenRequest(
    challenge.Challenge,
    contextIdentifier: new ContextIdentifier { Type = ContextIdentifierType.Nip, Identifier = "1234567890" },
    subjectIdentifierType: SubjectIdentifierType.CertificateSubject);

// Krok C: wyślij podpisany dokument do KSeF
var initResponse = await authClient.AuthenticateWithXadesAsync(signedXml);

// Metoda 2: Token KSeF (zaszyfrowany kluczem publicznym MF)
var initResponse = await authClient.AuthenticateWithTokenAsync(new KsefTokenRequest
{
    Token = encryptedToken,      // zaszyfrowany RSA-OAEP token z MCU
    Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
    ContextIdentifier = new ContextIdentifier { Type = ContextIdentifierType.Nip, Identifier = "1234567890" },
    SubjectIdentifierType = SubjectIdentifierType.CertificateSubject,
});

Po inicjalizacji otrzymujesz authenticationToken oraz referenceNumber.

Uwaga: BuildAndSignAuthTokenRequest w przykładzie powyżej to funkcja po stronie aplikacji nadrzędnej. Obecna wersja biblioteki przyjmuje gotowy podpisany XML; w kolejnej iteracji planowany jest wbudowany helper do budowy dokumentu AuthTokenRequest zgodnie ze schematem KSeF.

  1. Polling statusu — operacja jest asynchroniczna:
var status = await authClient.WaitForAuthenticationCompletedAsync(
    initResponse.ReferenceNumber,
    initResponse.AuthenticationToken);
  1. Wymiana na tokeny dostępowe:
var tokens = await authClient.RedeemTokensAsync(
    initResponse.ReferenceNumber,
    initResponse.AuthenticationToken);

string accessToken = tokens.AccessToken;
string refreshToken = tokens.RefreshToken;
  1. Odświeżanie tokena (przed wygaśnięciem):
var refreshed = await authClient.RefreshTokensAsync(refreshToken);
accessToken = refreshed.AccessToken;

Tokeny są per-tenant

accessToken i refreshToken NIE są przechowywane w tej bibliotece. Są tożsamością konkretnego podmiotu (tenanta) w KSeF. Aplikacja nadrzędna powinna:

  • przechowywać tokeny w konfiguracji/bazie danych powiązanej z tenantem,
  • pobrać odpowiedni token dla aktualnego kontekstu,
  • przekazać go jako parametr metodom klientów KSeF.

Kompletny przykład wysyłki faktury z aplikacji nadrzędnej

Poniżej znajduje się przykładowa usługa w aplikacji nadrzędnej, która:

  1. Pobiera token KSeF i NIP dla aktualnego tenanta z bazy danych.
  2. Otwiera sesję online.
  3. Wysyła pojedynczą fakturę (plik XML).
  4. Pobiera UPO dla sesji.
  5. Zamyka sesję w bloku finally.
public class KSeFInvoiceService
{
    private readonly ISessionsClient _sessions;
    private readonly IAuthenticationClient _auth;
    private readonly ITenantKSeFTokenStore _tokenStore;
    private readonly ILogger<KSeFInvoiceService> _logger;

    public KSeFInvoiceService(
        ISessionsClient sessions,
        IAuthenticationClient auth,
        ITenantKSeFTokenStore tokenStore,
        ILogger<KSeFInvoiceService> logger)
    {
        _sessions = sessions;
        _auth = auth;
        _tokenStore = tokenStore;
        _logger = logger;
    }

    /// <summary>
    /// Wysyła fakturę do KSeF w imieniu wskazanego tenanta.
    /// </summary>
    public async Task<SendInvoiceResponse> SendInvoiceAsync(
        int tenantId,
        Stream invoiceXml,
        CancellationToken ct = default)
    {
        // 1. Pobierz konfigurację KSeF dla tenanta z bazy.
        var tenantConfig = await _tokenStore.GetTenantConfigAsync(tenantId, ct);
        var accessToken = tenantConfig.AccessToken;
        var refreshToken = tenantConfig.RefreshToken;
        var nip = tenantConfig.Nip;

        // 2. Spróbuj odświeżyć token, jeśli jest bliski wygaśnięcia.
        if (tenantConfig.AccessTokenExpiresAt < DateTimeOffset.UtcNow.AddMinutes(5))
        {
            _logger.LogInformation("Odświeżanie tokena KSeF dla tenant {TenantId}", tenantId);
            var refreshed = await _auth.RefreshTokensAsync(refreshToken, ct);
            accessToken = refreshed.AccessToken;
            refreshToken = refreshed.RefreshToken;

            await _tokenStore.SaveTokensAsync(
                tenantId,
                accessToken,
                refreshToken,
                DateTimeOffset.UtcNow.AddSeconds(refreshed.ExpiresIn),
                ct);
        }

        // 3. Otwórz sesję online.
        var session = await _sessions.OpenOnlineSessionAsync(
            new OpenSessionRequest
            {
                ContextIdentifier = new ContextIdentifier
                {
                    Type = ContextIdentifierType.Nip,
                    Identifier = nip,
                },
            },
            accessToken,
            ct);

        var sessionId = session.SessionIdentifier.SessionIdentifierValue;
        _logger.LogInformation(
            "Otwarto sesję KSeF {SessionId} dla tenant {TenantId}",
            sessionId,
            tenantId);

        try
        {
            // 4. Wyślij fakturę.
            var result = await _sessions.SendInvoiceAsync(
                sessionId,
                invoiceXml,
                new InvoiceMetadata { Format = "FA_V2" },
                accessToken,
                ct);

            _logger.LogInformation(
                "Wysłano fakturę {KsefNumber} w sesji {SessionId}",
                result.KsefNumber,
                sessionId);

            // 5. Pobierz UPO dla sesji.
            var upo = await _sessions.GetSessionUpoAsync(sessionId, accessToken, ct);
            _logger.LogInformation(
                "UPO dla sesji {SessionId}: {Upo}",
                sessionId,
                upo.Upo);

            return result;
        }
        catch (KSeFApiException ex)
        {
            _logger.LogError(
                ex,
                "Błąd KSeF podczas wysyłki faktury dla tenant {TenantId}: HTTP {StatusCode}, kod {KSeFCode}",
                tenantId,
                ex.StatusCode,
                ex.KsefExceptionCode);
            throw;
        }
        finally
        {
            // 6. Zamknij sesję niezależnie od wyniku.
            try
            {
                await _sessions.CloseOnlineSessionAsync(sessionId, accessToken, ct);
                _logger.LogInformation("Zamknięto sesję {SessionId}", sessionId);
            }
            catch (Exception closeEx)
            {
                _logger.LogWarning(closeEx, "Nie udało się zamknąć sesji {SessionId}", sessionId);
            }
        }
    }
}

Przykładowy interfejs repozytorium tenanta

public interface ITenantKSeFTokenStore
{
    Task<TenantKSeFConfig> GetTenantConfigAsync(int tenantId, CancellationToken ct);
    Task SaveTokensAsync(
        int tenantId,
        string accessToken,
        string refreshToken,
        DateTimeOffset accessTokenExpiresAt,
        CancellationToken ct);
}

public class TenantKSeFConfig
{
    public string AccessToken { get; set; } = string.Empty;
    public string RefreshToken { get; set; } = string.Empty;
    public string Nip { get; set; } = string.Empty;
    public DateTimeOffset AccessTokenExpiresAt { get; set; }
}

Wywołanie z kontrolera

[ApiController]
[Route("api/[controller]")]
public class InvoicesController : ControllerBase
{
    private readonly KSeFInvoiceService _ksefService;

    [HttpPost("send")]
    public async Task<IActionResult> SendInvoice(IFormFile invoice, CancellationToken ct)
    {
        // tenantId pochodzi z kontekstu użytkownika / nagłówka
        int tenantId = GetCurrentTenantId();

        await using var stream = invoice.OpenReadStream();
        var result = await _ksefService.SendInvoiceAsync(tenantId, stream, ct);

        return Ok(new { result.KsefNumber, result.ReferenceNumber });
    }
}

Przykłady użycia

AuthenticationClient

var authClient = serviceProvider.GetRequiredService<IAuthenticationClient>();

// Pobierz listę aktywnych sesji
await foreach (var session in authClient.GetAllSessionsAsync(pageSize: 25))
{
    Console.WriteLine(session.ReferenceNumber);
}

// Unieważnij aktualną sesję
await authClient.TerminateCurrentSessionAsync(accessToken);

SessionsClient

var sessions = serviceProvider.GetRequiredService<ISessionsClient>();

// Sesja online
var openResponse = await sessions.OpenOnlineSessionAsync(
    new OpenSessionRequest { /* ... */ },
    accessToken);

await sessions.SendInvoiceAsync(
    openResponse.SessionIdentifier.SessionIdentifierValue,
    invoiceStream,
    new InvoiceMetadata { Format = "FA_V2" },
    accessToken);

var upo = await sessions.GetSessionUpoAsync(
    openResponse.SessionIdentifier.SessionIdentifierValue,
    accessToken);

await sessions.CloseOnlineSessionAsync(
    openResponse.SessionIdentifier.SessionIdentifierValue,
    accessToken);

// Sesja wsadowa
var batch = await sessions.OpenBatchSessionAsync(
    new OpenBatchSessionRequest
    {
        ContextIdentifier = new ContextIdentifier { /* ... */ },
        Invoices = new[]
        {
            new BatchInvoice
            {
                Invoice = encryptedInvoiceBase64,
                Metadata = new InvoiceMetadata { Format = "FA_V2" },
            },
        },
    },
    accessToken);

await sessions.CloseBatchSessionAsync(
    batch.SessionIdentifier.SessionIdentifierValue,
    accessToken);

InvoicesClient

var invoices = serviceProvider.GetRequiredService<IInvoicesClient>();

// Wyszukaj faktury
var page = await invoices.QueryInvoicesAsync(
    dateFrom: DateTimeOffset.UtcNow.AddDays(-30),
    dateTo: DateTimeOffset.UtcNow,
    pageSize: 25,
    accessToken: accessToken);

// Iteruj przez wszystkie strony
await foreach (var invoice in invoices.GetAllInvoicesAsync(
    dateFrom: DateTimeOffset.UtcNow.AddDays(-30),
    accessToken: accessToken))
{
    Console.WriteLine(invoice.KsefNumber);
}

// Pobierz plik faktury
await using var stream = await invoices.DownloadInvoiceAsync(ksefNumber, accessToken);
await stream.CopyToAsync(File.Create("faktura.xml"));

// Eksport
var export = await invoices.InitiateExportAsync(
    new ExportRequest
    {
        Criteria = new ExportCriteria { DateFrom = DateTimeOffset.UtcNow.AddDays(-30) },
        Format = ExportFormat.Json,
    },
    accessToken);

// Pobierz wynik eksportu
await using var exportStream = await invoices.DownloadExportAsync(export.ExportId, accessToken);

Paginacja

KSeF API nie używa numerów stron. Lista wyników jest paginowana przez token kontynuacji (continuationToken):

var page = await invoices.QueryInvoicesAsync(pageSize: 25, accessToken: accessToken);

while (page.HasNextPage)
{
    page = await invoices.QueryInvoicesAsync(
        pageSize: 25,
        continuationToken: page.ContinuationToken,
        accessToken: accessToken);
}

Wygodniej jest użyć IAsyncEnumerable<T>:

await foreach (var invoice in invoices.GetAllInvoicesAsync(accessToken: accessToken))
{
    // ...
}

Obsługa błędów

Błędy KSeF są reprezentowane jako KSeFApiException:

try
{
    await invoices.GetInvoiceAsync(ksefNumber, accessToken);
}
catch (KSeFApiException ex)
{
    Console.WriteLine($"HTTP {ex.StatusCode}, KSeF code {ex.KsefExceptionCode}: {ex.Description}");
}

Biblioteka domyślnie wysyła nagłówek X-Error-Format: problem-details, więc błędy są parsowane z formatu RFC 7807. Jeśli API zwróci starszy format ExceptionResponse, zostanie on również poprawnie odczytany.


Konfiguracja

Właściwość Domyślnie Opis
BaseUrl https://api-test.ksef.mf.gov.pl/v2 Bazowy adres KSeF API
Timeout 60 s Timeout żądań HTTP
UseProblemDetailsErrorFormat true Czy żądać błędów w formacie Problem Details
MaxAuthenticationStatusPollAttempts 60 Maksymalna liczba prób pollingu statusu auth
AuthenticationStatusPollDelay 1 s Odstęp między próbami pollingu
DefaultAccessToken null Domyślny token naszego konta (Scenariusz B). Gdy null, token musi być przekazany per-żądanie.

Znane ograniczenia i kolejne kroki

  1. XAdES — biblioteka przyjmuje gotowy podpisany XML. Należy dodać osobny helper do budowy AuthTokenRequest i podpisywania XAdES.
  2. Szyfrowanie tokena KSeF — token musi być zaszyfrowany kluczem publicznym MF (RSA-OAEP). Klucze należy pobrać z /public-key-certificates.
  3. Szyfrowanie faktur — wysyłane pliki faktur muszą być zaszyfrowane kluczem sesji. Obecnie SendInvoiceAsync przyjmuje gotowy strumień.
  4. Modele — część modeli jest uproszczona; po pierwszych rzeczywistych wywołaniach warto zweryfikować je z dokumentacją KSeF.
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 130 7/4/2026
1.0.0 110 7/4/2026