PDFPipe

Guide

HTML to PDF in Tauri

Tauri splits your desktop app into two processes: a Rust core that handles system calls and secrets, and a WebView frontend that handles the UI. PDF generation belongs in the Rust core, where your API key never touches the renderer. You define a generate_pdf Tauri command in Rust, call it from your TypeScript frontend with invoke(), and get a document URL back. No Chromium subprocess to manage, no file system temp directories, and the key stays out of the WebView entirely.

Tauri command

Annotate a Rust function with #[tauri::command] to expose it to the frontend. The function reads PDFPIPE_API_KEY from the environment, posts the HTML to the PDF API with store: true, and returns the response JSON. Errors are surfaced as Err(String) so the frontend can catch them cleanly:

rust
// src-tauri/src/main.rs
use tauri::command;
use std::env;

#[derive(serde::Deserialize)]
struct PdfApiResponse {
    document_url: Option<String>,
    document_id: Option<String>,
}

#[command]
async fn generate_pdf(html: String, filename: String) -> Result<serde_json::Value, String> {
    let api_key = env::var("PDFPIPE_API_KEY")
        .map_err(|_| "PDFPIPE_API_KEY is not set".to_string())?;

    let client = reqwest::Client::new();

    let body = serde_json::json!({
        "html": html,
        "store": true,
        "filename": filename,
        "options": { "format": "A4" }
    });

    let response = client
        .post("https://api.pdfpipe.xyz/v1/pdf")
        .header("Authorization", format!("Bearer {}", api_key))
        .header("Content-Type", "application/json")
        .json(&body)
        .send()
        .await
        .map_err(|e| e.to_string())?;

    if !response.status().is_success() {
        let err: serde_json::Value = response.json().await.unwrap_or_default();
        return Err(err["detail"]
            .as_str()
            .unwrap_or("render failed")
            .to_string());
    }

    let result: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
    Ok(result)
}

Registering the command

Pass the function to tauri::generate_handler! in your main.rs. Tauri uses the macro to wire the command name to the Rust function at compile time:

rust
// src-tauri/src/main.rs — register the command
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![generate_pdf])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Frontend invocation

From your TypeScript frontend, call the command with invoke() from @tauri-apps/api/core. The argument names must match the Rust function parameters exactly. The return value is the JSON body from the API, which includes document_url for the stored PDF:

typescript
// In your TypeScript/Vite frontend
import { invoke } from "@tauri-apps/api/core";

interface PdfResult {
  document_url: string;
  document_id: string;
}

async function exportAsPdf(html: string, filename: string): Promise<string> {
  const result = await invoke<PdfResult>("generate_pdf", { html, filename });
  return result.document_url;
}

// Example usage from a button handler:
const url = await exportAsPdf(
  "<h1>My Report</h1><p>Generated by my Tauri app.</p>",
  "my-report.pdf"
);
console.log("PDF available at:", url);

Cargo.toml dependencies

Add reqwest for the HTTP client, serde_json for JSON handling, and tokio as the async runtime. The default-features = false on reqwest keeps the binary lean by omitting TLS backends you do not need on a desktop target:

toml
# src-tauri/Cargo.toml — add to [dependencies]
[dependencies]
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

[dependencies.reqwest]
version = "0.12"
features = ["json"]
default-features = false

Environment variable setup

Set PDFPIPE_API_KEY as an OS environment variable before launching the app. For local development a .env file works well when sourced before cargo tauri dev. For production builds, set it in CI or use a platform keychain integration. The key is read by the Rust core process only and is never bundled into the frontend assets or accessible from the WebView:

sh
# .env (project root, for local development)
PDFPIPE_API_KEY=your_api_key_here

# Load it before running tauri dev:
#   On Unix: export $(cat .env | xargs) && cargo tauri dev
#   On Windows PowerShell:
#   $env:PDFPIPE_API_KEY = "your_api_key_here"; cargo tauri dev

# For production builds, set the variable in your CI environment or
# via your OS keychain integration. Never embed the key in the
# compiled binary or expose it in the WebView/frontend bundle.

Getting an API key

The Hobby plan gives 500 free documents a month with no credit card required. Paid plans start at $19 and include a longer document archive, higher limits, and email support.

Full API reference on the docs page. Try it now in the live playground with no signup.

Get an API key →