# HTML to PDF in Go (Golang) HTML to PDF in Go with chromedp, wkhtmltopdf wrappers, self-hosted Gotenberg, or a plain net/http API call. Runnable code for each. **Quick answer.** Post JSON to `https://html-pdf.com/v1/pdf` with a Bearer key and write the response body to a file. This works from Go's standard library or any shell. ```bash curl https://html-pdf.com/v1/pdf -H "Authorization: Bearer $HTML_PDF_KEY" \ -d '{"html": "

Hello

"}' -o hello.pdf ``` Go has no native HTML layout engine. Every HTML to PDF option in Go drives something else, such as headless Chrome, the old wkhtmltopdf binary, or a rendering service over HTTP. Below are the realistic options, what each costs you to run, and a version that needs nothing but the standard library. ## Which Go HTML to PDF option? This table compares the four options at a glance. | Option | Renders with | Modern CSS | You run | Status | |---|---|---|---|---| | chromedp | Headless Chrome via DevTools protocol | Yes | Chrome on every server | Active | | go-wkhtmltopdf | wkhtmltopdf binary (QtWebKit) | Partial, no grid | The wkhtmltopdf binary | Wrapper maintained, binary archived | | Gotenberg (self-hosted) | Headless Chromium in Docker | Yes | A Gotenberg container | Active | | html-pdf.com API | Headless Chromium (Gotenberg), hosted | Yes | Nothing | Hosted | ## Option 1: chromedp chromedp gives you Chrome-quality PDFs from pure Go code. It controls Chrome over the DevTools protocol with no extra Go dependencies, but Chrome itself must be installed (or use the `chromedp/headless-shell` image). ```go package main import ( "context" "log" "os" "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/page" "github.com/chromedp/chromedp" ) func main() { html := `

Invoice #042

$120.00

