Base URL

When running locally via Docker, the base URL is:

http://localhost:8080

In production, replace with your deployment URL. All endpoints are relative to this base URL.

ℹ️
See the Docker Guide for instructions on running the service and enabling authentication.

External resources

Every render endpoint loads the stylesheets, @imports, @font-face fonts and images the HTML references under one server-wide policy, since callers are untrusted: http(s) resources are fetched safely (loopback, private, link-local and cloud-metadata addresses are refused, every redirect is re-checked, responses are capped at 10 MB and 10 seconds), and files on the server are never read. One request may fetch at most 50 distinct resources within 20 seconds (then LIMIT_EXCEEDED), and fetches stop when the client disconnects. A refused file shows up as RESOURCE_BLOCKED in the X-EggPdf-Warnings header. POST /api/render/url fetches its page under the same policy and returns 400 when the URL is refused or unreachable. The policy is server configuration only, never a request field, so a caller can't loosen it; operators can change it with the EggPdf__Resources__* environment variables in the Docker Guide.

POST /api/render

Converts an HTML string to a PDF document. Returns the PDF as a binary response.

Request

PropertyTypeRequiredDescription
htmlstring✅ YesThe HTML document to render
optionsobjectNoPage size, margins, orientation, title/author, PDF/A conformance, Factur-X invoice, PDF/UA-1/UA-2 tagging (see below)

Response

HeaderValue
Content-Typeapplication/pdf
Content-Dispositioninline; filename="document.pdf"
X-EggPdf-Page-CountNumber of pages in the returned PDF, e.g. for per-page billing without parsing the PDF.
X-EggPdf-WarningsOnly when the render raised non-fatal warnings: CODE: message entries separated by | (ASCII, truncated to 1024 characters). For example FONT_GLYPH_MISSING when no installed font covers some characters, which would otherwise paint as empty boxes. See Per-character font fallback.

Minimal example

JSON Request Body
{
  "html": "<h1>Hello, EggPdf!</h1><p>Pure C# PDF rendering.</p>"
}

options mirrors PdfRenderOptions from the C# API — it's translated into an injected @page rule (and a <title>/<meta name="author"> tag for those two fields) internally, so it wins the CSS cascade over anything conflicting already in html. Running headers/footers still need CSS directly in the HTML — @page margin boxes such as @bottom-center with counter(page)/counter(pages) for page numbers; see Headers & Footers.

Example: page size, margins, and title via options

JSON Request Body
{
  "html": "<h1>Report</h1>",
  "options": {
    "pageSize": "A4",
    "marginTop": 60, "marginBottom": 60,
    "title": "Q4 Report"
  }
}

Example: page size, margins, and a page-number footer via CSS

JSON Request Body
{
  "html": "<html><head><style>@page{size:A4;margin:60px 50px;@bottom-center{content:'Page ' counter(page) ' of ' counter(pages)}}</style></head><body><h1>Report</h1></body></html>"
}

Example: PDF/A conformance and a Factur-X invoice attachment (MINIMUM profile)

JSON Request Body
{
  "html": "<h1>Invoice 2026-042</h1>",
  "options": {
    "conformance": "PdfA3b",
    "invoice": {
      "invoiceNumber": "2026-042",
      "sellerName": "Seller GmbH", "buyerName": "Buyer SARL",
      "taxBasisTotal": 100.00, "taxTotal": 20.00,
      "grandTotal": 120.00, "duePayableAmount": 120.00
    }
  }
}

Example: Factur-X invoice with line items (EN 16931 / Comfort profile)

JSON Request Body
{
  "html": "<h1>Invoice 2026-042</h1>",
  "options": {
    "conformance": "PdfA3b",
    "invoice": {
      "invoiceNumber": "2026-042",
      "sellerName": "Seller GmbH", "buyerName": "Buyer SARL",
      "lineItems": [ {
        "lineId": "1", "itemName": "Widget",
        "netUnitPrice": 10.00, "billedQuantity": 5,
        "lineTotalAmount": 50.00,
        "vatCategoryCode": "S", "vatRatePercent": 20
      } ]
    }
  }
}

