Fitko.FitConnect.Client 4.0.0-rc.1.2

This is a prerelease version of Fitko.FitConnect.Client.
dotnet add package Fitko.FitConnect.Client --version 4.0.0-rc.1.2
                    
NuGet\Install-Package Fitko.FitConnect.Client -Version 4.0.0-rc.1.2
                    
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="Fitko.FitConnect.Client" Version="4.0.0-rc.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Fitko.FitConnect.Client" Version="4.0.0-rc.1.2" />
                    
Directory.Packages.props
<PackageReference Include="Fitko.FitConnect.Client" />
                    
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 Fitko.FitConnect.Client --version 4.0.0-rc.1.2
                    
#r "nuget: Fitko.FitConnect.Client, 4.0.0-rc.1.2"
                    
#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 Fitko.FitConnect.Client@4.0.0-rc.1.2
                    
#: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=Fitko.FitConnect.Client&version=4.0.0-rc.1.2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Fitko.FitConnect.Client&version=4.0.0-rc.1.2&prerelease
                    
Install as a Cake Tool

FIT-Connect SDK — Client Module

High-level .NET client for the FIT-Connect platform: sending submissions and replies, receiving and decrypting them, looking up routes, and managing destinations. This is the typical entry point for an integrator — it hides the JWE/JWS, REST and protocol details behind a fluent builder API and a single IFitConnectClient resolved from the DI container.

If you only need crypto, hashing, or validation primitives, depend on Fitko.FitConnect.Core directly. For malware scanning, add Fitko.FitConnect.VirusScanning. For ZBP messages, add Fitko.FitConnect.Zbp.

Install

<PackageReference Include="Fitko.FitConnect.Client" />

Quick Start

// Register services
services.AddFitConnect();   // reads "FitConnect" section from IConfiguration

// Resolve client
var client = serviceProvider.GetRequiredService<IFitConnectClient>();
// Pick the role-scoped facade for the destination you're acting as:
// - AsOrganisation() for an A/B-destination (Verwaltung/Wirtschaft) — send/receive submissions,
//   send replies. Has no method to receive replies.
// - AsOnlineService() for a C-destination (Onlinedienst) — send submissions, send/receive replies.
//   Has no method to receive submissions.
// A method the caller can't legitimately use for their role simply isn't on the facade's type.
var organisation = client.AsOrganisation();

Legacy flat access: client.SubmissionService (typed ISubmissionService) remains available and exposes the full send/receive surface regardless of role. It is kept for backward compatibility, but AsOrganisation()/AsOnlineService() are the recommended entry points since they make illegal operations for your role a compile error rather than a runtime REST rejection.

Role verification: by default the SDK resolves the configured DestinationId's type (A/B/C) once while the host starts, and asking for the facade of the wrong role then throws FitConnectConfigurationException naming the correct one. A DestinationId that does not exist aborts host startup. Turn this off with Client.VerifyDestinationOnStartup = false for offline scenarios; the check needs an IHost (it runs as an IHostedService), so a bare ServiceProvider skips it.

// Send a submission
var request = OutgoingSubmissionBuilder.Builder()
    .WithDestinationId(destinationId)
    .WithServiceType("urn:de:fim:leika:leistung:99400048079000", "Service Name")
    .WithMetadataVersion(new Version(3, 0, 0))
    .WithJsonData(jsonData, new Uri("https://schema.example.org/schema.json"))
    .WithAttachment(Attachment.FromFile("document.pdf", "application/pdf"), shouldBeChunked: false)
    .Build();

SentSubmission sent = await organisation.SendSubmission(request);
// Receive a submission
IncomingSubmission received = await organisation.FetchSubmission(
    sent.SubmissionId, sent.CaseId);

string data = received.GetDataAsString();
List<Attachment> attachments = received.GetAttachments();

Registration

// From IConfiguration (default section: "FitConnect")
services.AddFitConnect();
services.AddFitConnect("MySection");

