Skip to content
PDFDispatch

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

POST /v1/convert/html

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);
}

Response

HTTP/2 200
content-type: application/pdf
x-request-id: 7f3b2c1e-5a9d-4e8b-9c41-2d6f0a8e1b37

out.pdf · 1 page · 30 KB

The first page of out.pdf: an invoice with an orange header, four items and a total of €715.

How it works

Three steps to your first PDF

  1. Make a key

    Sign up, then make a key in the web app. It starts with pdd_ and is shown once.

  2. Send your file

    POST your HTML, a URL or an Office file to the API, with the key in the Authorization header.

  3. 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.