conformance accepts PdfA1b, PdfA1a, PdfA2b, PdfA2u, PdfA2a, PdfA3b, PdfA3u, or PdfA3a (PdfA1b/PdfA1a return 400 if the document uses opacity, blend modes, or image alpha, since PDF/A-1 forbids transparency; the a levels return 400 unless tagged: true is also set — level A conformance requires accessibility tagging). invoice requires PdfA3b/PdfA3u (returns 400 otherwise) and embeds the CII invoice XML as a PDF/A-3 attachment. Omitted/empty lineItems produces the MINIMUM profile; a non-empty lineItems array produces EN 16931 (Comfort) instead, with the header tax breakdown and totals computed from the line items (any header total fields you also send are ignored in favor of the computed values). See PDF Compliance for details, including why BASIC/EXTENDED aren't modeled as separate profiles.

Example: PDF/UA-1 tagged PDF

JSON Request Body
{
  "html": "<h1>Title</h1><p>Body text</p>",
  "options": { "tagged": true }
}

Produces a structure tree (headings, paragraphs, tables, lists, landmark regions, figures with alt text, links cross-referenced to their annotation via OBJR) linked to the page content, plus /MarkInfo, /Lang and the required XMP identification. Combinable with conformance and with named page groups. See PDF Compliance for the full tag set and known limitations.

Example: PDF/UA-2 tagged PDF

JSON Request Body
{
  "html": "<h1>Title</h1><p>Body text</p>",
  "options": { "tagged": true, "uaVersion": "Ua2" }
}

uaVersion accepts "Ua1" (default) or "Ua2" (ISO 14289-2:2024, PDF 2.0-based — a %PDF-2.0 header, a declared PDF 2.0 structure namespace, and pdfuaid:rev). Returns 400 if combined with conformance — there is no defined joint PDF/A + PDF/UA-2 standard. See PDF Compliance for details.

GET /health

Returns the health status of the service. Use this for liveness probes in Kubernetes, ECS, or other orchestrators.

Response

HTTP/1.1 200 OK
Content-Type: application/json

{
  "status":  "Healthy",
  "version": "2.0.8"
}

POST /api/encrypt

Render HTML and encrypt the resulting PDF (RC4 128-bit) with owner/user passwords and permission flags. Permissions default to view-and-print without modification. Note: PDF permission flags are advisory — compliant viewers enforce them, but they are not cryptographic protection. For tamper evidence, use signing.

Request

{
  "html": "<h1>Confidential</h1>",
  "userPassword": "",            // empty = anyone can open
  "ownerPassword": "owner-secret",
  "allowPrinting": true,
  "allowCopying": false,
  "allowModifying": false
}

Response

200 OK with application/pdf body (the encrypted document).

POST /api/stamp

Place images (PNG, JPEG, GIF, BMP or WebP) onto pages of an existing PDF from any producer. Positions are in PDF points from the top-left corner of the page as a viewer shows it, so rotated pages and crop boxes are handled. The images are appended as an incremental update: the original bytes stay unchanged, and an earlier signature still verifies (a verifier will report that content was added after signing). Encrypted PDFs, PDFs whose cross-reference is damaged and certified PDFs (a DocMDP signature: no certification level permits new page content) are refused with 400, as are page numbers out of range, positions or sizes beyond 14,400 points, and unsupported images. Stamps are marked as artifacts, so a tagged PDF's structure doesn't include them, and an image used on several pages is embedded once. In .NET, Build(stream) writes the result to a stream and holds only the small update in memory.

Request

{
  "pdf": "<base64-encoded PDF>",
  "stamps": [
    { "page": 1, "x": 72, "y": 600, "width": 150, "height": 50,
      "image": "<base64-encoded PNG>", "fit": "contain" }
  ]
}

