PDF API, hosted in the EU
HTML, web pages and Office files to PDF, with one HTTP request.
Send your HTML, a URL or a Word, Excel or PowerPoint file to the PDFDispatch API and get a PDF back. Merge, split, encrypt and convert to PDF/A too.
- 100 PDFs a month for free
- Servers in Germany
- Monthly plans, cancel at any time
An example request
Bash (curl)
# Saves out.pdf, or prints the error: {"code": ..., "error": "..."}
curl https://api.pdfdispatch.com/v1/convert/html \
--header "Authorization: Bearer pdd_..." \
--form files=@index.html \
--fail-with-body --output out.pdf \
|| { cat out.pdf >&2; rm -f out.pdf; false; }
Node.js
// convert.mjs: Node.js 20 or newer, no packages.
import { openAsBlob } from 'node:fs';
import { writeFile } from 'node:fs/promises';
const form = new FormData();
form.append('files', await openAsBlob('index.html', { type: 'text/html' }), 'index.html');
const res = await fetch('https://api.pdfdispatch.com/v1/convert/html', {
method: 'POST',
headers: { Authorization: 'Bearer pdd_...' },
body: form,
});
if (!res.ok) {
const text = await res.text();
let message = text;
try {
const { code, error } = JSON.parse(text);
message = `${code}: ${error}`;
} catch {}
console.error(`HTTP ${res.status} ${message}`);
process.exit(1);
}
await writeFile('out.pdf', Buffer.from(await res.arrayBuffer()));
Bun
// convert.ts: bun convert.ts
const form = new FormData();
form.append('files', Bun.file('index.html'), 'index.html');
const res = await fetch('https://api.pdfdispatch.com/v1/convert/html', {
method: 'POST',
headers: { Authorization: 'Bearer pdd_...' },
body: form,
});
if (!res.ok) {
const text = await res.text();
let message = text;
try {
const { code, error } = JSON.parse(text);
message = `${code}: ${error}`;
} catch {}
console.error(`HTTP ${res.status} ${message}`);
process.exit(1);
}
await Bun.write('out.pdf', res);
Python
import sys
import requests
with open("index.html", "rb") as f:
res = requests.post(
"https://api.pdfdispatch.com/v1/convert/html",
headers={"Authorization": "Bearer pdd_..."},
files={"files": ("index.html", f, "text/html")},
timeout=90,
)
if not res.ok:
try:
err = res.json()
message = f"{err['code']}: {err['error']}"
except ValueError:
message = res.text
sys.exit(f"HTTP {res.status_code} {message}")
with open("out.pdf", "wb") as out:
out.write(res.content)
PHP
<?php
$ch = curl_init('https://api.pdfdispatch.com/v1/convert/html');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer pdd_...'],
CURLOPT_POSTFIELDS => ['files' => new CURLFile('index.html', 'text/html', 'index.html')],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false) {
fwrite(STDERR, 'Request failed: '.curl_error($ch).PHP_EOL);
exit(1);
}
if ($status < 200 || $status >= 300) {
$error = json_decode($body, true);
$message = isset($error['code']) ? "{$error['code']}: {$error['error']}" : $body;
fwrite(STDERR, "HTTP $status $message".PHP_EOL);
exit(1);
}
file_put_contents('out.pdf', $body);
Go
package main
import (
"bytes"
"encoding/json"
"io"
"log"
"mime/multipart"
"net/http"
"os"
)
func main() {
html, err := os.ReadFile("index.html")
if err != nil {
log.Fatal(err)
}
var body bytes.Buffer
form := multipart.NewWriter(&body)
part, err := form.CreateFormFile("files", "index.html")
if err != nil {
log.Fatal(err)
}
part.Write(html)
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 pdd_...")
req.Header.Set("Content-Type", form.FormDataContentType())
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
}
if res.StatusCode < 200 || res.StatusCode > 299 {
var apiErr struct {
Code int `json:"code"`
Error string `json:"error"`
}
if json.Unmarshal(data, &apiErr) == nil {
log.Fatalf("HTTP %d %d: %s", res.StatusCode, apiErr.Code, apiErr.Error)
}
log.Fatalf("HTTP %d %s", res.StatusCode, data)
}
if err := os.WriteFile("out.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
Java
// ConvertHtml.java: JDK 11 or newer, no libraries.
import java.io.ByteArrayOutputStream;
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.util.UUID;
public class ConvertHtml {
public static void main(String[] args) throws Exception {
String boundary = "pdd-" + UUID.randomUUID();
ByteArrayOutputStream body = new ByteArrayOutputStream();
body.write(("--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"files\"; filename=\"index.html\"\r\n"
+ "Content-Type: text/html\r\n\r\n").getBytes(StandardCharsets.UTF_8));
body.write(Files.readAllBytes(Path.of("index.html")));
body.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.pdfdispatch.com/v1/convert/html"))
.header("Authorization", "Bearer pdd_...")
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray()))
.build();
HttpResponse<byte[]> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() / 100 != 2) {
// The error as JSON: {"code": ..., "error": "..."}
String error = new String(response.body(), StandardCharsets.UTF_8);
System.err.println("HTTP " + response.statusCode() + " " + error);
System.exit(1);
}
Files.write(Path.of("out.pdf"), response.body());
}
}
C#
using System.Net.Http.Headers;
using System.Text.Json;
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "pdd_...");
using var form = new MultipartFormDataContent();
var html = new ByteArrayContent(await File.ReadAllBytesAsync("index.html"));
html.Headers.ContentType = new MediaTypeHeaderValue("text/html");
form.Add(html, "files", "index.html");
using var res = await client.PostAsync("https://api.pdfdispatch.com/v1/convert/html", form);
if (!res.IsSuccessStatusCode)
{
var message = await res.Content.ReadAsStringAsync();
try
{
using var json = JsonDocument.Parse(message);
message = $"{json.RootElement.GetProperty("code")}: {json.RootElement.GetProperty("error")}";
}
catch (JsonException)
{
// Not JSON: a plain-text message.
}
Console.Error.WriteLine($"HTTP {(int)res.StatusCode} {message}");
return 1;
}
await File.WriteAllBytesAsync("out.pdf", await res.Content.ReadAsByteArrayAsync());
return 0;
Swift
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct APIError: Decodable {
let code: Int
let error: String
}
let boundary = "pdd-\(UUID().uuidString)"
var body = Data()
body.append(Data("--\(boundary)\r\n".utf8))
body.append(Data("Content-Disposition: form-data; name=\"files\"; filename=\"index.html\"\r\n".utf8))
body.append(Data("Content-Type: text/html\r\n\r\n".utf8))
body.append(try Data(contentsOf: URL(fileURLWithPath: "index.html")))
body.append(Data("\r\n--\(boundary)--\r\n".utf8))
var request = URLRequest(url: URL(string: "https://api.pdfdispatch.com/v1/convert/html")!)
request.httpMethod = "POST"
request.setValue("Bearer pdd_...", forHTTPHeaderField: "Authorization")
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
let (data, response) = try await URLSession.shared.upload(for: request, from: body)
let status = (response as! HTTPURLResponse).statusCode
guard (200..<300).contains(status) else {
if let apiError = try? JSONDecoder().decode(APIError.self, from: data) {
fatalError("HTTP \(status) \(apiError.code): \(apiError.error)")
}
fatalError("HTTP \(status) \(String(decoding: data, as: UTF8.self))")
}
try data.write(to: URL(fileURLWithPath: "out.pdf"))
Dart (Flutter)
// pubspec.yaml: dependencies: http: ^1.2.0
import 'dart:convert';
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 pdd_...'
..files.add(await http.MultipartFile.fromPath('files', 'index.html'));
final response = await http.Response.fromStream(await request.send());
if (response.statusCode < 200 || response.statusCode >= 300) {
var message = response.body;
try {
final error = jsonDecode(response.body) as Map<String, dynamic>;
message = '${error['code']}: ${error['error']}';
} on FormatException {
// Not JSON: a plain-text message.
}
stderr.writeln('HTTP ${response.statusCode} $message');
exit(1);
}
await File('out.pdf').writeAsBytes(response.bodyBytes);
}
How it works
Three steps to your first PDF
-
Make a key
Sign up, then make a key in the web app. It starts with pdd_ and is shown once.
-
Send your file
POST your HTML, a URL or an Office file to the API, with the key in the Authorization header.
-
Get the PDF
The PDF comes back in the answer. One successful request is one PDF, whatever its pages.
What you can send
One API for every kind of document
HTML
Your HTML with its CSS, images and fonts, printed by Chromium. Paper size, margins, headers and footers.
/v1/convert/html
Web pages
Any public page by its URL, JavaScript included. Wait for a selector or a delay before printing.
/v1/convert/url
Markdown
Markdown files, placed in your own HTML template and styled with your CSS.
/v1/convert/markdown
Office files
Word, Excel, PowerPoint, OpenDocument, RTF and more, converted with LibreOffice.
/v1/convert/office
Edit PDFs
Merge, split, rotate, watermark, stamp, encrypt, flatten and make smaller.
/v1/pdf/merge, /v1/pdf/split, ...
PDF/A, PDF/UA, e-invoices
PDF/A for archives, PDF/UA for screen readers, metadata, bookmarks and Factur-X / ZUGFeRD.
/v1/pdf/pdfa, /v1/pdf/factur-x
Every route and option, with examples: the routes in the docs.
Pricing
Pay for the PDFs you make
Monthly plans by the number of PDFs. Every plan has the whole API.
Free
€0
100 PDFs a month
Then €0.048 per extra PDF, paid with credits.
Paid plans
from €8 a month
500 PDFs a month, then €0.035 per extra PDF
8 more plans up to 500,000 PDFs a month.
Past your plan
Credits
Extra PDFs are paid with credits, on every plan, Free included. Packs from €10; nothing goes on an invoice.
Credits never expire. No automatic top-up.
All plans and prices. Prices in euros. Need a contract and invoices? Write to us.
Your data
Built and hosted in the EU
Servers in Germany
The API and your account data are on our own servers in Germany.
Files are not kept
Files sent to the API are kept only while the PDF is made and are not stored afterwards.
Your key stays yours
We store only a hash of each key. It is checked at our gateway; the PDF service never sees it.
Made in Slovenia
Europe first. Prices in euros.
The details: Privacy Policy.
Make your first PDF in a minute
100 PDFs a month are free. Make a key, send a request, get a PDF.