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-goclient, 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
- `io.Copy` straight from the response body to the file — never `io.ReadAll` into memory first. A 400-page render is tens of megabytes and there is no reason for any of it to be resident.
- `defer resp.Body.Close()` goes immediately after the error check, not at the end of the happy path. A leaked body holds the connection out of the pool.
- Set a Timeout on the client. `http.DefaultClient` has none, and a render can legitimately take 30 seconds.
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
- HTML to PDF in Node.js
- HTML to PDF in Python
- HTML to PDF in PHP
- HTML to PDF in Ruby
- HTML to PDF in Java
- HTML to PDF in C#
- HTML to PDF in Rust
- HTML to PDF in curl
- HTML to PDF in Deno
- HTML to PDF in Bun
- HTML to PDF in TypeScript
Or try it with no code at all in the playground. A free key is 100 renders a month.