Epistola.Contract.Client 1.2.0

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

Epistola .NET Client

.NET client library for the Epistola document generation API, generated from the OpenAPI contract with OpenAPI Generator (csharp / HttpClient) and hand-written glue for identity, authentication, error handling, result collection, and client-side validation.

It is the .NET counterpart of the Kotlin client and targets .NET 8 — consumable by any modern .NET project (ASP.NET Core, worker services, console apps, …).

Release history: CHANGELOG.md.

Installation

dotnet add package Epistola.Contract.Client

The generated API namespaces are Epistola.Client.Api (operations) and Epistola.Client.Model (DTOs). The hand-written helpers live under Epistola.Client.{Identity,Auth,Error,Http,Collect,Validation}.

Quick start

using Epistola.Client.Api;
using Epistola.Client.Auth;
using Epistola.Client.Http;
using Epistola.Client.Identity;
using Epistola.Client.Model;

var identity = ClientIdentity.Builder()
    .NodeId("my-pod-123")                       // defaults to hostname
    .Product("my-app", "1.0.0")                 // appended to User-Agent
    .Build();

var signer = JwtSigner.Builder()
    .ConsumerId("my-consumer")
    .PrivateKey(JwtSigner.LoadPrivateKey("private.pem"))
    .Build();

var http = new EpistolaHttpClientBuilder()
    .BaseUrl("https://epistola.example.com/api")
    .Identity(identity)                         // User-Agent + X-EP-Node-Id
    .JwtSigner(signer)                          // Authorization: Bearer <jwt>
    .InstallProblemDetailHandler()              // typed ProblemDetailException
    .Build();

var templates = new TemplatesApi(http, "https://epistola.example.com/api");
var template = templates.GetTemplate("my-tenant", "default", "monthly-invoice");

The builder always installs two corrections the C# generator gives no way to configure on the model: a handler that rewrites the request Content-Type to the versioned Epistola media type application/vnd.epistola.v1+json, and one that drops top-level properties you never set from request bodies instead of sending them as null.

The second is not cosmetic. The generated models are plain nullable properties with no way to distinguish "not set" from "explicitly null", so whatever the serializer does with an unset property becomes the request's meaning. On the API's PATCH operations a null is an instruction — description and contact are documented "null to clear", expiresAt as "null to remove expiry" — so a default serializer turned renaming a consumer into renaming it and erasing the rest. Nulls inside caller-supplied free-form data (a generation request's data) are left alone; they are yours.

The trade is that clearing a field is not expressible — but it never was, since you could not clear one field without clearing every other you had not set.

Client identity

Every request must carry User-Agent and X-EP-Node-Id. ClientIdentity builds them; the first User-Agent token is always epistola-contract/{contractVersion}:

User-Agent: epistola-contract/0.11.0 my-app/1.0.0
X-EP-Node-Id: my-pod-123

Authentication

JwtSigner mints short-lived self-signed JWTs (RSA-2048+ or EC P-256) with iss, iat, exp, and a unique jti per request. For OAuth 2.0 client-credentials, supply your own bearer handler via EpistolaHttpClientBuilder.PrimaryHandler(...) or add a DelegatingHandler around the chain.

Static tenant API keys can be sent with .ApiKey("epk_..."), which sets Authorization: ApiKey <key>. The legacy X-API-Key header remains supported for existing integrations, but is deprecated. Some Epistola Suite deployments may disable API-key authentication entirely; with InstallProblemDetailHandler(), switch on ProblemDetailException.TypeSlug == KnownProblemSlugs.API_KEY_AUTH_DISABLED and guide the caller to JWT auth.

Error handling

With InstallProblemDetailHandler(), application/problem+json error responses raise a typed ProblemDetailException. Switch on the stable TypeSlug and compare against KnownProblemSlugs (generated from the contract's x-problem-types registry):

using Epistola.Client.Error;

try
{
    templates.GetTemplate("my-tenant", "default", "unknown");
}
catch (ProblemDetailException e)
{
    switch (e.TypeSlug)
    {
        case KnownProblemSlugs.NOT_FOUND:
            Console.WriteLine($"not found: {e.Detail}");
            break;
        case KnownProblemSlugs.VALIDATION_ERROR:
            foreach (var err in e.Errors) Console.WriteLine($"{err.Field}: {err.Message}");
            break;
        default:                                 // always keep a default branch
            Console.WriteLine($"{e.ProblemStatus} {e.Title}");
            break;
    }
}

ProblemDetailException extends the generated ApiException, so existing catch (ApiException) sites keep working. Non-problem errors fall through to the generated ApiException.

Document generation & result collection

Poll completed/failed generation results with ResultCollector — NDJSON streaming (constant memory), compression (gzip built-in; lz4/zstd auto-detected when K4os.Compression.LZ4 / ZstdSharp are present), adaptive polling, and partition-aware routing helpers:

using Epistola.Client.Collect;

var collector = ResultCollector.Builder()
    .HttpClient(http)
    .TenantId("acme-corp")
    .Handler(result =>
    {
        switch (result.Status)
        {
            case "COMPLETED": Download(result.DocumentId, result.CorrelationId); break;
            case "FAILED":    LogFailure(result.CorrelationId, result.Error);    break;
        }
    })
    .Build();

collector.Start();          // blocks, running the adaptive poll loop; Stop() to end

CollectOnce() / CollectOnceAsync() perform a single poll for custom scheduling; Kick() shortens the backoff when a result is expected soon. Partition helpers: PartitionFor, IsMyPartition, RoutingKeyToMe.

Client-side schema validation

Validate request data against a template's JSON Schema before sending:

using Epistola.Client.Validation.Schema;

var validating = new ValidatingGenerationApi(new GenerationApi(http), new TemplatesApi(http));
validating.GenerateDocument("my-tenant", request);   // throws TemplateDataValidationException on failure

The generated request/response models also carry fail-fast constraint checks via Validate() extension methods (from Epistola.Client.Validation):

using Epistola.Client.Validation;

var request = new CreateTenantRequest(id: "acme-corp", name: "Acme").Validate();

Building from source

This module is generated from the bundled OpenAPI spec. From the repository root:

make bundle          # produce openapi.yaml
make build-dotnet    # generate.sh (client + derived sources) then dotnet build

generate.sh runs the OpenAPI Generator and a small derived-source generator (KnownProblemSlugs, Validate() methods, and the contract version) — the .NET analogue of the Kotlin build's generation tasks. Generated sources are not committed.

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 was computed.  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 was computed.  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.2.0 90 9/3/2026
1.1.0 110 8/20/2026
1.0.1 112 8/4/2026
1.0.0 99 7/30/2026
0.16.1 113 7/30/2026
0.16.0 110 7/29/2026
0.15.0 109 7/28/2026
0.14.0 107 7/23/2026
0.13.0 107 7/22/2026
0.12.0 110 7/17/2026