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