` ctx, cancel := chromedp.NewContext(context.Background()) defer cancel() // Start the browser on a blank page, then replace its content. if err := chromedp.Do(ctx, chromedp.Navigate("about:blank")); err != nil { log.Fatal(err) } tree, err := chromedp.Call(ctx, page.GetFrameTree, cdp.Empty{}) if err != nil { log.Fatal(err) } if _, err := chromedp.Call(ctx, page.SetDocumentContent, page.SetDocumentContentParams{ FrameID: tree.FrameTree.Frame.ID, HTML: html, }); err != nil { log.Fatal(err) } printBackground := true res, err := chromedp.Call(ctx, page.PrintToPDF, page.PrintToPDFParams{ PrintBackground: &printBackground, PaperWidth: 8.27, // A4, in inches PaperHeight: 11.69, }) if err != nil { log.Fatal(err) } os.WriteFile("invoice.pdf", res.Data, 0o644) } ``` This uses the generic API that chromedp adopted in v0.17 (tested with v0.20). Older tutorials pass `chromedp.ActionFunc` steps to `chromedp.Run`, which no longer compiles on current versions. Output matches Chrome's print preview, and paper sizes and margins are in inches. The cost is operations. Chrome adds a few hundred MB to your image, and each render needs a browser tab and its memory. Long-running services also need to reuse a browser, cap concurrency and recover from crashed processes. ## Option 2: wkhtmltopdf wrappers This option is only worth it for legacy templates. `github.com/SebastiaanKlippert/go-wkhtmltopdf` wraps the wkhtmltopdf command line with typed options, and it still works. But wkhtmltopdf itself is archived and no longer receives fixes, and its QtWebKit engine predates flexbox and grid support. The wrapper's own README recommends looking at alternatives for new projects. Our [wkhtmltopdf alternative](https://html-pdf.com/wkhtmltopdf-alternative) guide covers migration. ## Option 3: self-hosted Gotenberg Gotenberg gives you Chromium output without Chrome inside your Go service. It packages Chromium and LibreOffice behind an HTTP API in one Docker image. Your Go code just posts a multipart form, which you can do with the standard library instead of a client package. ```bash docker run --rm -p 3000:3000 gotenberg/gotenberg:8 ``` ```go var body bytes.Buffer w := multipart.NewWriter(&body) part, _ := w.CreateFormFile("files", "index.html") // must be named index.html part.Write([]byte("

Invoice #042

")) w.WriteField("paperWidth", "8.27") w.WriteField("paperHeight", "11.69") w.Close() resp, err := http.Post("http://localhost:3000/forms/chromium/convert/html", w.FormDataContentType(), &body) ``` The trade-off is that you now run, scale, secure and update the container yourself. See [self-host Gotenberg with Docker](https://html-pdf.com/self-host-gotenberg-docker). ## Or skip the server: html-pdf.com This section gives you a complete Go client with no server to run. html-pdf.com is hosted Gotenberg with an API key. You send JSON and get PDF bytes back, using only `net/http` and `encoding/json`. Get a key, then open the link we email you to confirm your address. That adds 100 free credits. ```bash curl -X POST https://html-pdf.com/v1/signup -H "Content-Type: application/json" -d '{"email": "you@example.com"}' ``` ```go package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "time" ) type apiError struct { Error struct { Code string `json:"code"` Message string `json:"message"` Hint string `json:"hint"` } `json:"error"` } var client = &http.Client{Timeout: 60 * time.Second} func RenderPDF(html string) ([]byte, error) { payload, _ := json.Marshal(map[string]any{ "html": html, "format": "A4", "margin": "15mm", "max_pages": 20, // fail without charge if it runs longer "footer_html": `
` + `Page of
`, }) req, err := http.NewRequest(http.MethodPost, "https://html-pdf.com/v1/pdf", bytes.NewReader(payload)) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+os.Getenv("HTML_PDF_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { var e apiError json.Unmarshal(data, &e) return nil, fmt.Errorf("%d %s: %s", resp.StatusCode, e.Error.Code, e.Error.Message) } fmt.Println("pages:", resp.Header.Get("X-Pages"), "remaining:", resp.Header.Get("X-Credits-Remaining")) return data, nil } func main() { pdf, err := RenderPDF(`

Invoice #042

Total: $120.00

`) if err != nil { panic(err) } os.WriteFile("invoice.pdf", pdf, 0o644) } ``` Margins take CSS units (`"15mm"`, `"0.5in"`) instead of inches as floats. To convert a public page, send `"url": "https://example.com"` instead of `html`. A `402` with code `insufficient_credits` means you need to top up. ## Serve the PDF from an HTTP handler Here is how to return the PDF directly from a `net/http` handler. ```go http.HandleFunc("/invoice.pdf", func(w http.ResponseWriter, r *http.Request) { pdf, err := RenderPDF(buildInvoiceHTML(r.URL.Query().Get("id"))) if err != nil { http.Error(w, "could not render PDF", http.StatusBadGateway) return } w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Disposition", `inline; filename="invoice.pdf"`) w.Write(pdf) }) ``` When you build the HTML from user data, escape it with `html.EscapeString` or the `html/template` package. Reference images and fonts by absolute `https://` URLs or inline them, since the renderer can't reach `localhost` or private networks. You can also send `index.html` plus asset files as multipart, with your options in an `options` JSON field. ## Pricing Pricing is per page, with no subscription. 1 credit = 1 PDF page, and you start with 100 free credits. After that, packs go from $10 for 1,000 pages down to $0.005 per page at $250 for 50,000. Purchased credits never expire and failed renders are free. - **Limits:** 20 MB request, 30 seconds per render, 120 requests per minute. More: [page breaks](https://html-pdf.com/html-to-pdf-page-breaks) · [headers, footers and page numbers](https://html-pdf.com/pdf-headers-footers-page-numbers) · [API docs](https://html-pdf.com/docs) · [sign in](https://html-pdf.com/login) ## FAQ ### How do I convert HTML to PDF in Go? Use chromedp to drive headless Chrome, post a form to a self-hosted Gotenberg container, or send JSON to `https://html-pdf.com/v1/pdf` with `net/http` and save the response bytes. ### Is there a pure Go HTML to PDF library? No. Go has no native HTML layout engine, so every reliable option drives Chromium (or the archived wkhtmltopdf binary) somewhere. ### Is wkhtmltopdf still a good choice in Go? Not for new projects. wkhtmltopdf is archived and its QtWebKit engine predates flexbox and grid, so keep it only for legacy templates. ### Do credits expire? No. Purchased credits never expire, 1 credit is 1 PDF page, and failed renders cost nothing.