// Programmatic
services.AddFitConnect(config =>
{
    config.Client = new ClientConfig
    {
        ClientId = "...",
        ClientSecret = "...",
        DestinationId = Guid.Parse("..."),   // C-Destination (sender identity, required in v3)
        DecryptionKeys = [privateKeyJson]
    };
    config.EnvironmentTag = "TEST";
});

Configuration

appsettings.json

{
  "FitConnect": {
    "Client": {
      "ClientId": "<client-id>",
      "ClientSecret": "<client-secret>",
      "DestinationId": "<c-destination-uuid>",
      "VerifyDestinationOnStartup": true,
      "DecryptionKeys": [
        "<RSA private JWK JSON>",
        "<RSA private JWK JSON>"
      ]
    },
    "EnvironmentTag": "TEST",
    "Http": {
      "Timeouts": {
        "Read": 30,
        "Write": 30,
        "Connection": 10
      },
      "Proxy": {
        "Host": "",
        "Port": 0
      },
      "Retry": {
        "AllowRetries": true,
        "RetryableStatusCodes": [408, 429, 500, 502, 503, 504],
        "MaxRetryCount": 5,
        "InitialDelayInMs": 500
      }
    },
    "Attachments": {
      "BaseDirectory": "/var/lib/fit-connect",
      "ChunkAllAttachments": true,
      "ChunkSizeInMB": 50
    },
    "Validation": {
      "Metadata": true,
      "Data": true,
      "Attachments": true
    }
  }
}

EnvironmentTag accepts: TEST | STAGE | PROD | LOCAL | CI | CUSTOM. DecryptionKeys takes an ordered list — the first key is primary, subsequent keys enable zero-downtime rotation. BaseDirectory defaults to the system temp path when omitted.

Custom environment

services.AddFitConnect(config =>
{
    config.EnvironmentTag = "CUSTOM";
    config.CustomEnvironment = new CustomEnvironmentUrls
    {
        TokenUrl = "https://my-auth-server/token",
        SubmissionUrl = "https://my-api-host/submission-api",
        RoutingUrl = "https://my-routing-server",
        DestinationUrl = "https://my-api-host/destination-api"
    };
});

Sending submissions

Sending is available on both role-scoped facades — organisation.SendSubmission(...) (A/B → A/B) and onlineService.SendSubmission(...) (C → A/B) — the examples below use organisation.

// JSON data + attachment
var request = OutgoingSubmissionBuilder.Builder()
    .WithDestinationId(destinationId)
    .WithServiceType(leikaKey, serviceName)
    .WithMetadataVersion(new Version(3, 0, 0))
    .WithJsonData(jsonData, schemaUri)
    .WithAttachment(Attachment.FromFile("evidence.pdf", "application/pdf"), shouldBeChunked: false)
    .Build();

SentSubmission sent = await organisation.SendSubmission(request);
Console.WriteLine($"submissionId={sent.SubmissionId}, caseId={sent.CaseId}");
// XML data
var request = OutgoingSubmissionBuilder.Builder()
    .WithDestinationId(destinationId)
    .WithServiceType(leikaKey, serviceName)
    .WithMetadataVersion(new Version(3, 0, 0))
    .WithXmlData(xmlString, schemaUri)
    .Build();
// With reply channel (so the receiver can send a reply back).
// In v3 the FIT-Connect reply channel carries the online service's own C-Destination id
// that replies should be sent to, plus the schemas it can receive replies in.
var request = OutgoingSubmissionBuilder.Builder()
    ...
    .WithJsonData(data, schemaUri)
    .WithReplyChannel(ReplyChannel.OfFitConnect(replyPublicKey, myCDestinationId))
    .Build();

Receiving submissions

// Fetch a specific submission — organisation (A/B) only, use client.AsOrganisation().
IncomingSubmission received = await organisation.FetchSubmission(submissionId, caseId);
string data = received.GetDataAsString();

