diff --git a/docs/reverse-engineering/grpc-interface-versions.md b/docs/reverse-engineering/grpc-interface-versions.md
new file mode 100644
index 0000000..5bf1b89
--- /dev/null
+++ b/docs/reverse-engineering/grpc-interface-versions.md
@@ -0,0 +1,44 @@
+# 2023 R2 gRPC Interface-Version Integers (C3a)
+
+**Captured:** 2026-06-25
+**Transport:** 2023 R2 gRPC (h2c, unauthenticated `GetInterfaceVersion` RPCs — no credentials required)
+**Status:** LIVE — integers captured from a real AVEVA Historian 2023 R2 server.
+
+## Captured Values
+
+| Service | UiVersion / Version | UiError / Error | Notes |
+|---------------|---------------------|-----------------|----------------------------------------------------|
+| History | 12 | 0 | Matches `HistoryInterfaceVersionGrpc2023R2 = 12` |
+| Retrieval | 4 | 0 | Matches `RetrievalInterfaceVersion = 4` (unchanged from 2020) |
+| Transaction | 2 | 0 | Matches `TransactionInterfaceVersion = 2` (unchanged from 2020) |
+| Status | 4 | 0 | Reachability-only; version integer is not version-gated (see note) |
+
+> **Field-name note:** The History, Retrieval, and Status proto responses use `UiError`/`UiVersion` fields.
+> The Transaction response uses `Error`/`Version` (different naming convention in the proto). Both are
+> captured correctly; the table uses a unified column header for readability.
+
+> **Status note:** `StatusService.GetStatusInterfaceVersion` returned UiVersion=4, UiError=0 on the live
+> 2023 R2 server. Status is classified as reachability-only — its version integer carries no semantic
+> meaning for the SDK's byte serializers — so its UiVersion is not gated and not asserted in tests.
+
+## Evidence Test
+
+`tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs` —
+`GrpcInterfaceVersions_LiveServer_MatchAcceptedSet` reads these four RPCs live and asserts:
+- `history.UiError == 0` and `history.UiVersion ∈ {11, 12}`
+- `retrieval.UiError == 0` and `retrieval.UiVersion == 4`
+- `transaction.Error == 0` and `transaction.Version == 2`
+- `status.UiError == 0` (version not asserted)
+
+The test skips silently when `HISTORIAN_GRPC_HOST` is absent.
+
+## Gap Closed
+
+This document closes the **C3a** gap: "2023 R2 gRPC server-version integers not yet captured."
+Prior to this capture, the `HistorianServerVersionGate` accepted History=12, Retrieval=4, and
+Transaction=2 on the basis that they were inferred/expected-to-be-unchanged. All four integers are
+now confirmed from a live 2023 R2 server over the gRPC transport; no widening of `AcceptedVersions`
+is required (all captured values were already accepted).
+
+The 2020 WCF baseline (History=11, Retrieval=4, Transaction=2) was captured earlier via the
+`wcf-probe` command and is documented in `wcf-probe-remote-latest.json` and `wcf-contract-evidence.md`.
diff --git a/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs b/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs
index 96930be..cc69813 100644
--- a/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs
+++ b/src/AVEVA.Historian.Client/HistorianServerVersionGate.cs
@@ -48,8 +48,12 @@ internal static class HistorianServerVersionGate
/// The 2023 R2 gRPC HistoryService reports interface version 12. It is buffer-compatible with
/// the 2020 version 11 — the OpenConnection3 v6 / token / DataQueryRequest / row buffers are
/// byte-identical — confirmed by a live end-to-end gRPC read against a real 2023 R2 server
- /// (2026-06-21). So both 11 and 12 are accepted for History. (Retrieval reported 4, matching
- /// the 2020 value, so it needs no widening.)
+ /// (2026-06-21). So both 11 and 12 are accepted for History.
+ ///
+ /// Retrieval=4, Transaction=2, and Status UiError=0 are now confirmed captured live over the
+ /// 2023 R2 gRPC transport (2026-06-25, unauthenticated GetInterfaceVersion RPCs); see
+ /// docs/reverse-engineering/grpc-interface-versions.md. All captured values were already
+ /// accepted — no widening of was required.
///
public const uint HistoryInterfaceVersionGrpc2023R2 = 12;
diff --git a/tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs b/tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs
new file mode 100644
index 0000000..2d0d854
--- /dev/null
+++ b/tests/AVEVA.Historian.Client.Tests/GrpcInterfaceVersionEvidenceTests.cs
@@ -0,0 +1,102 @@
+using AVEVA.Historian.Client.Grpc;
+using GrpcHistory = ArchestrA.Grpc.Contract.History;
+using GrpcRetrieval = ArchestrA.Grpc.Contract.Retrieval;
+using GrpcStatus = ArchestrA.Grpc.Contract.Status;
+using GrpcTransaction = ArchestrA.Grpc.Contract.Transaction;
+using Xunit.Abstractions;
+
+namespace AVEVA.Historian.Client.Tests;
+
+///
+/// Live evidence test (C3a): reads the four unauthenticated GetInterfaceVersion RPCs from a
+/// real 2023 R2 Historian over gRPC and asserts the accepted version set. These RPCs run before any
+/// credential exchange, so no HISTORIAN_USER / HISTORIAN_PASSWORD is required — only a reachable host.
+///
+/// Skips silently when HISTORIAN_GRPC_HOST is absent (offline / CI). The captured integers
+/// are recorded in docs/reverse-engineering/grpc-interface-versions.md and close the C3a
+/// "2023 R2 gRPC server-version integers not yet captured" gap.
+///
+public sealed class GrpcInterfaceVersionEvidenceTests
+{
+ private readonly ITestOutputHelper _output;
+
+ public GrpcInterfaceVersionEvidenceTests(ITestOutputHelper output) => _output = output;
+
+ [Fact]
+ public void GrpcInterfaceVersions_LiveServer_MatchAcceptedSet()
+ {
+ string host = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_HOST") ?? "";
+ if (string.IsNullOrWhiteSpace(host))
+ {
+ _output.WriteLine("SKIP: HISTORIAN_GRPC_HOST is not set — no live historian available.");
+ return;
+ }
+
+ HistorianClientOptions options = BuildOptions(host);
+
+ using HistorianGrpcConnection connection = HistorianGrpcChannelFactory.Create(options);
+ DateTime deadline = DateTime.UtcNow.Add(TimeSpan.FromSeconds(10));
+
+ GrpcHistory.GetInterfaceVersionResponse history =
+ new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel)
+ .GetInterfaceVersion(new GrpcHistory.GetInterfaceVersionRequest(), connection.Metadata, deadline, default);
+
+ GrpcRetrieval.GetRetrievalInterfaceVersionResponse retrieval =
+ new GrpcRetrieval.RetrievalService.RetrievalServiceClient(connection.Channel)
+ .GetRetrievalInterfaceVersion(new GrpcRetrieval.GetRetrievalInterfaceVersionRequest(), connection.Metadata, deadline, default);
+
+ GrpcStatus.GetStatusInterfaceVersionResponse status =
+ new GrpcStatus.StatusService.StatusServiceClient(connection.Channel)
+ .GetStatusInterfaceVersion(new GrpcStatus.GetStatusInterfaceVersionRequest(), connection.Metadata, deadline, default);
+
+ GrpcTransaction.GetTransactionInterfaceVersionResponse transaction =
+ new GrpcTransaction.TransactionService.TransactionServiceClient(connection.Channel)
+ .GetTransactionInterfaceVersion(new GrpcTransaction.GetTransactionInterfaceVersionRequest(), connection.Metadata, deadline, default);
+
+ _output.WriteLine($"History UiVersion={history.UiVersion} UiError={history.UiError}");
+ _output.WriteLine($"Retrieval UiVersion={retrieval.UiVersion} UiError={retrieval.UiError}");
+ _output.WriteLine($"Status UiVersion={status.UiVersion} UiError={status.UiError}");
+ // Note: Transaction response fields are named Error/Version (not UiError/UiVersion) per the proto.
+ _output.WriteLine($"Transaction Version={transaction.Version} Error={transaction.Error}");
+
+ // History: accepted set is {11 (2020 WCF), 12 (2023 R2 gRPC)}.
+ Assert.Equal(0u, history.UiError);
+ Assert.Contains(history.UiVersion, new uint[] { 11u, 12u });
+
+ // Retrieval: 4 on both 2020 WCF and 2023 R2 gRPC.
+ Assert.Equal(0u, retrieval.UiError);
+ Assert.Equal(4u, retrieval.UiVersion);
+
+ // Transaction: 2 (confirmed by live gRPC capture — see grpc-interface-versions.md).
+ // NOTE: the Transaction response proto uses Error/Version (not UiError/UiVersion).
+ Assert.Equal(0u, transaction.Error);
+ Assert.Equal(2u, transaction.Version);
+
+ // Status: reachability-only — UiError must be 0; UiVersion is intentionally 0 by design.
+ Assert.Equal(0u, status.UiError);
+ }
+
+ private static HistorianClientOptions BuildOptions(string host)
+ {
+ string? user = Environment.GetEnvironmentVariable("HISTORIAN_USER");
+ string? password = Environment.GetEnvironmentVariable("HISTORIAN_PASSWORD");
+ bool explicitCreds = !string.IsNullOrEmpty(user);
+ int port = int.TryParse(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_PORT"), out int parsed)
+ ? parsed
+ : HistorianClientOptions.DefaultGrpcPort;
+ bool tls = string.Equals(Environment.GetEnvironmentVariable("HISTORIAN_GRPC_TLS"), "true", StringComparison.OrdinalIgnoreCase);
+
+ return new HistorianClientOptions
+ {
+ Host = host,
+ Port = port,
+ Transport = HistorianTransport.RemoteGrpc,
+ GrpcUseTls = tls,
+ AllowUntrustedServerCertificate = tls,
+ ServerDnsIdentity = Environment.GetEnvironmentVariable("HISTORIAN_GRPC_DNSID"),
+ IntegratedSecurity = !explicitCreds,
+ UserName = user ?? string.Empty,
+ Password = password ?? string.Empty,
+ };
+ }
+}
diff --git a/tests/AVEVA.Historian.Client.Tests/HistorianServerVersionGateTests.cs b/tests/AVEVA.Historian.Client.Tests/HistorianServerVersionGateTests.cs
index cbc7539..61051ae 100644
--- a/tests/AVEVA.Historian.Client.Tests/HistorianServerVersionGateTests.cs
+++ b/tests/AVEVA.Historian.Client.Tests/HistorianServerVersionGateTests.cs
@@ -71,8 +71,9 @@ public sealed class HistorianServerVersionGateTests
[Fact]
public void Validate_VerificationDisabled_NeverThrows()
{
- // A wildly wrong version is tolerated when the operator opts out (e.g. bringing up a
- // 2023 R2 gRPC server whose reported integers have not yet been captured).
+ // A wildly wrong version is tolerated when the operator opts out. The 2023 R2 gRPC
+ // integers are now captured live (see docs/reverse-engineering/grpc-interface-versions.md)
+ // and all accepted; this opt-out is a safety valve for some future, not-yet-captured value.
HistorianServerVersionGate.Validate(HistorianServiceInterface.History, 999u, Options(verify: false));
HistorianServerVersionGate.Validate(HistorianServiceInterface.Retrieval, 0u, Options(verify: false));
}