Screenshot API Elixir Guide 2026

Capture screenshots and generate PDFs from Elixir and Phoenix with HTTPoison, Tesla, GenServer pools, and Oban background jobs.

Get Free API Key

Calling SnapAPI from Elixir

Elixir's HTTPoison and Tesla HTTP clients both handle SnapAPI's simple Bearer token authentication and binary response bodies cleanly. Add HTTPoison or Tesla to your mix.exs dependencies, configure your API key as an application environment variable, and make GET requests to the screenshot endpoint with your target URL and desired parameters. The response body is the raw image bytes which you can write to disk, store in S3 via ExAws, or return directly from a Phoenix controller action.

# mix.exs dependencies
defp deps do
  [
    {:httpoison, "~> 2.2"},
    {:jason, "~> 1.4"},
    {:ex_aws, "~> 2.5"},
    {:ex_aws_s3, "~> 2.5"},
  ]
end
# lib/myapp/screenshot.ex
defmodule Myapp.Screenshot do
  @base_url "https://api.snapapi.pics"

  defp api_key, do: Application.fetch_env!(:myapp, :snapapi_key)

  def capture(url, opts \ []) do
    format = Keyword.get(opts, :format, "png")
    full_page = Keyword.get(opts, :full_page, false)
    width = Keyword.get(opts, :viewport_width, 1280)

    params = %{
      url: url,
      format: format,
      full_page: full_page,
      viewport_width: width,
    }

    headers = [{"Authorization", "Bearer #{api_key()}"}]

    case HTTPoison.get("#{@base_url}/screenshot", headers, params: params) do
      {:ok, %{status_code: 200, body: body}} ->
        {:ok, body}
      {:ok, %{status_code: code, body: body}} ->
        {:error, "HTTP #{code}: #{body}"}
      {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, "Network error: #{reason}"}
    end
  end

  def capture_pdf(url, opts \ []) do
    paper = Keyword.get(opts, :paper_format, "A4")
    params = %{url: url, format: "pdf", paper_format: paper}
    headers = [{"Authorization", "Bearer #{api_key()}"}]

    case HTTPoison.get("#{@base_url}/screenshot", headers, params: params) do
      {:ok, %{status_code: 200, body: body}} -> {:ok, body}
      {:ok, %{status_code: code, body: body}} -> {:error, "HTTP #{code}: #{body}"}
      {:error, reason} -> {:error, reason}
    end
  end
end

Tesla Middleware for SnapAPI

Tesla provides a composable middleware system that handles authentication, retry logic, and JSON encoding cleanly. Define a Tesla client module with the base URL, authentication middleware, and a retry middleware for transient failures. This approach keeps authentication concerns separated from business logic and makes it easy to add logging or telemetry middleware later.

# lib/myapp/snapapi_client.ex
defmodule Myapp.SnapapiClient do
  use Tesla

  plug Tesla.Middleware.BaseUrl, "https://api.snapapi.pics"
  plug Tesla.Middleware.Headers, [
    {"Authorization", "Bearer #{Application.compile_env!(:myapp, :snapapi_key)}"}
  ]
  plug Tesla.Middleware.Retry,
    delay: 500,
    max_retries: 3,
    should_retry: fn
      {:ok, %{status: status}} when status in [429, 503] -> true
      {:error, _} -> true
      _ -> false
    end

  def screenshot(url, opts \ []) do
    params = Keyword.merge([url: url, format: "png"], opts)
    get("/screenshot", query: params)
  end

  def scrape(url) do
    get("/scrape", query: [url: url])
  end
end

GenServer Pool for Concurrent Screenshots

Elixir's actor model makes concurrent screenshot processing natural. Use Task.async_stream to process a list of URLs concurrently with configurable parallelism. Task.async_stream handles backpressure automatically: set max_concurrency to a reasonable value such as 10 to avoid overwhelming the API with simultaneous requests, and set timeout to 30 seconds to handle slow-rendering pages gracefully.

defmodule Myapp.BatchScreenshot do
  alias Myapp.Screenshot

  def capture_batch(urls, opts \ []) do
    concurrency = Keyword.get(opts, :max_concurrency, 10)
    timeout = Keyword.get(opts, :timeout, 30_000)

    urls
    |> Task.async_stream(
      fn url ->
        case Screenshot.capture(url, opts) do
          {:ok, bytes} -> {url, {:ok, bytes}}
          {:error, reason} -> {url, {:error, reason}}
        end
      end,
      max_concurrency: concurrency,
      timeout: timeout,
      on_timeout: :kill_task
    )
    |> Enum.map(fn
      {:ok, result} -> result
      {:exit, :timeout} -> {:error, "timeout"}
    end)
  end
end

Oban Background Jobs for Screenshot Generation

For production Phoenix applications, screenshot generation should run as background jobs rather than in the request cycle. Oban is the standard Elixir job processing library, providing reliable job queuing with PostgreSQL persistence, retry logic, and scheduled execution. Define an Oban worker module that captures a screenshot and stores the result in S3 or another storage backend, then enqueue jobs from your Phoenix controllers or event handlers when screenshots are needed.

defmodule Myapp.Workers.ScreenshotWorker do
  use Oban.Worker, queue: :screenshots, max_attempts: 3

  alias Myapp.Screenshot
  alias Myapp.Screenshots

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"url" => url, "record_id" => record_id}}) do
    with {:ok, bytes} <- Screenshot.capture(url, full_page: true, format: "png"),
         {:ok, s3_key} <- upload_to_s3(bytes, record_id),
         {:ok, _} <- Screenshots.update_record(record_id, %{screenshot_url: s3_key}) do
      :ok
    else
      {:error, reason} ->
        {:error, reason}
    end
  end

  defp upload_to_s3(bytes, id) do
    key = "screenshots/#{id}.png"
    result = ExAws.S3.put_object("my-bucket", key, bytes)
      |> ExAws.request()
    case result do
      {:ok, _} -> {:ok, key}
      {:error, _} = err -> err
    end
  end
