PDFCraft
Go

HTML to PDF in Go

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.

With the official client

There is a published Go client, so the shortest version is two lines. It has no dependencies of its own, retries 429s and 5xxs for you, and turns the API’s error envelope into a typed error you can branch on. The raw HTTP below still works and is still supported — plenty of people would rather send a POST than take a dependency.

go get github.com/igaurav-dev/pdfcraft-go
client, err := pdfcraft.New(os.Getenv("PDFCRAFT_API_KEY"))
if err != nil {
	log.Fatal(err)
}

pdf, err := client.Render(ctx, pdfcraft.RenderRequest{
	HTML: "<h1>Invoice 1042</h1>",
})
if err != nil {
	log.Fatal(err)
}
os.WriteFile("invoice.pdf", pdf, 0o644)

On pkg.go.dev.

Render HTML to a PDF with plain HTTP

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

func main() {
	body, _ := json.Marshal(map[string]any{
		"html":    "<h1>Invoice 1042</h1>",
		"options": map[string]any{"printBackground": true},
	})

	req, _ := http.NewRequest("POST", "https://api.pdfcraft.dev/v1/render", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PDFCRAFT_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 60 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		message, _ := io.ReadAll(resp.Body)
		panic(fmt.Sprintf("%d: %s", resp.StatusCode, message))
	}

	out, _ := os.Create("invoice.pdf")
	defer out.Close()
	io.Copy(out, resp.Body)
}

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.

payload, _ := json.Marshal(map[string]any{
	"file":   base64.StdEncoding.EncodeToString(pdfBytes),
	"schema": map[string]string{"account_number": "string", "closing_balance": "currency"},
})

req, _ := http.NewRequest("POST", "https://api.pdfcraft.dev/v1/extract", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("PDFCRAFT_API_KEY"))
req.Header.Set("Content-Type", "application/json")

resp, err := client.Do(req)
if err != nil {
	return err
}
defer resp.Body.Close()

var result struct {
	Fields map[string]struct {
		Value      any     `json:"value"`
		Raw        string  `json:"raw"`
		Confidence float64 `json:"confidence"`
	} `json:"fields"`
}
json.NewDecoder(resp.Body).Decode(&result)

What bites in Go

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.