Base URL
When running locally via Docker, the base URL is:
http://localhost:8080In production, replace with your deployment URL. All endpoints are relative to this base URL.
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
| Property | Type | Required | Description |
|---|---|---|---|
html | string | ✅ Yes | The HTML document to render |
options | object | No | Page size, margins, orientation, title/author, PDF/A conformance, Factur-X invoice, PDF/UA-1/UA-2 tagging (see below) |
Response
| Header | Value |
|---|---|
Content-Type | application/pdf |
Content-Disposition | inline; filename="document.pdf" |
X-EggPdf-Page-Count | Number of pages in the returned PDF, e.g. for per-page billing without parsing the PDF. |
X-EggPdf-Warnings | Only 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
{
"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
{
"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
{
"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)
{
"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)
{
"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
{
"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
{
"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.pdfWith 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.pdfFrom 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.pdfHealth check
curl http://localhost:8080/healthPython Example
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
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:
| Status | Meaning |
|---|---|
400 Bad Request | Missing or malformed request body (e.g. no html field) |
401 Unauthorized | Missing or invalid X-Api-Key header when auth is enabled |
500 Internal Server Error | Unexpected rendering failure (includes error message in body) |
// 400 example
{ "error": "The 'html' field is required." }