end

# Enqueue from a controller:
# %{url: url, record_id: record.id}
# |> Myapp.Workers.ScreenshotWorker.new()
# |> Oban.insert()

Phoenix Controller: Screenshot Proxy Endpoint

A Phoenix controller can proxy screenshot requests to SnapAPI, protecting the API key from browser exposure and adding authentication, rate limiting, and caching in front of the screenshot endpoint. The controller calls SnapAPI with the requested URL, receives the image bytes, and sends them back to the client with the appropriate content type header. Add plug-based authentication and rate limiting before this controller to control access.

defmodule MyappWeb.ScreenshotController do
  use MyappWeb, :controller
  alias Myapp.Screenshot

  def show(conn, %{"url" => url}) do
    with {:ok, bytes} <- Screenshot.capture(url, format: "png"),
         do: conn
             |> put_resp_content_type("image/png")
             |> put_resp_header("cache-control", "public, max-age=3600")
             |> send_resp(200, bytes),
         else: ({:error, reason} ->
           conn |> put_status(502) |> json(%{error: reason}))
  end
end

Scraping and Data Extraction in Elixir

SnapAPI's scraping and extraction endpoints follow the same request pattern as the screenshot endpoint in Elixir, with the response body being JSON rather than binary image bytes. The scrape endpoint returns a map with title, description, text, links, and status code keys. Decode the JSON response body with Jason.decode! and pattern match on the result to extract the fields you need. The extract endpoint accepts a JSON body describing the schema of the data you want extracted from the page: pass a map with field names and types and receive back a map with the extracted values. This structured extraction pattern is useful for building web data pipelines in Elixir that consume external web pages and transform their content into structured records stored in your Ecto schema.

defmodule Myapp.Scraper do
  @base_url "https://api.snapapi.pics"

  defp api_key, do: Application.fetch_env!(:myapp, :snapapi_key)

  def scrape(url) do
    headers = [{"Authorization", "Bearer #{api_key()}"}]
    case HTTPoison.get("#{@base_url}/scrape", headers, params: %{url: url}) do
      {:ok, %{status_code: 200, body: body}} ->
        {:ok, Jason.decode!(body)}
      {:ok, %{status_code: code}} ->
        {:error, "HTTP #{code}"}
      {:error, reason} ->
        {:error, reason}
    end
  end

  def extract(url, schema) do
    headers = [
      {"Authorization", "Bearer #{api_key()}"},
      {"Content-Type", "application/json"}
    ]
    body = Jason.encode!(%{url: url, schema: schema})
    case HTTPoison.post("#{@base_url}/extract", body, headers) do
      {:ok, %{status_code: 200, body: resp_body}} ->
        {:ok, Jason.decode!(resp_body)}
      {:ok, %{status_code: code}} ->
        {:error, "HTTP #{code}"}
      {:error, reason} ->
        {:error, reason}
    end
  end
end

Error Handling and Retry in Elixir

Elixir's pattern matching makes explicit error handling natural and readable. The with construct chains multiple operations and handles the first error that occurs, reducing the nesting depth of nested case expressions. For retry logic, implement a simple recursive function that decrements a retry counter and sleeps with Process.sleep between attempts. Differentiate between retryable errors such as 503 Service Unavailable and connection timeouts versus non-retryable errors such as 400 Bad Request and 401 Unauthorized. Oban's built-in retry logic with exponential backoff handles this automatically for background jobs, so explicit retry logic is primarily needed for synchronous screenshot calls in request handlers where Oban is not available. For Phoenix applications, a reasonable timeout for synchronous screenshot proxy endpoints is 30 seconds, which accommodates most pages including those with dynamic JavaScript rendering, while preventing request handlers from hanging indefinitely on unreachable URLs.

Storing Screenshots with ExAws S3

ExAws provides idiomatic Elixir bindings for AWS S3 that work well for screenshot storage. Configure ExAws with your AWS credentials through environment variables or the application config, then use ExAws.S3.put_object to upload screenshot bytes directly. The upload returns an ExAws operation that you execute with ExAws.request. For SnapAPI S3-compatible storage on non-AWS providers such as ServerSpace or Cloudflare R2, configure ExAws with a custom host in the application config. Store the S3 key alongside the screenshot record in your Ecto schema to enable retrieval and deletion. For monitoring systems that accumulate many screenshots over time, set S3 lifecycle policies to automatically transition old screenshots to cheaper storage tiers or delete them after a configured retention period, keeping storage costs manageable as screenshot volume grows.

Getting Started with SnapAPI in Elixir

Register for a free API key at snapapi.pics/register, add HTTPoison to your mix.exs dependencies, and store your key in config/runtime.exs as config :myapp, snapapi_key: System.get_env("SNAPAPI_KEY"). Run mix deps.get, set the SNAPAPI_KEY environment variable, and you are ready to make your first screenshot call. The free tier at 200 requests per month gives you sufficient quota to evaluate every SnapAPI feature against your actual Phoenix application before committing to a paid plan. SnapAPI returns standard HTTP status codes and JSON error bodies, making debugging straightforward in iex with IO.inspect on the full response. The Elixir ecosystem's excellent documentation culture means integrating a new HTTP API follows well-documented patterns, and the examples in this guide provide a working starting point for all major use cases in Phoenix and plain Elixir applications.