HTML to PDF
Send an index.html and the files it uses; get a PDF back. The route for invoices, reports, tickets, certificates and anything else you can lay out in HTML and CSS.
POST https://api.pdfdispatch.com/v1/convert/html
Form fields
| Field | Type | Default | What it does |
|---|---|---|---|
index.html required | file | – | The page to print. The file must be named index.html; the name of the form part does not matter (we use files). |
any other file | file | – | Images, stylesheets, fonts and scripts that index.html uses. Refer to them by file name only (src="logo.png", not a path). Every file sent is available, whatever its form part’s name, except the special names in Files and results. |
header.html, footer.html | file | – | Repeated at the top and bottom of every page. See Header and footer. |
The options most requests need:
| Field | Type | Default | What it does |
|---|---|---|---|
paperWidth, paperHeight | size | 8.5 × 11 (US Letter) | The paper size. A number is inches; add a unit for others: 210mm, 29.7cm, 595pt, 794px, 50pc. A4 is 210mm × 297mm. |
marginTop, marginBottom, marginLeft, marginRight | size | 0.39 (1 cm) | The page margins, in the same units. A header or footer is drawn inside the top or bottom margin. |
landscape | boolean | false | Turn the paper sideways. |
printBackground | boolean | false | Print background colours and images. Without it, most backgrounds are left out, as a browser does when it prints. |
preferCssPageSize | boolean | false | Use the size from the page’s CSS @page rule instead of paperWidth and paperHeight. |
waitDelay | duration | – | Wait this long after the page has loaded before printing, for example 500ms or 2s. See Waiting for better ways. |
emulatedMediaType | string | print | print or screen: which CSS media rules apply. |
Many more (scale, page ranges, waiting for scripts, cookies, failing on broken resources, tagged PDF) are on Rendering options. The PDF can also be turned into PDF/A, encrypted, watermarked, split and more in the same request: see options on conversion routes.
Headers
| Header | Type | Default | What it does |
|---|---|---|---|
Authorization required | request header | – | Your key: Bearer pdd_..., or Basic auth with the key as the password. See Authentication. |
Output-Filename | request header | – | The name of the file you get back, without the extension (invoice-1042 gives invoice-1042.pdf, or .zip when there are several files). Without it, the name is random. |
Webhook-Url | request header | – | Send the result to this URL instead of in the answer. See Webhooks for the other Webhook-* headers. |
X-Request-Id | answer header | a UUID | The id of this request, on every answer (errors too). Quote it when you write to support. You cannot choose it; one you send is replaced. |
Retry-After | answer header | – | Only with code 42910, the per-minute limit. The seconds to wait before you send again. |
Examples
index.html links its stylesheet by file name, <link rel="stylesheet" href="style.css">, and both files are sent
in the same request. The result is an A4 PDF named invoice-1042.pdf.
Bash (curl)
# index.html and style.css are in the current folder; the key is in PDD_KEY.
# On an error curl exits with a non-zero status, and the output file holds the error message instead.
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer $PDD_KEY" \
--header "Output-Filename: invoice-1042" \
--form files=@index.html \
--form files=@style.css \
--form paperWidth=210mm \
--form paperHeight=297mm \
--form printBackground=true \
--output invoice-1042.pdf \
https://api.pdfdispatch.com/v1/convert/htmlNode.js
// node html-to-pdf.mjs (index.html and style.css next to it; the key in PDD_KEY)
import { openAsBlob } from "node:fs";
import { writeFile } from "node:fs/promises";
const form = new FormData();
form.append("files", await openAsBlob("index.html"), "index.html");
form.append("files", await openAsBlob("style.css"), "style.css");
form.append("paperWidth", "210mm");
form.append("paperHeight", "297mm");
form.append("printBackground", "true");
const res = await fetch("https://api.pdfdispatch.com/v1/convert/html", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDD_KEY}`,
"Output-Filename": "invoice-1042",
},
body: form,
});
if (!res.ok) {
// Errors from the gateway are JSON with a "code"; errors of the conversion are plain text.
console.error(res.status, res.headers.get("x-request-id"), await res.text());
process.exit(1);
}
await writeFile("invoice-1042.pdf", Buffer.from(await res.arrayBuffer()));fetch, FormData and fs.openAsBlob). No packages needed.Bun
// bun html-to-pdf.ts (index.html and style.css next to it; the key in PDD_KEY)
const form = new FormData();
form.append("files", Bun.file("index.html"), "index.html");
form.append("files", Bun.file("style.css"), "style.css");
form.append("paperWidth", "210mm");
form.append("paperHeight", "297mm");
form.append("printBackground", "true");
const res = await fetch("https://api.pdfdispatch.com/v1/convert/html", {
method: "POST",
headers: {
Authorization: `Bearer ${Bun.env.PDD_KEY}`,
"Output-Filename": "invoice-1042",
},
body: form,
});
if (!res.ok) {
console.error(res.status, res.headers.get("x-request-id"), await res.text());
process.exit(1);
}
await Bun.write("invoice-1042.pdf", res);Python
# python html_to_pdf.py (index.html and style.css next to it; the key in PDD_KEY)
import os
import sys
import requests
with open("index.html", "rb") as html, open("style.css", "rb") as css:
res = requests.post(
"https://api.pdfdispatch.com/v1/convert/html",
headers={
"Authorization": f"Bearer {os.environ['PDD_KEY']}",
"Output-Filename": "invoice-1042",
},
files=[
("files", ("index.html", html, "text/html")),
("files", ("style.css", css, "text/css")),
],
data={"paperWidth": "210mm", "paperHeight": "297mm", "printBackground": "true"},
timeout=70,
)
if not res.ok:
print(res.status_code, res.headers.get("X-Request-Id"), res.text, file=sys.stderr)
sys.exit(1)
with open("invoice-1042.pdf", "wb") as out:
out.write(res.content)pip install requests).PHP
<?php
// php html-to-pdf.php (index.html and style.css next to it; the key in PDD_KEY)
$curl = curl_init('https://api.pdfdispatch.com/v1/convert/html');
$out = fopen('invoice-1042.pdf', 'wb');
curl_setopt_array($curl, [
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PDD_KEY'),
'Output-Filename: invoice-1042',
],
// PHP needs a different key per file; the API does not mind what the file parts are called.
CURLOPT_POSTFIELDS => [
'files[0]' => new CURLFile('index.html', 'text/html', 'index.html'),
'files[1]' => new CURLFile('style.css', 'text/css', 'style.css'),
'paperWidth' => '210mm',
'paperHeight' => '297mm',
'printBackground' => 'true',
],
CURLOPT_FILE => $out,
CURLOPT_TIMEOUT => 70,
]);
$ok = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
fclose($out);
if (! $ok || $status !== 200) {
// The error body was written to the file; show it and remove the file.
fwrite(STDERR, "PDFDispatch $status: " . curl_error($curl) . file_get_contents('invoice-1042.pdf') . "\n");
unlink('invoice-1042.pdf');
exit(1);
}Go
// go run . (index.html and style.css next to it; the key in PDD_KEY)
package main
import (
"bytes"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"time"
)
func main() {
var body bytes.Buffer
form := multipart.NewWriter(&body)
for _, name := range []string{"index.html", "style.css"} {
if err := addFile(form, "files", name); err != nil {
log.Fatal(err)
}
}
form.WriteField("paperWidth", "210mm")
form.WriteField("paperHeight", "297mm")
form.WriteField("printBackground", "true")
form.Close()
req, err := http.NewRequest(http.MethodPost, "https://api.pdfdispatch.com/v1/convert/html", &body)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("PDD_KEY"))
req.Header.Set("Content-Type", form.FormDataContentType())
req.Header.Set("Output-Filename", "invoice-1042")
client := &http.Client{Timeout: 70 * time.Second}
res, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
log.Fatalf("PDFDispatch %d (request %s): %s", res.StatusCode, res.Header.Get("X-Request-Id"), msg)
}
out, err := os.Create("invoice-1042.pdf")
if err != nil {
log.Fatal(err)
}
defer out.Close()
if _, err := io.Copy(out, res.Body); err != nil {
log.Fatal(err)
}
fmt.Println("wrote invoice-1042.pdf")
}
func addFile(form *multipart.Writer, field, path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
part, err := form.CreateFormFile(field, path)
if err != nil {
return err
}
_, err = io.Copy(part, f)
return err
}Java
// java Main.java (index.html and style.css next to it; the key in PDD_KEY)
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;
public class Main {
public static void main(String[] args) throws IOException, InterruptedException {
String boundary = UUID.randomUUID().toString();
Multipart form = new Multipart(boundary);
form.file("files", Path.of("index.html"), "text/html");
form.file("files", Path.of("style.css"), "text/css");
for (Map.Entry<String, String> field : Map.of(
"paperWidth", "210mm",
"paperHeight", "297mm",
"printBackground", "true").entrySet()) {
form.field(field.getKey(), field.getValue());
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.pdfdispatch.com/v1/convert/html"))
.timeout(Duration.ofSeconds(70))
.header("Authorization", "Bearer " + System.getenv("PDD_KEY"))
.header("Output-Filename", "invoice-1042")
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(form.finish()))
.build();
HttpResponse<byte[]> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() != 200) {
System.err.printf("PDFDispatch %d (request %s): %s%n", response.statusCode(),
response.headers().firstValue("X-Request-Id").orElse("-"),
new String(response.body(), StandardCharsets.UTF_8));
System.exit(1);
}
Files.write(Path.of("invoice-1042.pdf"), response.body());
}
/** A small multipart/form-data body builder: java.net.http has none. */
static final class Multipart {
private final ByteArrayOutputStream body = new ByteArrayOutputStream();
private final String boundary;
Multipart(String boundary) {
this.boundary = boundary;
}
void field(String name, String value) {
write("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + name + "\"\r\n\r\n" + value + "\r\n");
}
void file(String name, Path path, String contentType) throws IOException {
write("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + name + "\"; filename=\""
+ path.getFileName() + "\"\r\nContent-Type: " + contentType + "\r\n\r\n");
body.write(Files.readAllBytes(path));
write("\r\n");
}
byte[] finish() {
write("--" + boundary + "--\r\n");
return body.toByteArray();
}
private void write(String text) {
body.writeBytes(text.getBytes(StandardCharsets.UTF_8));
}
}
}java.net.http only. Run with java Main.java.C#
// dotnet run (index.html and style.css next to it; the key in PDD_KEY)
using System.Net.Http.Headers;
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(70) };
using var form = new MultipartFormDataContent();
foreach (var (name, type) in new[] { ("index.html", "text/html"), ("style.css", "text/css") })
{
var file = new ByteArrayContent(await File.ReadAllBytesAsync(name));
file.Headers.ContentType = new MediaTypeHeaderValue(type);
form.Add(file, "files", name);
}
form.Add(new StringContent("210mm"), "paperWidth");
form.Add(new StringContent("297mm"), "paperHeight");
form.Add(new StringContent("true"), "printBackground");
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.pdfdispatch.com/v1/convert/html")
{
Content = form,
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("PDD_KEY"));
request.Headers.Add("Output-Filename", "invoice-1042");
using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var requestId = response.Headers.TryGetValues("X-Request-Id", out var ids) ? ids.First() : "-";
Console.Error.WriteLine($"PDFDispatch {(int)response.StatusCode} (request {requestId}): {await response.Content.ReadAsStringAsync()}");
return 1;
}
await using var output = File.Create("invoice-1042.pdf");
await response.Content.CopyToAsync(output);
return 0;Program.cs of a console app (dotnet new console).Swift
// swift main.swift (index.html and style.css next to it; the key in PDD_KEY)
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking // Linux
#endif
let boundary = UUID().uuidString
var body = Data()
func field(_ name: String, _ value: String) {
body.append("--\(boundary)\r\nContent-Disposition: form-data; name=\"\(name)\"\r\n\r\n\(value)\r\n".data(using: .utf8)!)
}
func file(_ name: String, path: String, type: String) throws {
let header = "--\(boundary)\r\nContent-Disposition: form-data; name=\"\(name)\"; filename=\"\(path)\"\r\n"
+ "Content-Type: \(type)\r\n\r\n"
body.append(header.data(using: .utf8)!)
body.append(try Data(contentsOf: URL(fileURLWithPath: path)))
body.append("\r\n".data(using: .utf8)!)
}
try file("files", path: "index.html", type: "text/html")
try file("files", path: "style.css", type: "text/css")
field("paperWidth", "210mm")
field("paperHeight", "297mm")
field("printBackground", "true")
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
var request = URLRequest(url: URL(string: "https://api.pdfdispatch.com/v1/convert/html")!)
request.httpMethod = "POST"
request.timeoutInterval = 70
request.setValue("Bearer \(ProcessInfo.processInfo.environment["PDD_KEY"] ?? "")", forHTTPHeaderField: "Authorization")
request.setValue("invoice-1042", forHTTPHeaderField: "Output-Filename")
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
let (data, response) = try await URLSession.shared.upload(for: request, from: body)
let http = response as! HTTPURLResponse
guard http.statusCode == 200 else {
let requestId = http.value(forHTTPHeaderField: "X-Request-Id") ?? "-"
FileHandle.standardError.write("PDFDispatch \(http.statusCode) (request \(requestId)): \(String(decoding: data, as: UTF8.self))\n".data(using: .utf8)!)
exit(1)
}
try data.write(to: URL(fileURLWithPath: "invoice-1042.pdf"))Foundation only (macOS, iOS, Linux). In an app you ship, keep the key on your server, not in the app.Dart (Flutter)
// dart run main.dart (index.html and style.css next to it; the key in PDD_KEY)
import 'dart:io';
import 'package:http/http.dart' as http;
Future<void> main() async {
final request = http.MultipartRequest(
'POST',
Uri.parse('https://api.pdfdispatch.com/v1/convert/html'),
)
..headers['Authorization'] = 'Bearer ${Platform.environment['PDD_KEY']}'
..headers['Output-Filename'] = 'invoice-1042'
..fields['paperWidth'] = '210mm'
..fields['paperHeight'] = '297mm'
..fields['printBackground'] = 'true'
..files.add(await http.MultipartFile.fromPath('files', 'index.html'))
..files.add(await http.MultipartFile.fromPath('files', 'style.css'));
final response = await http.Response.fromStream(
await request.send().timeout(const Duration(seconds: 70)),
);
if (response.statusCode != 200) {
stderr.writeln('PDFDispatch ${response.statusCode} '
'(request ${response.headers['x-request-id']}): ${response.body}');
exit(1);
}
await File('invoice-1042.pdf').writeAsBytes(response.bodyBytes);
}dart pub add http, or flutter pub add http). In
a Flutter app you ship, anyone can take the key out of the app: call your own server from the app, and make the PDF
request there.Assets
Everything index.html needs that is not on the public internet must be in the request: stylesheets, images, fonts,
scripts. Refer to each by its bare file name:
<link rel="stylesheet" href="style.css">
<img src="logo.svg" alt="Acme">
<style>
@font-face { font-family: "Inter"; src: url("Inter.woff2") format("woff2"); }
</style>
Assets on the public internet (a CDN font, an image URL) work too, but each one is a download during the conversion
and counts against the time limit. For speed, send them as files, or inline small images as
data: URLs.
Installed fonts, usable by name without sending them: Noto (with CJK and colour emoji), DejaVu, Liberation (metric
twins of Arial, Times New Roman and Courier New), Carlito and Caladea (twins of Calibri and Cambria). Any other font,
send as a file with @font-face.
Print CSS that helps
@page { size: A4; margin: 15mm; }together withpreferCssPageSize=truekeeps the paper size in your template.break-inside: avoidon table rows and cards keeps them from being cut in half;break-before: pagestarts a new page.-webkit-print-color-adjust: exact(orprint-color-adjust: exact) on an element keeps its background colour even withoutprintBackground.@media print { ... }styles apply by default. SendemulatedMediaType=screento get the screen styles instead.- Repeat a table’s header row on every page with a
<thead>; the browser does that by itself.
When it fails
Problems with the request come back as 400 with a plain-text message that names the field, for example a missing
index.html or a paperWidth that is not a size. Gateway errors (key, plan, size, time) are JSON with a code. See
Limits and errors for both.