Getting started
Three steps, about a minute. You need a PDFDispatch account and a terminal with curl; the same request in nine other languages is further down.
1. Make a key
Sign in at
pdfdispatch.com/app/keys
and make a key. Give it a name that says where it will
be used (for example “billing server”), so you can recognise it later. The key starts with pdd_ and is shown only
once: copy it now.
Keep it in an environment variable rather than in your code:
export PDD_KEY=pdd_your_key_here
The Free plan includes 100 PDFs a month, so you can try everything below without paying.
2. Write a page
Any HTML works. Save this as index.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: sans-serif; margin: 0; }
h1 { color: #dc2626; }
</style>
</head>
<body>
<h1>Hello from PDFDispatch</h1>
<p>This page became a PDF.</p>
</body>
</html>
3. Send it
curl --fail-with-body \
--header "Authorization: Bearer $PDD_KEY" \
--form files=@index.html \
--output hello.pdf \
https://api.pdfdispatch.com/v1/convert/html
Open hello.pdf: that is the whole round trip. The answer had status 200 and the PDF as its body. Had something
gone wrong, --fail-with-body makes curl stop with an error, and hello.pdf holds the explanation instead (for
example {"code": 40101, ...} for a key that is not right; see Limits and errors).
The same in your language
The example below adds a stylesheet, A4 paper, background colours and a file name. Pick a language; your choice is remembered on every page.
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.Next
- Authentication: Bearer or Basic, and how to keep keys safe.
- How it works: what counts as a PDF, time and size limits, request ids.
- HTML to PDF: paper size, margins, headers and footers, waiting for scripts.
- Office documents and PDF tools.