diff --git a/DocumentService.Client/Clients/BaseDocumentClient.cs b/DocumentService.Client/Clients/BaseDocumentClient.cs
new file mode 100644
index 0000000..76600c5
--- /dev/null
+++ b/DocumentService.Client/Clients/BaseDocumentClient.cs
@@ -0,0 +1,179 @@
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+
+namespace DocumentService.Client.Clients;
+
+///
+/// Base class for all DocumentService HTTP clients.
+/// Provides common HTTP operations and error handling.
+///
+///
+///
+///
+///
+///
+///
+public abstract class BaseDocumentClient(HttpClient HttpClient, ILogger logger)
+{
+ ///
+ ///
+ ///
+ protected readonly ILogger Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ ///
+ /// Sends a multipart/form-data POST with a single file and deserializes the JSON response.
+ ///
+ protected async Task SendMultipartAsync(
+ string endpoint,
+ Stream fileStream,
+ string fileName = "file.pdf",
+ CancellationToken cancellationToken = default)
+ {
+ using var content = new MultipartFormDataContent();
+ var streamContent = new StreamContent(fileStream);
+ streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
+ content.Add(streamContent, "file", fileName);
+
+ var response = await HttpClient.PostAsync(endpoint, content, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ return await response.Content.ReadFromJsonAsync(cancellationToken);
+ }
+
+ ///
+ /// Sends a multipart/form-data POST with a single file and returns the response as a stream.
+ ///
+ protected async Task SendMultipartForStreamAsync(
+ string endpoint,
+ Stream fileStream,
+ string fileName = "file.pdf",
+ CancellationToken cancellationToken = default)
+ {
+ using var content = new MultipartFormDataContent();
+ var streamContent = new StreamContent(fileStream);
+ streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
+ content.Add(streamContent, "file", fileName);
+
+ var response = await HttpClient.PostAsync(endpoint, content, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+#if NETFRAMEWORK
+ return await response.Content.ReadAsStreamAsync();
+#else
+ return await response.Content.ReadAsStreamAsync(cancellationToken);
+#endif
+ }
+
+ ///
+ /// Sends a multipart/form-data POST with a single file and returns the raw byte array.
+ ///
+ protected async Task SendMultipartForBinaryAsync(
+ string endpoint,
+ Stream fileStream,
+ string fileName = "file.pdf",
+ CancellationToken cancellationToken = default)
+ {
+ using var content = new MultipartFormDataContent();
+ var streamContent = new StreamContent(fileStream);
+ streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
+ content.Add(streamContent, "file", fileName);
+
+ var response = await HttpClient.PostAsync(endpoint, content, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+#if NETFRAMEWORK
+ return await response.Content.ReadAsByteArrayAsync();
+#else
+ return await response.Content.ReadAsByteArrayAsync(cancellationToken);
+#endif
+ }
+
+ ///
+ /// Sends a caller-built and returns the response as a stream.
+ /// Use this overload when the multipart body contains more than one file (e.g., merge).
+ ///
+ protected async Task SendMultipartContentForStreamAsync(
+ string endpoint,
+ MultipartFormDataContent content,
+ CancellationToken cancellationToken = default)
+ {
+ var response = await HttpClient.PostAsync(endpoint, content, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+#if NETFRAMEWORK
+ return await response.Content.ReadAsStreamAsync();
+#else
+ return await response.Content.ReadAsStreamAsync(cancellationToken);
+#endif
+ }
+
+ ///
+ /// Serializes as JSON, POSTs it, and deserializes the response.
+ ///
+ protected async Task SendJsonAsync(
+ string endpoint,
+ TRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ var response = await HttpClient.PostAsJsonAsync(endpoint, request, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+ return await response.Content.ReadFromJsonAsync(cancellationToken);
+ }
+
+ ///
+ /// Serializes as JSON, POSTs it, and returns the response as a stream.
+ ///
+ protected async Task SendJsonForStreamAsync(
+ string endpoint,
+ TRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ var response = await HttpClient.PostAsJsonAsync(endpoint, request, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+#if NETFRAMEWORK
+ return await response.Content.ReadAsStreamAsync();
+#else
+ return await response.Content.ReadAsStreamAsync(cancellationToken);
+#endif
+ }
+
+ ///
+ /// Serializes as JSON, POSTs it, and returns the raw byte array.
+ ///
+ protected async Task SendJsonForBinaryAsync(
+ string endpoint,
+ TRequest request,
+ CancellationToken cancellationToken = default)
+ {
+ var response = await HttpClient.PostAsJsonAsync(endpoint, request, cancellationToken);
+ response.EnsureSuccessStatusCode();
+
+#if NETFRAMEWORK
+ return await response.Content.ReadAsByteArrayAsync();
+#else
+ return await response.Content.ReadAsByteArrayAsync(cancellationToken);
+#endif
+ }
+
+ /// Converts a byte array to a Base64 string.
+ protected static string ToBase64(byte[] bytes) => Convert.ToBase64String(bytes);
+
+ /// Reads a stream fully into a byte array.
+ protected static async Task StreamToBytesAsync(Stream stream, CancellationToken cancellationToken = default)
+ {
+ if (stream is MemoryStream ms)
+ return ms.ToArray();
+
+ using var memoryStream = new MemoryStream();
+#if NET8_0
+ await stream.CopyToAsync(memoryStream, cancellationToken);
+#else
+ await stream.CopyToAsync(memoryStream);
+#endif
+ return memoryStream.ToArray();
+ }
+}
diff --git a/DocumentService.Client/Clients/PdfAttachmentClient.cs b/DocumentService.Client/Clients/PdfAttachmentClient.cs
new file mode 100644
index 0000000..21bb9f5
--- /dev/null
+++ b/DocumentService.Client/Clients/PdfAttachmentClient.cs
@@ -0,0 +1,124 @@
+using DocumentService.Client.Interfaces;
+using DocumentService.Client.Models.Requests;
+using Microsoft.Extensions.Logging;
+using System.IO.Compression;
+using System.Net.Http;
+
+namespace DocumentService.Client.Clients;
+
+///
+/// Implementation of PDF attachment client.
+///
+public class PdfAttachmentClient(HttpClient httpClient, ILogger logger)
+ : BaseDocumentClient(httpClient, logger), IPdfAttachmentClient
+{
+ ///
+ public async Task CheckAttachmentsAsync(Stream pdfStream, CancellationToken cancellationToken = default)
+ {
+ Logger.LogDebug("Checking PDF attachments from stream (multipart)");
+
+ var result = await SendMultipartAsync(
+ "/api/pdf/attachments/check",
+ pdfStream,
+ "document.pdf",
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+
+ ///
+ public async Task CheckAttachmentsAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
+ {
+ Logger.LogDebug("Checking PDF attachments from byte array (Base64 JSON)");
+
+ var request = new CheckPdfAttachmentsRequest { Base64Pdf = ToBase64(pdfBytes) };
+
+ var result = await SendJsonAsync(
+ "/api/pdf/attachments/check",
+ request,
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+
+ ///
+ public async Task> ExtractAttachmentsAsync(Stream pdfStream, CancellationToken cancellationToken = default)
+ {
+ Logger.LogDebug("Extracting PDF attachments from stream (multipart)");
+
+ var zipStream = await SendMultipartForStreamAsync(
+ "/api/pdf/attachments/extract",
+ pdfStream,
+ "document.pdf",
+ cancellationToken);
+
+ return UnzipToStreams(zipStream);
+ }
+
+ ///
+ public async Task> ExtractAttachmentsAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
+ {
+ Logger.LogDebug("Extracting PDF attachments from byte array (Base64 JSON)");
+
+ var request = new ExtractPdfAttachmentsRequest { Base64Pdf = ToBase64(pdfBytes) };
+
+ var zipStream = await SendJsonForStreamAsync(
+ "/api/pdf/attachments/extract",
+ request,
+ cancellationToken);
+
+ return UnzipToStreams(zipStream);
+ }
+
+ ///
+ [Obsolete("API endpoint not implemented yet")]
+ public Task AddAttachmentsAsync(Stream pdfStream, List attachments, CancellationToken cancellationToken = default)
+ {
+ Logger.LogWarning("AddAttachments endpoint is not implemented yet in API");
+ throw new NotImplementedException("API endpoint /api/pdf/attachments/add is not implemented yet");
+ }
+
+ ///
+ [Obsolete("API endpoint not implemented yet")]
+ public Task AddAttachmentsAsync(byte[] pdfBytes, List attachments, CancellationToken cancellationToken = default)
+ {
+ Logger.LogWarning("AddAttachments endpoint is not implemented yet in API");
+ throw new NotImplementedException("API endpoint /api/pdf/attachments/add is not implemented yet");
+ }
+
+ // ?????????????????????????????????????????????????????????????????????????
+ // Private helpers
+ // ?????????????????????????????????????????????????????????????????????????
+
+ ///
+ /// Reads a ZIP stream and returns a dictionary mapping each entry's full name
+ /// to an in-memory containing the decompressed bytes.
+ /// The caller owns the returned streams and is responsible for disposing them.
+ ///
+ private static Dictionary UnzipToStreams(Stream zipStream)
+ {
+ var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read, leaveOpen: false);
+
+ foreach (var entry in archive.Entries)
+ {
+ // Skip directory entries (name ends with '/')
+ if (string.IsNullOrEmpty(entry.Name))
+ continue;
+
+ var ms = new MemoryStream((int)entry.Length);
+
+ using (var entryStream = entry.Open())
+ {
+ entryStream.CopyTo(ms);
+ }
+
+ ms.Position = 0;
+ result[entry.FullName] = ms;
+ }
+
+ return result;
+ }
+}
+
diff --git a/DocumentService.Client/Clients/PdfConversionClient.cs b/DocumentService.Client/Clients/PdfConversionClient.cs
new file mode 100644
index 0000000..0ecb0ed
--- /dev/null
+++ b/DocumentService.Client/Clients/PdfConversionClient.cs
@@ -0,0 +1,48 @@
+using DocumentService.Client.Interfaces;
+using DocumentService.Client.Models.Requests;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+
+namespace DocumentService.Client.Clients;
+
+///
+/// Implementation of PDF conversion client (PDF ? PDF/A).
+///
+///
+/// All methods throw because the corresponding
+/// API endpoints are not yet implemented on the server side.
+///
+public class PdfConversionClient(HttpClient httpClient, ILogger logger) : BaseDocumentClient(httpClient, logger), IPdfConversionClient
+{
+ ///
+ [Obsolete("API endpoint not implemented yet")]
+ public Task ConvertToPdfAAsync(Stream pdfStream, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default)
+ {
+ Logger.LogWarning("ConvertToPdfA endpoint is not implemented yet in API");
+ throw new NotImplementedException("API endpoint POST /api/pdf/conversion/to-pdfa is not implemented yet");
+ }
+
+ ///
+ [Obsolete("API endpoint not implemented yet")]
+ public Task ConvertToPdfAAsync(byte[] pdfBytes, string pdfALevel = "PDF/A-3b", CancellationToken cancellationToken = default)
+ {
+ Logger.LogWarning("ConvertToPdfA endpoint is not implemented yet in API");
+ throw new NotImplementedException("API endpoint POST /api/pdf/conversion/to-pdfa is not implemented yet");
+ }
+
+ ///
+ [Obsolete("API endpoint not implemented yet")]
+ public Task ConvertFromPdfAAsync(Stream pdfStream, CancellationToken cancellationToken = default)
+ {
+ Logger.LogWarning("ConvertFromPdfA endpoint is not implemented yet in API");
+ throw new NotImplementedException("API endpoint POST /api/pdf/conversion/from-pdfa is not implemented yet");
+ }
+
+ ///
+ [Obsolete("API endpoint not implemented yet")]
+ public Task ConvertFromPdfAAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
+ {
+ Logger.LogWarning("ConvertFromPdfA endpoint is not implemented yet in API");
+ throw new NotImplementedException("API endpoint POST /api/pdf/conversion/from-pdfa is not implemented yet");
+ }
+}
diff --git a/DocumentService.Client/Clients/PdfOperationsClient.cs b/DocumentService.Client/Clients/PdfOperationsClient.cs
new file mode 100644
index 0000000..3ba4919
--- /dev/null
+++ b/DocumentService.Client/Clients/PdfOperationsClient.cs
@@ -0,0 +1,161 @@
+using DocumentService.Client.Interfaces;
+using DocumentService.Client.Models.Requests;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+using System.Net.Http.Headers;
+
+namespace DocumentService.Client.Clients;
+
+///
+/// Implementation of PDF operations client (merge, annotate, stamp).
+///
+public class PdfOperationsClient(HttpClient httpClient, ILogger logger) : BaseDocumentClient(httpClient, logger), IPdfOperationsClient
+{
+
+ // ==================== MERGE OPERATIONS ====================
+
+ ///
+ public async Task MergeAsync(IEnumerable pdfStreams, List? pageRanges = null, CancellationToken cancellationToken = default)
+ {
+ using var content = new MultipartFormDataContent();
+
+ foreach (var stream in pdfStreams)
+ {
+ var streamContent = new StreamContent(stream);
+ streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
+ content.Add(streamContent, "files", $"file_{Guid.NewGuid()}.pdf");
+ }
+
+ return await SendMultipartContentForStreamAsync("/api/pdf/operations/merge", content, cancellationToken);
+ }
+
+ ///
+ public async Task MergeAsync(IEnumerable pdfByteArrays, List? pageRanges = null, CancellationToken cancellationToken = default)
+ {
+ var request = new MergePdfsBase64Request
+ {
+ Base64Pdfs = [.. pdfByteArrays.Select(ToBase64)],
+ PageRanges = pageRanges
+ };
+
+ return await SendJsonForStreamAsync(
+ "/api/pdf/operations/merge",
+ request,
+ cancellationToken);
+ }
+
+ // ==================== ANNOTATION OPERATIONS ====================
+
+ ///
+ public async Task AnnotateAsync(Stream pdfStream, AddAnnotationBase64Request request, CancellationToken cancellationToken = default)
+ {
+ // For multipart, we need to send form data with all annotation parameters
+ using var content = new MultipartFormDataContent();
+
+ var streamContent = new StreamContent(pdfStream);
+ streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
+ content.Add(streamContent, "File", "document.pdf");
+
+ // Add annotation parameters as form fields
+ content.Add(new StringContent(request.AnnotationType.ToString()), "AnnotationType");
+ content.Add(new StringContent(request.PageNumber.ToString()), "PageNumber");
+ content.Add(new StringContent(request.X1.ToString()), "X1");
+ content.Add(new StringContent(request.Y1.ToString()), "Y1");
+
+ if (request.X2.HasValue)
+ content.Add(new StringContent(request.X2.Value.ToString()), "X2");
+ if (request.Y2.HasValue)
+ content.Add(new StringContent(request.Y2.Value.ToString()), "Y2");
+ if (request.Width.HasValue)
+ content.Add(new StringContent(request.Width.Value.ToString()), "Width");
+ if (request.Height.HasValue)
+ content.Add(new StringContent(request.Height.Value.ToString()), "Height");
+ if (!string.IsNullOrEmpty(request.Content))
+ content.Add(new StringContent(request.Content), "Content");
+ if (!string.IsNullOrEmpty(request.Author))
+ content.Add(new StringContent(request.Author), "Author");
+ if (!string.IsNullOrEmpty(request.Color))
+ content.Add(new StringContent(request.Color), "Color");
+ if (request.TextMarkupStyle.HasValue)
+ content.Add(new StringContent(request.TextMarkupStyle.Value.ToString()), "TextMarkupStyle");
+
+ content.Add(new StringContent(request.Origin.ToString()), "Origin");
+
+ return await SendMultipartContentForStreamAsync("/api/pdf/operations/annotate", content, cancellationToken);
+ }
+
+ ///
+ public async Task AnnotateAsync(byte[] pdfBytes, AddAnnotationBase64Request request, CancellationToken cancellationToken = default)
+ {
+ var requestWithPdf = request with { Base64Pdf = ToBase64(pdfBytes) };
+
+ return await SendJsonForStreamAsync(
+ "/api/pdf/operations/annotate",
+ requestWithPdf,
+ cancellationToken);
+ }
+
+ // ==================== STAMP OPERATIONS ====================
+
+ ///
+ public async Task StampAsync(Stream pdfStream, AddStampBase64Request request, CancellationToken cancellationToken = default)
+ {
+ // For multipart, we need to send form data with all stamp parameters
+ using var content = new MultipartFormDataContent();
+
+ var streamContent = new StreamContent(pdfStream);
+ streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
+ content.Add(streamContent, "File", "document.pdf");
+
+ // Add stamp parameters as form fields
+ content.Add(new StringContent(request.StampType.ToString()), "StampType");
+ content.Add(new StringContent(request.X.ToString()), "X");
+ content.Add(new StringContent(request.Y.ToString()), "Y");
+
+ if (request.PageNumbers != null && request.PageNumbers.Length > 0)
+ {
+ foreach (var pageNum in request.PageNumbers)
+ {
+ content.Add(new StringContent(pageNum.ToString()), "PageNumbers");
+ }
+ }
+
+ if (request.Width.HasValue)
+ content.Add(new StringContent(request.Width.Value.ToString()), "Width");
+ if (request.Height.HasValue)
+ content.Add(new StringContent(request.Height.Value.ToString()), "Height");
+
+ content.Add(new StringContent(request.Origin.ToString()), "Origin");
+
+ if (!string.IsNullOrEmpty(request.Text))
+ content.Add(new StringContent(request.Text), "Text");
+ if (!string.IsNullOrEmpty(request.FontName))
+ content.Add(new StringContent(request.FontName), "FontName");
+ if (request.FontSize.HasValue)
+ content.Add(new StringContent(request.FontSize.Value.ToString()), "FontSize");
+ if (!string.IsNullOrEmpty(request.Color))
+ content.Add(new StringContent(request.Color), "Color");
+ if (request.Opacity.HasValue)
+ content.Add(new StringContent(request.Opacity.Value.ToString()), "Opacity");
+ if (request.Rotation.HasValue)
+ content.Add(new StringContent(request.Rotation.Value.ToString()), "Rotation");
+
+ content.Add(new StringContent(request.Placement.ToString()), "Placement");
+
+ if (request.PredefinedType.HasValue)
+ content.Add(new StringContent(request.PredefinedType.Value.ToString()), "PredefinedType");
+
+ return await SendMultipartContentForStreamAsync("/api/pdf/operations/stamp", content, cancellationToken);
+ }
+
+ ///
+ public async Task StampAsync(byte[] pdfBytes, AddStampBase64Request request, CancellationToken cancellationToken = default)
+ {
+ var requestWithPdf = request with { Base64Pdf = ToBase64(pdfBytes) };
+
+ return await SendJsonForStreamAsync(
+ "/api/pdf/operations/stamp",
+ requestWithPdf,
+ cancellationToken);
+ }
+}
diff --git a/DocumentService.Client/Clients/PdfValidationClient.cs b/DocumentService.Client/Clients/PdfValidationClient.cs
new file mode 100644
index 0000000..bb2a179
--- /dev/null
+++ b/DocumentService.Client/Clients/PdfValidationClient.cs
@@ -0,0 +1,68 @@
+using DocumentService.Client.Interfaces;
+using DocumentService.Client.Models.Requests;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+
+namespace DocumentService.Client.Clients;
+
+///
+/// Implementation of PDF validation client.
+///
+public class PdfValidationClient(HttpClient httpClient, ILogger logger) : BaseDocumentClient(httpClient, logger), IPdfValidationClient
+{
+ ///
+ public async Task ValidatePdfAsync(Stream pdfStream, CancellationToken cancellationToken = default)
+ {
+ var result = await SendMultipartAsync(
+ "/api/pdf/validation/validate",
+ pdfStream,
+ "document.pdf",
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+
+ ///
+ public async Task ValidatePdfAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
+ {
+ var request = new ValidatePdfBase64Request
+ {
+ Base64Pdf = ToBase64(pdfBytes)
+ };
+
+ var result = await SendJsonAsync(
+ "/api/pdf/validation/validate",
+ request,
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+
+ ///
+ public async Task ValidatePdfAAsync(Stream pdfStream, CancellationToken cancellationToken = default)
+ {
+ var result = await SendMultipartAsync(
+ "/api/pdf/validation/validate-pdfa",
+ pdfStream,
+ "document.pdf",
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+
+ ///
+ public async Task ValidatePdfAAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
+ {
+ var request = new ValidatePdfABase64Request
+ {
+ Base64Pdf = ToBase64(pdfBytes)
+ };
+
+ var result = await SendJsonAsync(
+ "/api/pdf/validation/validate-pdfa",
+ request,
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+}
diff --git a/DocumentService.Client/Clients/SwissQrCodeClient.cs b/DocumentService.Client/Clients/SwissQrCodeClient.cs
new file mode 100644
index 0000000..ada933d
--- /dev/null
+++ b/DocumentService.Client/Clients/SwissQrCodeClient.cs
@@ -0,0 +1,44 @@
+using DocumentService.Client.Interfaces;
+using DocumentService.Client.Models.Requests;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+
+namespace DocumentService.Client.Clients;
+
+///
+/// Implementation of Swiss QR Code extraction client.
+///
+public class SwissQrCodeClient(HttpClient httpClient, ILogger logger) : BaseDocumentClient(httpClient, logger), ISwissQrCodeClient
+{
+ ///
+ public async Task ExtractSwissQrCodeAsync(Stream pdfStream, bool raw = false, CancellationToken cancellationToken = default)
+ {
+ var endpoint = $"/api/pdf/qr-code/extract-swiss?raw={raw}";
+
+ var result = await SendMultipartAsync(
+ endpoint,
+ pdfStream,
+ "document.pdf",
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+
+ ///
+ public async Task ExtractSwissQrCodeAsync(byte[] pdfBytes, bool raw = false, CancellationToken cancellationToken = default)
+ {
+ var endpoint = $"/api/pdf/qr-code/extract-swiss?raw={raw}";
+
+ var request = new ExtractSwissQrCodeBase64Request
+ {
+ Base64Pdf = ToBase64(pdfBytes)
+ };
+
+ var result = await SendJsonAsync(
+ endpoint,
+ request,
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+}
diff --git a/DocumentService.Client/Clients/ZugferdClient.cs b/DocumentService.Client/Clients/ZugferdClient.cs
new file mode 100644
index 0000000..b7f1589
--- /dev/null
+++ b/DocumentService.Client/Clients/ZugferdClient.cs
@@ -0,0 +1,87 @@
+using DocumentService.Client.Interfaces;
+using DocumentService.Client.Models.Requests;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+
+namespace DocumentService.Client.Clients;
+
+///
+/// Implementation of ZUGFeRD client (detection and extraction).
+///
+public class ZugferdClient(HttpClient httpClient, ILogger logger) : BaseDocumentClient(httpClient, logger), IZugferdClient
+{
+ ///
+ public async Task HasZugferdAsync(Stream pdfStream, CancellationToken cancellationToken = default)
+ {
+ var result = await SendMultipartAsync(
+ "/api/pdf/zugferd/has-zugferd",
+ pdfStream,
+ "document.pdf",
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+
+ ///
+ public async Task HasZugferdAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
+ {
+ var request = new HasZugferdRequest { Base64Pdf = ToBase64(pdfBytes) };
+
+ var result = await SendJsonAsync(
+ "/api/pdf/zugferd/has-zugferd",
+ request,
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+
+ ///
+ public async Task ExtractZugferdAsync(Stream pdfStream, CancellationToken cancellationToken = default)
+ {
+ // asFile=true returns the XML file directly (application/xml stream)
+ return await SendMultipartForStreamAsync(
+ "/api/pdf/zugferd/extract?asFile=true",
+ pdfStream,
+ "document.pdf",
+ cancellationToken);
+ }
+
+ ///
+ public async Task ExtractZugferdAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
+ {
+ var request = new ExtractZugferdRequest { Base64Pdf = ToBase64(pdfBytes) };
+
+ // format=file returns the XML file directly (application/xml stream)
+ return await SendJsonForStreamAsync(
+ "/api/pdf/zugferd/extract?format=file",
+ request,
+ cancellationToken);
+ }
+
+ ///
+ public async Task ExtractZugferdAsResultAsync(Stream pdfStream, CancellationToken cancellationToken = default)
+ {
+ // asFile=false returns JSON with metadata + XML content
+ var result = await SendMultipartAsync(
+ "/api/pdf/zugferd/extract?asFile=false",
+ pdfStream,
+ "document.pdf",
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+
+ ///
+ public async Task ExtractZugferdAsResultAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
+ {
+ var request = new ExtractZugferdRequest { Base64Pdf = ToBase64(pdfBytes) };
+
+ // format=json returns JSON with metadata + XML content
+ var result = await SendJsonAsync(
+ "/api/pdf/zugferd/extract?format=json",
+ request,
+ cancellationToken);
+
+ return result ?? throw new InvalidOperationException("API returned null response");
+ }
+}