fit is "stretch" (default: fill the box) or "contain" (keep proportions, centred). In .NET use new PdfStamper(pdf).AddImage(page, image, x, y, width, height, PdfImageFit.Contain).Build(); page sizes as displayed come from PdfReader.Open(pdf).GetPage(n).

Response

200 OK with application/pdf body (the stamped document).

POST /api/sign

Add a digital-signature placeholder (adbe.pkcs7.detached) to an existing PDF. The endpoint does not hold private keys: compute the CMS signature with your certificate and embed it via the library (PdfSigner.SignWithCmsData), or sign fully in-process with PdfSigner.Sign(pdfBytes, x509Certificate), PdfSigner.Sign(pdfBytes, pfxBytes, password) or PdfSigner.SignWithPem(pdfBytes, certificatePem, privateKeyPem) when you control the key material. The service never accepts private keys or certificate passwords in a request.

Request

{
  "pdf": "<base64-encoded PDF>",
  "name": "Jane Smith",
  "reason": "Approval",
  "location": "Hanoi"
}

Response

200 OK with application/pdf body containing the signature placeholder.

curl Examples

Minimal — save to file

curl -X POST http://localhost:8080/api/render \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Hello</h1>"}' \
  --output hello.pdf

With page size, margins, and a footer (via CSS in the HTML)

curl -X POST http://localhost:8080/api/render \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<html><head><style>@page{size:Letter;margin:60px 50px;@bottom-center{content:\"Page \" counter(page) \" of \" counter(pages)}}</style></head><body><h1>Invoice #42</h1></body></html>"
  }' \
  --output invoice-42.pdf

From an HTML file

# Read HTML from file and send as JSON
HTML=$(cat report.html | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
curl -X POST http://localhost:8080/api/render \
  -H "Content-Type: application/json" \
  -d "{\"html\": $HTML}" \
  --output report.pdf

Health check

curl http://localhost:8080/health

Python Example

render_pdf.py
import requests

EGGPDF_URL = "http://localhost:8080"

def render_pdf(html: str) -> bytes:
    response = requests.post(
        f"{EGGPDF_URL}/api/render",
        json={"html": html},
        timeout=30,
    )
    response.raise_for_status()
    return response.content


# Basic usage
pdf_bytes = render_pdf("<h1>Hello from Python</h1>")
with open("output.pdf", "wb") as f:
    f.write(pdf_bytes)
print(f"PDF saved: {len(pdf_bytes):,} bytes")

# Page size / margins / footer are set via CSS inside the html string —
# see "Example: page size, margins, and a page-number footer via CSS" above.

Node.js Example

renderPdf.mjs
const EGGPDF_URL = 'http://localhost:8080';

async function renderPdf(html) {
  const response = await fetch(`${EGGPDF_URL}/api/render`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ html }),
  });

  if (!response.ok) {
    const text = await response.text();
    throw new Error(`EggPdf error ${response.status}: ${text}`);
  }

  return Buffer.from(await response.arrayBuffer());
}

// Basic usage
const pdf = await renderPdf('<h1>Hello from Node.js</h1>');
await fs.writeFile('output.pdf', pdf);
console.log(`PDF saved: ${pdf.length.toLocaleString()} bytes`);

// Return as HTTP response (Express example) — page size/title/footer set via
// @page and position:fixed CSS inside buildInvoiceHtml's returned string
app.get('/invoice/:id', async (req, res) => {
  const html = await buildInvoiceHtml(req.params.id);
  const pdf = await renderPdf(html);
  res.setHeader('Content-Type', 'application/pdf');
  res.send(pdf);
});

Error Responses

On failure, the API returns a JSON error body with an error field:

StatusMeaning
400 Bad RequestMissing or malformed request body (e.g. no html field)
401 UnauthorizedMissing or invalid X-Api-Key header when auth is enabled
500 Internal Server ErrorUnexpected rendering failure (includes error message in body)
// 400 example
{ "error": "The 'html' field is required." }