diff --git a/src/ScadaLink.AuditLog/Payload/DefaultAuditPayloadFilter.cs b/src/ScadaLink.AuditLog/Payload/DefaultAuditPayloadFilter.cs
index 78328b1..0d22447 100644
--- a/src/ScadaLink.AuditLog/Payload/DefaultAuditPayloadFilter.cs
+++ b/src/ScadaLink.AuditLog/Payload/DefaultAuditPayloadFilter.cs
@@ -118,7 +118,14 @@ public sealed class DefaultAuditPayloadFilter : IAuditPayloadFilter
try
{
var opts = _options.CurrentValue;
- var cap = IsErrorStatus(rawEvent.Status) ? opts.ErrorCapBytes : opts.DefaultCapBytes;
+ // Inbound API gets a dedicated, larger ceiling — request/response bodies are
+ // captured verbatim up to InboundMaxBytes (default 1 MiB) so support can
+ // replay exactly what the caller sent and what we returned. Other channels
+ // keep the global 8 KiB / 64 KiB policy.
+ // See docs/plans/2026-05-23-inbound-api-full-response-audit-design.md.
+ var cap = rawEvent.Channel == AuditChannel.ApiInbound
+ ? opts.InboundMaxBytes
+ : (IsErrorStatus(rawEvent.Status) ? opts.ErrorCapBytes : opts.DefaultCapBytes);
// --- Header-redaction stage (runs BEFORE truncation) ----------
var request = RedactHeaders(rawEvent.RequestSummary, opts.HeaderRedactList);
diff --git a/tests/ScadaLink.AuditLog.Tests/Payload/InboundChannelCapTests.cs b/tests/ScadaLink.AuditLog.Tests/Payload/InboundChannelCapTests.cs
new file mode 100644
index 0000000..42f5cf8
--- /dev/null
+++ b/tests/ScadaLink.AuditLog.Tests/Payload/InboundChannelCapTests.cs
@@ -0,0 +1,133 @@
+using System.Text;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using ScadaLink.AuditLog.Configuration;
+using ScadaLink.AuditLog.Payload;
+using ScadaLink.Commons.Entities.Audit;
+using ScadaLink.Commons.Types.Enums;
+
+namespace ScadaLink.AuditLog.Tests.Payload;
+
+///
+/// Pins the docs/plans/2026-05-23-inbound-api-full-response-audit-design.md
+/// inbound carve-out: ApiInbound rows use InboundMaxBytes (default 1 MiB) for
+/// RequestSummary / ResponseSummary truncation, NOT DefaultCapBytes /
+/// ErrorCapBytes. Other channels keep the existing caps.
+///
+///
+/// Uses a file-local helper mirroring the
+/// convention in the sibling Payload tests (TruncationTests,
+/// FilterIntegrationTests, BodyRegexRedactionTests, etc.) — the
+/// TestOptionsMonitor<T> helper referenced by the plan is a
+/// private nested class inside AuditLogOptionsBindingTests and thus
+/// not reachable from this file.
+///
+public class InboundChannelCapTests
+{
+ private static AuditEvent MakeInbound(
+ AuditStatus status,
+ string? request = null,
+ string? response = null) =>
+ new()
+ {
+ EventId = Guid.NewGuid(),
+ OccurredAtUtc = DateTime.UtcNow,
+ Channel = AuditChannel.ApiInbound,
+ Kind = AuditKind.InboundRequest,
+ Status = status,
+ RequestSummary = request,
+ ResponseSummary = response,
+ };
+
+ [Fact]
+ public void ApiInbound_Delivered_RequestBody_BelowInboundMaxBytes_NotTruncated()
+ {
+ // Body well above the legacy 8 KiB default cap but under the 1 MiB
+ // inbound ceiling — must NOT truncate.
+ var body = new string('a', 100_000);
+ var opts = new AuditLogOptions(); // defaults
+ var filter = new DefaultAuditPayloadFilter(
+ new StaticMonitor(opts),
+ NullLogger.Instance);
+
+ var result = filter.Apply(MakeInbound(AuditStatus.Delivered, request: body));
+
+ Assert.False(result.PayloadTruncated);
+ Assert.Equal(100_000, Encoding.UTF8.GetByteCount(result.RequestSummary!));
+ }
+
+ [Fact]
+ public void ApiInbound_Delivered_ResponseBody_BelowInboundMaxBytes_NotTruncated()
+ {
+ var body = new string('a', 100_000);
+ var opts = new AuditLogOptions();
+ var filter = new DefaultAuditPayloadFilter(
+ new StaticMonitor(opts),
+ NullLogger.Instance);
+
+ var result = filter.Apply(MakeInbound(AuditStatus.Delivered, response: body));
+
+ Assert.False(result.PayloadTruncated);
+ Assert.Equal(100_000, Encoding.UTF8.GetByteCount(result.ResponseSummary!));
+ }
+
+ [Fact]
+ public void ApiInbound_Failed_BodyAboveInboundMaxBytes_TruncatedToInboundMaxBytes()
+ {
+ // Even on error rows, the inbound cap is InboundMaxBytes (NOT ErrorCapBytes).
+ var opts = new AuditLogOptions { InboundMaxBytes = 16_384 };
+ var oversized = new string('z', 50_000);
+ var filter = new DefaultAuditPayloadFilter(
+ new StaticMonitor(opts),
+ NullLogger.Instance);
+
+ var result = filter.Apply(MakeInbound(AuditStatus.Failed, response: oversized));
+
+ Assert.True(result.PayloadTruncated);
+ Assert.True(Encoding.UTF8.GetByteCount(result.ResponseSummary!) <= 16_384);
+ }
+
+ [Fact]
+ public void ApiOutbound_StillUsesDefaultCap_NotInboundMaxBytes()
+ {
+ // Regression guard: lifting the inbound cap MUST NOT change other
+ // channels. An ApiOutbound 100 KB body still hits the 8 KiB cap.
+ var opts = new AuditLogOptions();
+ var body = new string('a', 100_000);
+ var filter = new DefaultAuditPayloadFilter(
+ new StaticMonitor(opts),
+ NullLogger.Instance);
+
+ var evt = new AuditEvent
+ {
+ EventId = Guid.NewGuid(),
+ OccurredAtUtc = DateTime.UtcNow,
+ Channel = AuditChannel.ApiOutbound,
+ Kind = AuditKind.ApiCall,
+ Status = AuditStatus.Delivered,
+ RequestSummary = body,
+ };
+ var result = filter.Apply(evt);
+
+ Assert.True(result.PayloadTruncated);
+ Assert.True(Encoding.UTF8.GetByteCount(result.RequestSummary!) <= opts.DefaultCapBytes);
+ }
+
+ ///
+ /// IOptionsMonitor test double — returns the same snapshot on every read,
+ /// no change-token plumbing required for these tests. Mirrors the helper
+ /// used in TruncationTests, FilterIntegrationTests, etc.
+ ///
+ private sealed class StaticMonitor : IOptionsMonitor
+ {
+ private readonly AuditLogOptions _value;
+
+ public StaticMonitor(AuditLogOptions value) => _value = value;
+
+ public AuditLogOptions CurrentValue => _value;
+
+ public AuditLogOptions Get(string? name) => _value;
+
+ public IDisposable? OnChange(Action listener) => null;
+ }
+}