Generate screenshots, PDFs, and scrape web content from your Rails application — one HTTP call, no Puppeteer, no browser infrastructure to manage.
Get Free API KeyRails ships with Ruby's standard library, so you can call SnapAPI with zero additional gems:
require 'net/http'
require 'uri'
require 'json'
class ScreenshotService
API_KEY = ENV['SNAPAPI_KEY']
BASE_URL = 'https://api.snapapi.pics/v1/screenshot'
def self.capture(url, format: 'png', width: 1280, full_page: false)
uri = URI(BASE_URL)
uri.query = URI.encode_www_form(
access_key: API_KEY,
url: url,
format: format,
width: width,
full_page: full_page
)
response = Net::HTTP.get_response(uri)
raise "SnapAPI error: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
response.body
end
end
# In a controller:
class ScreenshotsController < ApplicationController
def create
png = ScreenshotService.capture(params[:url])
send_data png, type: 'image/png', disposition: 'inline'
end
end
If you prefer a gem, both Faraday and HTTParty integrate cleanly:
# With HTTParty
class SnapAPI
include HTTParty
base_uri 'https://api.snapapi.pics'
def screenshot(url, opts = {})
self.class.get('/v1/screenshot', query: {
access_key: ENV['SNAPAPI_KEY'],
url: url,
format: opts.fetch(:format, 'png'),
width: opts.fetch(:width, 1280),
full_page: opts.fetch(:full_page, false),
delay: opts.fetch(:delay, 0)
})
end
end
Screenshots can be slow — never block your web request. Offload them to Sidekiq:
class ScreenshotJob
include Sidekiq::Job
sidekiq_options retry: 3, queue: :screenshots
def perform(record_id, target_url)
png = ScreenshotService.capture(target_url, format: 'png', full_page: true)
# Store to Active Storage
record = Website.find(record_id)
record.screenshot.attach(
io: StringIO.new(png),
filename: "screenshot-#{Date.today}.png",
content_type: 'image/png'
)
record.update!(screenshotted_at: Time.current)
end
end
# Enqueue from controller
ScreenshotJob.perform_async(@website.id, @website.url)
class PdfController < ApplicationController
def invoice
invoice_url = invoice_url(@invoice) # your named route
pdf_bytes = ScreenshotService.capture(
invoice_url,
format: 'pdf',
full_page: true
)
send_data pdf_bytes,
filename: "invoice-#{@invoice.number}.pdf",
type: 'application/pdf',
disposition: 'attachment'
end
end
Rails Active Storage makes it trivial to attach screenshots to any model and serve them via S3 or GCS:
# app/models/page_preview.rb
class PagePreview < ApplicationRecord
has_one_attached :image
def capture!(url)
png = ScreenshotService.capture(url)
image.attach(io: StringIO.new(png), filename: 'preview.png', content_type: 'image/png')
save!
end
end
def cached_screenshot(url, ttl: 1.hour)
Rails.cache.fetch("snap:#{Digest::MD5.hexdigest(url)}", expires_in: ttl) do
ScreenshotService.capture(url)
end
end
Store your API key in Rails credentials or environment variables. Never commit it to version control:
# config/credentials.yml.enc (via rails credentials:edit)
snapapi:
key: your_api_key_here
# Access it:
ENV['SNAPAPI_KEY'] = Rails.application.credentials.snapapi[:key]
Free tier: 200 captures/month. Paid plans from $19/month (5K) to $79/month (50K). All plans include screenshots, PDFs, scraping, and extraction. No per-seat pricing — one key, unlimited team members.
Ruby on Rails has always prioritized developer happiness and convention over configuration. SnapAPI follows the same philosophy for browser automation: one service, one REST call, zero configuration. No gem conflicts, no native extension compilation, no Puppeteer binary to manage. Your Rails app stays clean and deployable anywhere.
Historically Rails developers used wkhtmltopdf for PDF generation. It's a native binary that converts HTML to PDF, but it doesn't run JavaScript and struggles with modern CSS. Puppeteer via node-rails bridges are another option but add Node.js as a dependency and complicate your deployment pipeline. SnapAPI eliminates all of that: one HTTP call from Net::HTTP, Faraday, or HTTParty returns a pixel-perfect screenshot or PDF generated by a real browser with full JavaScript and modern CSS support.
A common Rails use case is attaching website thumbnails to notification emails. Here's how to integrate SnapAPI with ActionMailer:
class WebsiteMailer < ApplicationMailer
def weekly_report(user)
@user = user
@sites = user.monitored_sites
@sites.each do |site|
png = ScreenshotService.capture(site.url)
attachments.inline["#{site.domain}.png"] = {
mime_type: 'image/png',
content: png
}
end
mail(to: user.email, subject: 'Your Weekly Site Report')
end
end
Stub SnapAPI calls in your test suite to avoid real HTTP requests:
RSpec.describe ScreenshotService do
describe '.capture' do
it 'returns PNG bytes for a valid URL' do
stub_request(:get, /api\.snapapi\.pics/)
.to_return(status: 200, body: File.read('spec/fixtures/sample.png'), headers: { 'Content-Type' => 'image/png' })
result = ScreenshotService.capture('https://example.com')
expect(result).to be_a(String)
expect(result.length).to be > 0
end
end
end
Wrap SnapAPI calls in a retry block for production resilience:
def self.capture_with_retry(url, retries: 3)
attempts = 0
begin
attempts += 1
capture(url)
rescue => e
retry if attempts < retries
Rails.logger.error "SnapAPI failed after #{retries} attempts: #{e.message}"
nil
end
end
If you're building a SaaS on Rails that exposes screenshot functionality to users, track usage per account to enforce plan limits before hitting SnapAPI:
class ScreenshotQuotaService
MAX_FREE = 50
def self.can_capture?(account)
count = account.screenshots.where('created_at > ?', 1.month.ago).count
count < (account.paid? ? Float::INFINITY : MAX_FREE)
end
def self.capture_for_account!(account, url)
raise QuotaExceededError unless can_capture?(account)
png = ScreenshotService.capture(url)
account.screenshots.create!(url: url, captured_at: Time.current)
png
end
end
Many Rails apps power blogs or content sites. Use SnapAPI to auto-generate OG images for every post from an HTML template route:
# routes.rb
get '/og/:id', to: 'og_images#show', as: :og_image
# og_images_controller.rb
class OgImagesController < ApplicationController
layout false # Render a minimal HTML template
def show
@post = Post.find(params[:id])
# Serve as rendered HTML — SnapAPI will screenshot this route
end
end
# In your meta tags:
# <meta property="og:image" content="https://api.snapapi.pics/v1/screenshot?url=...og/42..." />
Since SnapAPI requires no native binaries in your app, your Gemfile stays unchanged and your dyno size can stay small. No buildpacks needed. The only change required is adding SNAPAPI_KEY to your environment variables in the Heroku dashboard or Render service settings.
SnapAPI offers a free tier with 200 captures per month — enough to prototype and launch. The Starter plan at 19 dollars per month gives 5,000 captures. The Growth plan at 79 dollars per month gives 50,000. All plans include screenshots, PDFs, full-page capture, scraping, and extraction. No per-seat pricing; one API key for the whole team.
Can I use SnapAPI with Rails API mode? Yes. Rails API mode strips out the view layer but leaves controllers and Active Job intact. ScreenshotService and ScreenshotJob work identically. You will typically return the screenshot URL (if storing to S3) or the binary bytes via send_data from your API endpoint.
Does it work with Hotwire and Turbo? Absolutely. SnapAPI generates screenshots from URLs, so it doesn't matter whether your pages use Hotwire, React, or plain HTML. The screenshot is taken from a real Chromium browser that renders the full JavaScript and CSS of your page, including any Stimulus controllers or Turbo frames that run on load.
What about authentication for screenshotting internal pages? For screenshotting pages behind Devise or any cookie-based auth, the recommended pattern is to generate a short-lived signed URL or token using Rails' built-in MessageVerifier or a JWT. Create a temporary bypass route that accepts this token, renders the page, and let SnapAPI screenshot it before the token expires. This keeps your main auth intact while allowing SnapAPI access to the specific page.
Can I run SnapAPI calls in a Rails engine or mountable app? Yes. ScreenshotService is a plain Ruby class with no Rails dependencies beyond ENV. It can live inside any Rails engine, mountable app, or concern. Sidekiq jobs work identically inside engines.
Is there a Rails gem for SnapAPI? Not yet, but the JavaScript SDK at github.com/Sleywill/snapapi-js can be called from Rails via execjs or a Node.js microservice if you prefer SDK-style calls over raw Net::HTTP. Most Rails developers find the raw HTTP approach simpler and more idiomatic.
How do I handle timeouts? Set Net::HTTP's read_timeout and open_timeout to 30 seconds each to accommodate SnapAPI's maximum job duration. For Faraday, set timeout: 30 in the connection builder. Sidekiq jobs can use sidekiq_options timeout: 60 to handle any edge cases on complex pages.
What image formats are supported? PNG, JPEG, WebP, and PDF. Pass format: 'webp' for smaller file sizes on photography-heavy pages, format: 'jpeg' for maximum compatibility, or format: 'pdf' for document generation. All formats support full_page: true for capturing the entire scrollable height.
How does SnapAPI handle cookies and sessions? You can pass cookies and custom request headers through the API. For authenticated pages, generate a time-limited token as described above. SnapAPI's browser creates a fresh browser context per request with no residual cookies from previous requests, ensuring clean and consistent screenshots every time.
Sign up for a free SnapAPI account at snapapi.pics. No credit card required. 200 captures per month on the free tier. Add SNAPAPI_KEY to your Rails credentials or environment variables, drop ScreenshotService into your app/services directory, and you're ready to capture your first screenshot in under five minutes.
Questions? Reach out at hello@snapapi.pics or visit the docs at snapapi.pics. The team responds within 24 hours and can help with Rails-specific integration questions, custom plan sizing for high-volume apps, and enterprise requirements including SLAs and dedicated infrastructure.