diff --git a/receiverUI/EnvelopeGenerator.ReceiverUI.Web/EnvelopeGenerator.ReceiverUI.Web.Client/Api/ReceiverApiClient.cs b/receiverUI/EnvelopeGenerator.ReceiverUI.Web/EnvelopeGenerator.ReceiverUI.Web.Client/Api/ReceiverApiClient.cs
new file mode 100644
index 00000000..9cbddc59
--- /dev/null
+++ b/receiverUI/EnvelopeGenerator.ReceiverUI.Web/EnvelopeGenerator.ReceiverUI.Web.Client/Api/ReceiverApiClient.cs
@@ -0,0 +1,214 @@
+using System.Net;
+using System.Net.Http.Json;
+using EnvelopeGenerator.ReceiverUI.Web.Client.Api.Models;
+
+namespace EnvelopeGenerator.ReceiverUI.Web.Client.Api;
+
+///
+/// Typed HTTP client for the EnvelopeGenerator receiver API.
+/// All endpoints are routed through the BFF (same origin), so the
+/// authentication cookie set by the API is automatically attached
+/// by the browser to every request issued by the injected HttpClient.
+///
+public class ReceiverApiClient
+{
+ private readonly HttpClient _http;
+ private readonly ILogger _logger;
+
+ public ReceiverApiClient(HttpClient http, ILogger logger)
+ {
+ _http = http;
+ _logger = logger;
+ }
+
+ // ?? Receiver Auth ????????????????????????????????????????????????
+
+ public Task GetStatusAsync(string envelopeKey, CancellationToken ct = default)
+ => GetAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/status", ct);
+
+ public Task SubmitAccessCodeAsync(string envelopeKey, AccessCodeRequest req, CancellationToken ct = default)
+ => PostAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/access-code", req, ct);
+
+ public Task SubmitTfaCodeAsync(string envelopeKey, TfaCodeRequest req, CancellationToken ct = default)
+ => PostAuthAsync($"api/receiverauth/{Uri.EscapeDataString(envelopeKey)}/tfa", req, ct);
+
+ // ?? Envelope Receiver ????????????????????????????????????????????
+
+ public async Task GetEnvelopeReceiverAsync(string envelopeKey, CancellationToken ct = default)
+ {
+ var res = await _http.GetAsync($"api/envelopereceiver/{Uri.EscapeDataString(envelopeKey)}", ct);
+ if (!res.IsSuccessStatusCode)
+ return null;
+ return await res.Content.ReadFromJsonAsync(cancellationToken: ct);
+ }
+
+ /// Downloads the document bytes for the receiver to display in a PDF viewer.
+ public async Task GetDocumentAsync(string envelopeKey, CancellationToken ct = default)
+ {
+ var res = await _http.GetAsync($"api/document/{Uri.EscapeDataString(envelopeKey)}", ct);
+ if (!res.IsSuccessStatusCode)
+ return null;
+ return await res.Content.ReadAsByteArrayAsync(ct);
+ }
+
+ // ?? Annotation / Sign / Reject ???????????????????????????????????
+
+ ///
+ /// Returns the signature placeholders the authenticated receiver must sign.
+ ///
+ public async Task> GetSignatureElementsAsync(CancellationToken ct = default)
+ {
+ try
+ {
+ return await _http.GetFromJsonAsync>("api/annotation/elements", ct)
+ ?? new List();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to fetch signature elements.");
+ return new List();
+ }
+ }
+
+ ///
+ /// Submits the signed envelope using the Blazor-friendly endpoint.
+ ///
+ public async Task SignBlazorAsync(BlazorSignaturePayload payload, CancellationToken ct = default)
+ {
+ var res = await _http.PostAsJsonAsync("api/annotation/blazor", payload, ct);
+ return res.StatusCode;
+ }
+
+ ///
+ /// Fetches the TOTP QR code + registration deadline for the given
+ /// envelope-receiver key (encoded uuid+signature). The API generates
+ /// a fresh secret on first call and persists it server-side.
+ ///
+ public async Task<(TfaRegistrationResponse? Data, HttpStatusCode Status)> GetTfaRegistrationAsync(string envelopeReceiverId, CancellationToken ct = default)
+ {
+ try
+ {
+ var res = await _http.GetAsync($"api/tfa/{Uri.EscapeDataString(envelopeReceiverId)}", ct);
+ if (!res.IsSuccessStatusCode)
+ return (null, res.StatusCode);
+ var data = await res.Content.ReadFromJsonAsync(cancellationToken: ct);
+ return (data, res.StatusCode);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to fetch TFA registration for {Key}", envelopeReceiverId);
+ return (null, HttpStatusCode.InternalServerError);
+ }
+ }
+
+ /// Submits the signed annotations payload. Returns the HTTP status code.
+ public async Task SignAsync(TPayload? payload, CancellationToken ct = default)
+ {
+ var res = payload is null
+ ? await _http.PostAsync("api/annotation", content: null, ct)
+ : await _http.PostAsJsonAsync("api/annotation", payload, ct);
+ return res.StatusCode;
+ }
+
+ public async Task RejectAsync(string reason, CancellationToken ct = default)
+ {
+ var res = await _http.PostAsJsonAsync("api/annotation/reject", reason, ct);
+ return res.IsSuccessStatusCode;
+ }
+
+ // ?? Read-only share ??????????????????????????????????????????????
+
+ public async Task ShareReadOnlyAsync(ReadOnlyShareRequest req, CancellationToken ct = default)
+ {
+ var res = await _http.PostAsJsonAsync("api/readonly", req, ct);
+ return res.IsSuccessStatusCode;
+ }
+
+ // ?? Auth (logout) ????????????????????????????????????????????????
+
+ public async Task LogoutAsync(CancellationToken ct = default)
+ {
+ var res = await _http.PostAsync("auth/logout", content: null, ct);
+ return res.IsSuccessStatusCode;
+ }
+
+ // ?? Localization ?????????????????????????????????????????????????
+
+ public async Task?> GetLocalizationStringsAsync(CancellationToken ct = default)
+ {
+ try
+ {
+ return await _http.GetFromJsonAsync>("api/Localization", ct);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to fetch localization strings.");
+ return null;
+ }
+ }
+
+ public async Task SetLanguageAsync(string language, CancellationToken ct = default)
+ {
+ try
+ {
+ await _http.PostAsync($"api/Localization/lang?language={Uri.EscapeDataString(language)}", content: null, ct);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to set language to {Lang}.", language);
+ }
+ }
+
+ ///
+ /// Returns the currently selected language code (e.g. "de", "en"), or
+ /// null if no language cookie has been set yet (the API answers
+ /// with HTTP 404 in that case).
+ ///
+ public async Task GetLanguageAsync(CancellationToken ct = default)
+ {
+ try
+ {
+ var res = await _http.GetAsync("api/Localization/lang", ct);
+ if (!res.IsSuccessStatusCode)
+ return null;
+ return (await res.Content.ReadAsStringAsync(ct))?.Trim('"');
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to read current language.");
+ return null;
+ }
+ }
+
+ // ?? Helpers ??????????????????????????????????????????????????????
+
+ private async Task GetAuthAsync(string url, CancellationToken ct)
+ {
+ var res = await _http.GetAsync(url, ct);
+ return await ReadAuthAsync(res, ct);
+ }
+
+ private async Task PostAuthAsync(string url, TReq body, CancellationToken ct)
+ {
+ var res = await _http.PostAsJsonAsync(url, body, ct);
+ return await ReadAuthAsync(res, ct);
+ }
+
+ private static async Task ReadAuthAsync(HttpResponseMessage res, CancellationToken ct)
+ {
+ // ReceiverAuthController returns a ReceiverAuthResponse body for both
+ // 2xx and known error statuses (401/404/500). We always try to deserialize.
+ try
+ {
+ return await res.Content.ReadFromJsonAsync(cancellationToken: ct);
+ }
+ catch
+ {
+ return new ReceiverAuthResponse
+ {
+ Status = ReceiverAuthStatus.Error,
+ ErrorMessage = $"Unexpected response ({(int)res.StatusCode})."
+ };
+ }
+ }
+}