diff --git a/DocumentOperator.API/Controllers/PdfOperationsController.cs b/DocumentOperator.API/Controllers/PdfOperationsController.cs
new file mode 100644
index 0000000..663572a
--- /dev/null
+++ b/DocumentOperator.API/Controllers/PdfOperationsController.cs
@@ -0,0 +1,120 @@
+using DocumentOperator.Application.MergePdfs;
+using DocumentOperator.Domain.Common.Exceptions;
+using MediatR;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace DocumentOperator.API.Controllers;
+
+///
+/// Controller for PDF operations (merge, stamp, annotate).
+///
+[ApiController]
+[Route("api/pdf/operations")]
+public class PdfOperationsController(IMediator mediator) : ControllerBase
+{
+ ///
+ /// Merges multiple PDF files into a single PDF.
+ /// Supports multipart/form-data file upload.
+ ///
+ /// PDF files to merge (minimum 2 required)
+ /// Cancellation token
+ /// Merged PDF file
+ /// PDFs merged successfully - returns merged PDF
+ /// Invalid input (fewer than 2 files, corrupted PDF)
+ /// Internal server error during PDF processing
+ [HttpPost("merge")]
+ [Consumes("multipart/form-data")]
+ [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
+ public async Task MergeFromFiles(
+ [FromForm] List files,
+ CancellationToken cancellationToken)
+ {
+ // Convert IFormFile[] to Stream[] (use OpenReadStream directly - no buffering)
+ var streams = files.Select(f => f.OpenReadStream()).ToList();
+
+ // Send command to MediatR (no page ranges for now - multipart binding is complex)
+ var command = new MergePdfsCommand
+ {
+ PdfStreams = streams,
+ PageRanges = null
+ };
+
+ byte[] mergedPdf = await mediator.Send(command, cancellationToken);
+
+ // Return merged PDF
+ return File(mergedPdf, "application/pdf", "merged.pdf");
+ }
+
+ ///
+ /// Merges multiple PDF files into a single PDF.
+ /// Supports Base64-encoded PDFs via JSON payload.
+ ///
+ /// Request containing Base64-encoded PDFs and optional page ranges
+ /// Cancellation token
+ /// Merged PDF file
+ /// PDFs merged successfully - returns merged PDF
+ /// Invalid input (Base64 format error, fewer than 2 files, corrupted PDF, invalid page range)
+ /// Internal server error during PDF processing
+ [HttpPost("merge")]
+ [Consumes("application/json")]
+ [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
+ public async Task MergeFromBase64(
+ [FromBody] MergePdfsRequest request,
+ CancellationToken cancellationToken)
+ {
+ // Convert Base64[] to MemoryStream[]
+ List streams = new();
+ try
+ {
+ foreach (var base64Pdf in request.Base64Pdfs)
+ {
+ byte[] pdfBytes = Convert.FromBase64String(base64Pdf);
+ streams.Add(new MemoryStream(pdfBytes));
+ }
+ }
+ catch (FormatException ex)
+ {
+ // Dispose opened streams on error
+ foreach (var stream in streams) stream.Dispose();
+ throw new BadRequestException("Invalid Base64 format: " + ex.Message);
+ }
+
+ var command = new MergePdfsCommand
+ {
+ PdfStreams = streams,
+ PageRanges = request.PageRanges
+ };
+
+ byte[] mergedPdf = await mediator.Send(command, cancellationToken);
+
+ // Cleanup streams (important for MemoryStreams we created)
+ foreach (var stream in streams) stream.Dispose();
+
+ return File(mergedPdf, "application/pdf", "merged.pdf");
+ }
+}
+
+///
+/// Request DTO for Base64-encoded PDF merge operation
+///
+public record MergePdfsRequest
+{
+ ///
+ /// Array of Base64-encoded PDF files (minimum 2 required)
+ ///
+ /// ["JVBERi0xLjQK...", "JVBERi0xLjQK..."]
+ public List Base64Pdfs { get; init; } = new();
+
+ ///
+ /// Optional page ranges per PDF (null = all pages).
+ /// Format: "1-3,5" means pages 1, 2, 3, and 5.
+ /// If provided, array length must match Base64Pdfs length.
+ ///
+ /// ["1-2", "1,3,5", null]
+ public List? PageRanges { get; init; }
+}