// Inspect the receive report and decide: accept a clean transfer, reject a broken one.
if (received.Acceptable())
{
    await organisation.AcceptSubmission(received);
}
else
{
    Console.WriteLine(received.Report.Describe());       // human-readable diagnosis
    await organisation.RejectSubmission(received, received.Report.AsProblems().ToList());
}

// Fetch all available submissions from a destination — each carries its own report
List<IncomingSubmission> all = await organisation
    .FetchAvailableSubmissionsFromDestination(destinationId);

Report, not exceptions: FetchSubmission no longer throws on a validation finding. Everything the receive pipeline finds (schema violations, hash/auth-tag mismatches, virus-scan findings, ...) is collected into received.Report (a ReceiveReport). Inspect received.Acceptable() / received.Report to decide what to do; only hard technical failures (missing keys, failed decryption, transport errors) still throw. AcceptSubmission refuses (throws SubmissionNotAcceptableException) while the report still has errors — warnings never block acceptance. Report.AsProblems() yields the problems ready to hand to RejectSubmission. The SDK does not automatically send a reject event. To poll for the final processing status (Accepted / Rejected) after sending, use GetSubmissionState(sentSubmission).

Replies

// Send a reply (receiver-to-sender) — organisation (A/B) only.
var replyRequest = ReplyRequestBuilder
    .ForCase(caseId)
    .EncryptWith(senderPublicKey)
    .WithMetadataVersion(new Version(3, 0, 0))
    .WithJsonData(replyJson, schemaUri)
    .WithAttachment(Attachment.FromBytes(pdfBytes, "application/pdf"))
    .Build();

SendReplyResponse sentReply = await organisation.SendReply(replyRequest);
// Answering a received submission — extracts the case ID and the FIT-Connect reply-channel
// encryption key from the received submission automatically. Throws FitConnectReplyException
// if the sender did not offer a FIT-Connect reply channel.
var replyRequest = ReplyRequestBuilder
    .ForSubmission(receivedSubmission)
    .WithMetadataVersion(new Version(3, 0, 0))
    .WithJsonData(replyJson, schemaUri)
    .Build();

SendReplyResponse sentReply = await organisation.SendReply(replyRequest);
// Fetch and decrypt a reply (sender-to-receiver) — online service (C) only, use client.AsOnlineService().
var onlineService = client.AsOnlineService();
IncomingSubmission reply = await onlineService
    .FetchSpecificReply(replyId, privateDecryptionKeyJson);

await onlineService.AcceptReply(reply);
// or:
await onlineService.RejectReply(reply, problems);

Reply-channel keys

A reply-channel key belongs to one case: the public half goes out with the submission that opens the case, and the private half decrypts the reply that comes back. Storing it in between is the application's job (a vault, an HSM, a column on the case), so IReplyKeys is how that store is handed to the SDK — after which it resolves the right key per reply itself:

using Fitko.FitConnect.Client.Models.Crypto;

// At send time, keep the private half against the case just opened.
var sent = await onlineService.SendSubmission(submissionRequest);
await vault.Store(sent.CaseId, privateJwk);

// Later: no need to know which key this particular reply needs.
IncomingSubmission reply = await onlineService.FetchSpecificReply(
    replyId, ReplyKeys.FromLookup(vault.Find));

ReplyKeys.Of(dictionary) wraps an in-memory map (tests, short-lived processes), ReplyKeys.FromLookup(func) adapts an existing store, and ReplyKeys.None() states explicitly that a destination only ever sends. If the store holds no key for the reply's case, a FitConnectReplyException names both the case and the reply.

Message status

GetSubmissionState / GetReplyState read the latest event-log entry. Event-log entries are signed rather than encrypted, so a status check needs no decryption key — including for messages still awaiting pickup:

// Outgoing: poll for the final state after sending.
EventState state = await organisation.GetSubmissionState(sentSubmission);
EventState replyState = await organisation.GetReplyState(sentReply);

// Incoming, still queued: check state without fetching/decrypting the message.
foreach (var waiting in await onlineService.FetchAvailableReplies() ?? [])
    Console.WriteLine(await onlineService.GetReplyState(waiting));

