Back to Blog
Guide February 26, 2026 · 4 min read

How to Take Screenshots and Generate PDFs in R

webshot2 requires a local Chromium install and breaks on RStudio Connect and Posit Cloud. Here's a zero-dependency alternative: one httr2 call, binary response. Works anywhere R runs — including Shiny apps, R Markdown reports, and Plumber APIs.

The standard R approach for capturing web content is webshot2, which wraps a local Chromium installation via chromote. It works locally but breaks on RStudio Connect, Posit Cloud, and any environment where you can't install or run a headless browser.

Here's the simpler path: one httr2 call, binary response. Works anywhere R runs.

Setup

No package installs beyond what most R projects already have:

# install.packages("httr2")  # if not already installed
library(httr2)

Screenshot from a URL

pagebolt_screenshot <- function(url, full_page = TRUE, block_banners = TRUE) {
  api_key <- Sys.getenv("PAGEBOLT_API_KEY", unset = stop("PAGEBOLT_API_KEY not set"))

  resp <- request("https://pagebolt.dev/api/v1/screenshot") |>
    req_headers("x-api-key" = api_key) |>
    req_body_json(list(
      url = url,
      fullPage = full_page,
      blockBanners = block_banners
    )) |>
    req_perform()

  resp_body_raw(resp)
}

# Save to file
image_bytes <- pagebolt_screenshot("https://example.com")
writeBin(image_bytes, "screenshot.png")
cat("Screenshot saved:", length(image_bytes), "bytes\n")

PDF from a URL

pagebolt_pdf_url <- function(url, block_banners = TRUE) {
  api_key <- Sys.getenv("PAGEBOLT_API_KEY", unset = stop("PAGEBOLT_API_KEY not set"))

  resp <- request("https://pagebolt.dev/api/v1/pdf") |>
    req_headers("x-api-key" = api_key) |>
    req_body_json(list(url = url, blockBanners = block_banners)) |>
    req_perform()

  resp_body_raw(resp)
}

pdf_bytes <- pagebolt_pdf_url("https://example.com")
writeBin(pdf_bytes, "report.pdf")

PDF from HTML (R Markdown / knitr output)

This is the key use case for R: render a report to HTML first, then capture it as a pixel-perfect PDF:

pagebolt_pdf_html <- function(html) {
  api_key <- Sys.getenv("PAGEBOLT_API_KEY", unset = stop("PAGEBOLT_API_KEY not set"))

  resp <- request("https://pagebolt.dev/api/v1/pdf") |>
    req_headers("x-api-key" = api_key) |>
    req_body_json(list(html = html)) |>
    req_perform()

  resp_body_raw(resp)
}

# Render an R Markdown document to HTML string, then capture as PDF
rmd_to_pdf <- function(rmd_file, output_file = "output.pdf") {
  # Render to a temp HTML file
  tmp_html <- tempfile(fileext = ".html")
  rmarkdown::render(rmd_file, output_format = "html_document", output_file = tmp_html, quiet = TRUE)

  html <- paste(readLines(tmp_html), collapse = "\n")
  pdf_bytes <- pagebolt_pdf_html(html)
  writeBin(pdf_bytes, output_file)
  invisible(output_file)
}

rmd_to_pdf("analysis.Rmd", "analysis.pdf")

Shiny app — export dashboard as PDF

library(shiny)

ui <- fluidPage(
  titlePanel("Sales Dashboard"),
  plotOutput("sales_plot"),
  downloadButton("download_pdf", "Export as PDF")
)

server <- function(input, output, session) {
  output$sales_plot <- renderPlot({
    plot(1:10, main = "Sales Data")
  })

  output$download_pdf <- downloadHandler(
    filename = function() paste0("dashboard-", Sys.Date(), ".pdf"),
    content = function(file) {
      # Capture the live Shiny app URL
      app_url <- paste0(session$clientData$url_protocol, "//",
                        session$clientData$url_hostname, ":",
                        session$clientData$url_port,
                        session$clientData$url_pathname)

      pdf_bytes <- pagebolt_pdf_url(app_url)
      writeBin(pdf_bytes, file)
    },
    type = "application/pdf"
  )
}

shinyApp(ui, server)

Plumber API — screenshot endpoint

library(plumber)

#* Capture a screenshot
#* @param url The URL to capture
#* @serializer contentType list(type="image/png")
#* @post /screenshot
function(req) {
  body <- jsonlite::fromJSON(req$postBody)
  pagebolt_screenshot(body$url)
}

#* Generate a PDF from HTML
#* @serializer contentType list(type="application/pdf")
#* @post /pdf
function(req) {
  body <- jsonlite::fromJSON(req$postBody)
  pagebolt_pdf_html(body$html)
}

Reusable helper script

Drop this into R/pagebolt.R in any project:

#' PageBolt API client
#'
#' Requires PAGEBOLT_API_KEY environment variable.
#' @import httr2

.pagebolt_post <- function(endpoint, body) {
  api_key <- Sys.getenv("PAGEBOLT_API_KEY", unset = stop("Set PAGEBOLT_API_KEY"))
  resp <- request(paste0("https://pagebolt.dev/api/v1", endpoint)) |>
    req_headers("x-api-key" = api_key) |>
    req_body_json(body) |>
    req_error(body = \(r) resp_body_string(r)) |>
    req_perform()
  resp_body_raw(resp)
}

pagebolt_screenshot <- function(url, ...) .pagebolt_post("/screenshot", list(url = url, ...))
pagebolt_pdf_url    <- function(url, ...) .pagebolt_post("/pdf", list(url = url, ...))
pagebolt_pdf_html   <- function(html, ...) .pagebolt_post("/pdf", list(html = html, ...))

No webshot2, no chromote, no Chromium binary. httr2 is a base R tidyverse package — it's already installed in virtually every R environment including RStudio Connect and Posit Cloud.

Get Started Free

100 requests/month, no credit card

Screenshots, PDFs, and video recordings from your R app — no webshot2, no Chromium, works on RStudio Connect, Posit Cloud, and Shiny deployments.

Get Your Free API Key →