diff --git a/src/PackageUploader.IntegrationTest/FakeApi/FakeApiControllerBase.cs b/src/PackageUploader.IntegrationTest/FakeApi/FakeApiControllerBase.cs
new file mode 100644
index 00000000..3677bd4b
--- /dev/null
+++ b/src/PackageUploader.IntegrationTest/FakeApi/FakeApiControllerBase.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using Microsoft.AspNetCore.Mvc;
+using PackageUploader.IntegrationTest.FakeApi;
+
+namespace PackageUploader.IntegrationTest.FakeApi;
+
+///
+/// Base for the fake-API controllers: records the incoming request into the given store and returns
+/// the scripted response (status code, with an optional JSON body).
+///
+public abstract class FakeApiControllerBase : ControllerBase
+{
+ private protected async Task RespondAsync(ScenarioStore store)
+ {
+ string? body = null;
+ if (Request.ContentLength is > 0)
+ {
+ // Only decode textual bodies; binary payloads (e.g. XFUS octet-stream block uploads)
+ // would be corrupted by a UTF-8 text read, so record their size instead.
+ var contentType = Request.ContentType ?? string.Empty;
+ if (contentType.Contains("json", StringComparison.OrdinalIgnoreCase) ||
+ contentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
+ {
+ using var reader = new StreamReader(Request.Body, leaveOpen: true);
+ body = await reader.ReadToEndAsync();
+ }
+ else
+ {
+ body = $"<{Request.ContentLength} binary bytes>";
+ }
+ }
+
+ var headers = Request.Headers.ToDictionary(
+ h => h.Key,
+ h => string.Join(", ", h.Value.ToArray()),
+ StringComparer.OrdinalIgnoreCase);
+
+ var path = Request.Path.Value ?? string.Empty;
+ store.Record(Request.Method, path, headers, body);
+
+ var response = store.Resolve(Request.Method, path);
+ if (response.Body is null)
+ {
+ return StatusCode(response.StatusCode);
+ }
+
+ return new ContentResult
+ {
+ StatusCode = response.StatusCode,
+ ContentType = "application/json",
+ Content = JsonSerializer.Serialize(response.Body),
+ };
+ }
+}
diff --git a/src/PackageUploader.IntegrationTest/FakeApi/IngestionController.cs b/src/PackageUploader.IntegrationTest/FakeApi/IngestionController.cs
new file mode 100644
index 00000000..aec9db89
--- /dev/null
+++ b/src/PackageUploader.IntegrationTest/FakeApi/IngestionController.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.AspNetCore.Mvc;
+
+namespace PackageUploader.IntegrationTest.FakeApi;
+
+///
+/// Fake Partner Center Ingestion API. Presents the products/... route space the client calls
+/// and delegates each request to the configured .
+///
+public sealed class IngestionController(IngestionScenarioStore store) : FakeApiControllerBase
+{
+ [AcceptVerbs("GET", "POST", "PUT", Route = "products/{**rest}")]
+ public Task Handle() => RespondAsync(store);
+}
diff --git a/src/PackageUploader.IntegrationTest/FakeApi/IngestionScenarioStore.cs b/src/PackageUploader.IntegrationTest/FakeApi/IngestionScenarioStore.cs
new file mode 100644
index 00000000..7125cc03
--- /dev/null
+++ b/src/PackageUploader.IntegrationTest/FakeApi/IngestionScenarioStore.cs
@@ -0,0 +1,201 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Net;
+using System.Net.Http;
+using PackageUploader.IntegrationTest.Infrastructure.Mocks;
+
+namespace PackageUploader.IntegrationTest.FakeApi;
+
+///
+/// Configurable scripted responses for the fake Ingestion API, consumed by .
+/// Exposes the same fluent stub surface as the other Task 2 prototypes.
+///
+public sealed class IngestionScenarioStore : ScenarioStore
+{
+ public IngestionScenarioStore StubGetProduct(string productId, ResponseScenario scenario = ResponseScenario.Success)
+ {
+ if (scenario != ResponseScenario.Success)
+ {
+ On(HttpMethod.Get, $"/products/{productId}", () => new FakeResponse((int)StatusFor(scenario)));
+ return this;
+ }
+
+ On(HttpMethod.Get, $"/products/{productId}", () => new FakeResponse(200, new
+ {
+ resourceType = "AzureGameProduct",
+ name = $"Test Product {productId}",
+ id = productId,
+ externalIds = new[] { new { type = "StoreId", value = "9TESTBIGID000" } },
+ isModularPublishing = true,
+ }));
+ return this;
+ }
+
+ public IngestionScenarioStore StubGetPackageBranches(
+ string productId,
+ params (string FriendlyName, string CurrentDraftInstanceId)[] branches)
+ {
+ var values = branches.Select(b => new
+ {
+ resourceType = "Branch",
+ friendlyName = b.FriendlyName,
+ type = "Main",
+ module = "Package",
+ currentDraftInstanceId = b.CurrentDraftInstanceId,
+ }).ToArray();
+
+ On(HttpMethod.Get, $"/products/{productId}/branches/getByModule*", () => new FakeResponse(200, new { value = values }));
+ On(HttpMethod.Get, $"/products/{productId}/flights", () => new FakeResponse(200, new { value = Array.Empty