PDFCraft
Rust

HTML to PDF in Rust

One POST, a PDF back. No headless browser to install, no Chromium to keep patched, no fonts to install on the box — the render happens on a warm browser that is already running.

Render HTML to a PDF

use serde_json::json;
use std::{env, fs, time::Duration};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(60))
        .build()?;

    let response = client
        .post("https://api.pdfcraft.dev/v1/render")
        .bearer_auth(env::var("PDFCRAFT_API_KEY")?)
        .json(&json!({
            "html": "<h1>Invoice 1042</h1>",
            "options": { "printBackground": true }
        }))
        .send()
        .await?
        .error_for_status()?;

    fs::write("invoice.pdf", response.bytes().await?)?;
    Ok(())
}

Read a PDF back as JSON

The same key works for extraction. Send a PDF, name the fields you want by the label printed on the page, and get them back with the raw text, a coerced value, a confidence and a bounding box.

use base64::{engine::general_purpose::STANDARD, Engine};

let encoded = STANDARD.encode(fs::read("statement.pdf")?);

let body: serde_json::Value = client
    .post("https://api.pdfcraft.dev/v1/extract")
    .bearer_auth(env::var("PDFCRAFT_API_KEY")?)
    .json(&json!({
        "file": encoded,
        "schema": { "account_number": "string", "closing_balance": "currency" }
    }))
    .send()
    .await?
    .json()
    .await?;

println!("{}", body["fields"]["closing_balance"]["value"]);

What bites in Rust

Errors are one shape

Every failure is {"error":{"code","message","docs_url"}} with a stable code, so you can switch on error.code rather than parsing prose. The two worth handling explicitly are rate_limited — honour Retry-After — and render_failed, which means your HTML broke rather than ours did.

The same thing in another language

Or try it with no code at all in the playground. A free key is 100 renders a month.