PDFDispatch Docs

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

FieldTypeDefaultWhat it does
index.html requiredfile–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 filefile–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.htmlfile–Repeated at the top and bottom of every page. See Header and footer.

The options most requests need:

FieldTypeDefaultWhat it does
paperWidth, paperHeightsize8.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, marginRightsize0.39 (1 cm)The page margins, in the same units. A header or footer is drawn inside the top or bottom margin.
landscapebooleanfalseTurn the paper sideways.
printBackgroundbooleanfalsePrint background colours and images. Without it, most backgrounds are left out, as a browser does when it prints.
preferCssPageSizebooleanfalseUse the size from the page’s CSS @page rule instead of paperWidth and paperHeight.
waitDelayduration–Wait this long after the page has loaded before printing, for example 500ms or 2s. See Waiting for better ways.
emulatedMediaTypestringprintprint 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

HeaderTypeDefaultWhat it does
Authorization requiredrequest header–Your key: Bearer pdd_..., or Basic auth with the key as the password. See Authentication.
Output-Filenamerequest 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-Urlrequest header–Send the result to this URL instead of in the answer. See Webhooks for the other Webhook-* headers.
X-Request-Idanswer headera UUIDThe 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-Afteranswer 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/html

Node.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()));
Node.js 20 or newer (built-in 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);
Bun 1.1 or newer. No packages needed.

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)
Python 3.9 or newer with Requests (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);
}
PHP 8.1 or newer with the curl extension (on by default in most builds).

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
}
Go 1.22 or newer; standard library only.

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 11 or newer; 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;
.NET 8 or newer, as the 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"))
Swift 5.7 or newer, 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 3 with the http package (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.

  • @page { size: A4; margin: 15mm; } together with preferCssPageSize=true keeps the paper size in your template.
  • break-inside: avoid on table rows and cards keeps them from being cut in half; break-before: page starts a new page.
  • -webkit-print-color-adjust: exact (or print-color-adjust: exact) on an element keeps its background colour even without printBackground.
  • @media print { ... } styles apply by default. Send emulatedMediaType=screen to 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.