← All docs

Server

DynDyn.Pdf.Web is a self-hosted HTTP job API: upload a PDF, get a job id back, watch progress over Server-Sent Events, then download or preview the converted HTML. It runs the same conversion engine as the CLI and the library, so output is identical — the server is a thin, stateful job wrapper around IPdfToHtmlConverter.

dotnet run --project src/DynDyn.Pdf.Web
# or, published:
./DynDyn.Pdf.Web --urls http://0.0.0.0:8080

A bundled upload page ships at / (static files from wwwroot), including a live drag-and-drop demo that can also run the same conversion entirely in the browser — see wasm.md.

Job lifecycle

POST   /api/jobs                          (multipart: file + options) ──▶ { jobId, token }
GET    /api/jobs/{id}/events?token=…       (SSE)  ──▶ progress… → done | error
DELETE /api/jobs/{id}?token=…              cancel an in-flight/queued job
GET    /api/jobs/{id}/download?token=…     the HTML (or a .zip for per-page output)
GET    /api/jobs/{id}/preview?token=…      inline HTML preview (single/viewer only)
GET    /health                             liveness, never gated by auth
GET    /ready                              readiness + capability probe, never gated by auth

A job is created, run on a background task, and its progress/terminal state is streamed over SSE. Results live on the instance's local disk keyed by jobId — an in-process registry, no persistence across restarts, no cross-instance sharing (see Scaling in deployment.md).

Every job is ownership-tokened. POST /api/jobs returns both a jobId and a token; every other job route requires that same token (as a query parameter — events, download, preview and the cancelling DELETE all check it) and responds 403 if it's missing or wrong. This is what lets a public deployment serve concurrent anonymous uploads safely: knowing a jobId alone is not enough to read or cancel someone else's job.

POST /api/jobs — create a job

Content-Type: multipart/form-data. Fields (all but file optional, with the shown default):

Field Default Values
file — (required) the PDF (binary)
engine managed managed, native
format viewer single, pages, viewer
rasterFallback auto never, auto, always
background svg svg, raster, none
dpi 120 integer, clamped 18–600 server-side
pages all e.g. 1-5,8,10-
lineGrouping always always, auto, never
semanticTags off true/on/1
detectDecorative off true/on/1
extractStyles off true/on/1
jpegQuality 85 1–100
rawTtf off true/on/1
password none user password for an encrypted PDF (managed engine only)
curl -sS -X POST http://localhost:8080/api/jobs \
  -F file=@report.pdf \
  -F format=viewer -F engine=managed -F dpi=150 -F pages=1-20
# {"jobId":"3f2c…","token":"…"}

Responses:

`GET /api/jobs/

text/event-stream, Cache-Control: no-cache. Each data: line is a JSON object; the stream ends after a terminal done or error. 404 for an unknown job, 403 for a wrong/missing token. If the client disconnects before the job finishes, the job is cancelled server-side rather than left to run out its slot.

{ "type": "progress", "stage": "extract", "total": 20, "extracted": 7, "composed": 3, "fraction": 0.42 }
{ "type": "done", "engine": "managed", "pages": 20, "warnings": 0, "elapsed": 4.2,
  "downloadUrl": "/api/jobs/3f2c…/download", "previewUrl": "/api/jobs/3f2c…/preview",
  "downloadName": "report.html" }
{ "type": "error", "message": "…" }

engine in the done event is the engine the job was configured to run — not an independent verification — because the server never silently substitutes a different engine; a request for native fails outright if pdfium isn't loaded rather than quietly running managed. A password-protected PDF with no (or the wrong) password fails with a distinct { "type": "error", "code": "password_required", "message": "…" } so a client can prompt rather than show a generic failure.

`DELETE /api/jobs/

Cancels an in-flight or still-queued job and frees its concurrency slot immediately, instead of letting it run to completion or its timeout. 404 unknown job, 403 wrong/missing token, 200 { "cancelled": true } on success.

`GET /api/jobs/

download returns the converted file with Content-Disposition set to the original document name: single/viewer → text/html; charset=utf-8; pages → application/zip (the whole per-page folder). preview serves single/viewer HTML inline for an <iframe>; 404 for the pages format. Both 404 for an unknown/unfinished job and 403 for a wrong/missing token.

GET /health and GET /ready

Never gated by auth — the only unguarded routes besides static assets.

Authentication

/api/* is gated by API-key middleware, controlled by DynDynPdf:ApiKeys and DynDynPdf:AllowAnonymous:

/api/* also carries a per-key / per-IP fixed-window rate limit (DynDynPdf:RateLimitRequestsPerMinute, default 120/min).

Deployment settings

The Web service reads standard ASP.NET Core host configuration plus its own DynDynPdf section (appsettings.json, DynDynPdf__Setting env vars, or the command line):

Setting Default
ASPNETCORE_URLS / --urls http://localhost:5000
ASPNETCORE_ENVIRONMENT Production
DynDynPdf:ApiKeys empty (loopback-only unless AllowAnonymous)
DynDynPdf:AllowAnonymous false
DynDynPdf:MaxConcurrentConversions 0 (auto: derived from cores + available memory)
DynDynPdf:MaxLiveJobs 200 — new uploads past this get 503
DynDynPdf:JobTtlMinutes 30 — idle job (and its temp dir) eviction
DynDynPdf:CleanupIntervalSeconds 60
DynDynPdf:RateLimitRequestsPerMinute 120
DynDynPdf:MaxUploadBytes 1 GB
DynDynPdf:ConversionTimeoutSeconds 600 (0/negative disables)
DynDynPdf:MaxPagesPerJob 0 (unlimited)
DYNDYN_PDF_NATIVE_LIB unset — points at a pinned/patched native rendering library

Full field-by-field reference: configuration.md and deployment.md.

Container image

No Dockerfile ships in this repository for the Web project. deployment.md includes a sample Dockerfile you can copy into your own build — the native rendering library arrives via NuGet under runtimes/<rid>/native, so the runtime image needs no extra OS packages beyond the stock mcr.microsoft.com/dotnet/aspnet:10.0 (or its -alpine/musl variant) base.

Operational notes


Related: cli.md for scripting/batch use of the same engine without a server, library.md for driving it in-process, and wasm.md for a zero-upload alternative that runs entirely in the caller's browser.