All four throw FitConnectMappingException when the event log is empty — "no status yet" is deliberately not conflated with "no status".

Attachments

// From file path
Attachment file = Attachment.FromFile("document.pdf", "application/pdf");

// From bytes (in-memory)
Attachment bytes = Attachment.FromBytes(pdfBytes, "application/pdf");

// From a stream
Attachment stream = Attachment.FromStream(
    inputStream, "application/xml", "data.xml", shouldBeChunked: false);

// From a repeatable stream supplier (e.g. blob storage — reopened on retry)
Attachment blob = Attachment.FromStreamSupplier(
    () => blobStorage.Open(documentId), "application/pdf", "document.pdf");

Routing

Destination discovery goes through client.Directory:

// LeiKa search, scoped to exactly one area (AGS / ARS / AreaId — enforced by AreaFilter)
IReadOnlyList<Route> routes = await client.Directory.FindByService(
    leikaKey: "urn:de:fim:leika:leistung:99400048079000",
    area: AreaFilter.Ars("081190090090"));

// Process-message search (returns full Destination objects)
MultipleDestinations? matches = await client.Directory.FindByProcessMessage(processModelId, messageId, "DE09");

AreaList? areas = await client.Directory.FindAreas(["Leipzig"]);

// Active encryption key of a destination (the key its EncryptionKid points at)
ApiJwk key = await client.Directory.GetActiveEncryptionKey(destinationId);

Destination management

IDestinationApiClient destinations = client.DestinationApiService;

Destination dest           = await destinations.GetDestination(destinationId);
MultipleDestinations list  = await destinations.ListDestinations(offset: 0, limit: 100);
Destination created        = await destinations.CreateDestination(createDestination);
await destinations.AddKey(destinationId, publicJwk);
DestinationAttachmentLimit limit = await destinations.GetDestinationAttachmentLimit(destinationId);

Callbacks

A destination can carry a callback URL and secret, so the delivery service notifies you when new messages are waiting instead of you polling. The callback endpoint is publicly reachable, so verify every incoming callback before acting on it, using CallbackValidationUtil from Fitko.FitConnect.Core:

using Fitko.FitConnect.Core.Callbacks;

// Pass the raw request body: re-serializing or trimming it changes the bytes and the HMAC will not match.
var result = CallbackValidationUtil.ValidateCallback(
    request.Headers[CallbackHeaders.Authentication],
    request.Headers[CallbackHeaders.Timestamp],
    rawBodyBytes,
    callbackSecret);

if (!result.IsValid) return Results.Unauthorized();

The body only announces what is available (NewSubmissionsCallback, NewRepliesCallback, NewEventsCallback) — fetch the actual messages through the authenticated API afterwards.

Build and Test

dotnet build Fitko.FitConnect.Client
dotnet test Fitko.FitConnect.Client.Test

Further reading

License

Source code is licensed under the EUPL.

Rechtlicher Hinweis: Dieses Software Development Kit (SDK) ist dazu bestimmt, die Anbindung einer Software an die FIT-Connect-Infrastruktur zu ermöglichen. Hierfür kann das SDK in die anzubindende Software integriert werden. Erfolgt die Integration des SDK in unveränderter Form, liegt keine Bearbeitung im Sinne der EUPL bzw. des deutschen Urheberrechts vor. Die Art und Weise der Verlinkung des SDK führt insbesondere nicht zur Schaffung eines abgeleiteten Werkes. Die unveränderte Übernahme des SDK in eine anzubindende Software führt damit nicht dazu, dass die anzubindende Software unter den Bedingungen der EUPL zu lizenzieren ist. Für die Weitergabe des SDK selbst – in unveränderter oder bearbeiteter Form, als Quellcode oder ausführbares Programm – gelten die Lizenzbedingungen der EUPL in unveränderter Weise.

Product Compatible and additional computed target framework versions.
.NET 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
4.0.0-rc.1.2 70 9/15/2026