ScreenshotFreeAPI — Ruby SDK
Official Ruby client for ScreenshotFreeAPI — screenshot-as-a-service.
- Capture web pages, mobile app store listings, and HTML strings
- Async job polling with configurable timeout
- Webhook signature verification (HMAC-SHA256)
- Billing, workspace, and monitor management
- Zero runtime dependencies — stdlib only (
net/http,json,openssl) - Ruby 2.7+ compatible
Installation
RubyGems
gem install screenshotfreeapi
Bundler
Add to your Gemfile:
gem "screenshotfreeapi", "~> 1.0"
Then run:
bundle install
Quick Start
require "screenshotfreeapi"
client = ScreenshotFreeAPI.new(api_key: ENV["SCREENSHOTFREEAPI_KEY"])
# Capture a URL and wait for the result (blocks until complete)
result = client.capture("https://stripe.com/pricing")
puts result["screenshots"].first["url"]
# => "https://s3.amazonaws.com/...?X-Amz-Expires=900"
All Screenshot Types
Web Screenshot
client = ScreenshotFreeAPI.new(api_key: ENV["SCREENSHOTFREEAPI_KEY"])
# Enqueue (returns immediately with a job receipt)
job = client.screenshots.web(
url: "https://stripe.com/pricing",
format: "png", # "png" | "jpeg" | "webp" | "pdf"
full_page: true, # capture the full scrollable page
dimensions: { width: 1440, height: 900 },
block_ads: true, # STARTER+
accept_cookies: true, # STARTER+
webhook_url: "https://yourapp.com/webhooks/screenshot-events"
)
puts job["jobId"] # => "clxyz123"
puts job["statusUrl"] # => "/jobs/clxyz123/status"
# --- AI-targeted element capture ---
job = client.screenshots.web(
url: "https://stripe.com/pricing",
description: "the pricing comparison table" # triggers Claude vision path
)
# --- CSS selector targeting ---
job = client.screenshots.web(
url: "https://github.com",
element: ".hero-section"
)
# --- Enqueue and block until done ---
result = client.screenshots.web_and_wait(
url: "https://example.com",
format: "png"
)
puts result["screenshots"].first["url"]
Mobile App Screenshot
# Store listing screenshots
job = client.screenshots.mobile(
app_name: "Instagram",
platform: "both", # "ios" | "android" | "both"
bundle_id: "com.instagram.android",
include_store_listing: true,
device_emulation: "iPhone 12"
)
# Block until complete
result = client.screenshots.mobile_and_wait(
app_name: "Spotify",
platform: "ios"
)
puts result["screenshots"].map { |s| s["url"] }
HTML String Screenshot
html_content = <<~HTML
<!DOCTYPE html>
<html>
<body style="font-family: sans-serif; padding: 40px;">
<h1>Invoice #1042</h1>
<p>Total: <strong>$149.00</strong></p>
</body>
</html>
HTML
result = client.screenshots.html_and_wait(
html: html_content,
format: "png",
full_page: true,
dimensions: { width: 800, height: 600 }
)
puts result["screenshots"].first["url"]
Manual Polling
If you want to enqueue a job and check on it yourself:
# 1. Enqueue
job = client.screenshots.web(url: "https://example.com")
job_id = job["jobId"]
# 2. Poll manually
loop do
status = client.jobs.status(job_id)
puts "#{status["status"]} — #{status["progress"]}%"
break if status["status"] == "completed"
raise "Job failed: #{status["error"]}" if status["status"] == "failed"
sleep 2
end
# 3. Fetch result
result = client.jobs.result(job_id)
puts result["screenshots"].first["url"]
Or use the built-in wait method with a progress callback:
result = client.screenshots.wait(job_id, poll_interval: 3, timeout: 180) do |status|
puts "[#{status["status"]}] #{status["progress"]}%"
end
Authentication
# Register a new account (returns apiKey shown once only)
account = client.auth.register(
email: "you@example.com",
password: "securepassword123!",
name: "Your Name"
)
puts account["apiKey"] # store this securely
# Get a management JWT (needed for billing / workspaces / monitors)
token_data = client.auth.token(
email: "you@example.com",
password: "securepassword123!"
)
jwt = token_data["token"]
# Refresh an expiring JWT
new_token = client.auth.refresh(refresh_token: "your_refresh_token")
Error Handling
require "screenshotfreeapi"
client = ScreenshotFreeAPI.new(api_key: ENV["SCREENSHOTFREEAPI_KEY"])
begin
result = client.capture("https://example.com")
rescue ScreenshotFreeAPI::AuthenticationError => e
# 401 — API key is missing or invalid
puts "Authentication failed: #{e.}"
rescue ScreenshotFreeAPI::PaymentRequiredError => e
# 402 — Subscription cancelled or payment overdue
puts "Payment required: #{e.}"
rescue ScreenshotFreeAPI::ForbiddenError => e
# 403 — API key revoked or account suspended
puts "Forbidden: #{e.}"
rescue ScreenshotFreeAPI::NotFoundError => e
# 404 — Job not found or not owned by this API key
puts "Not found: #{e.}"
rescue ScreenshotFreeAPI::ValidationError => e
# 400 — Request body failed server-side validation
puts "Validation error: #{e.}"
puts "Details: #{e.details.inspect}" if e.details
rescue ScreenshotFreeAPI::RateLimitError => e
# 429 per-minute rate limit
puts "Rate limited. Retry after #{e.retry_after}s"
sleep(e.retry_after || 60)
retry
rescue ScreenshotFreeAPI::QuotaExceededError => e
# 429 monthly quota exhausted
puts "Monthly quota exceeded: #{e.}"
rescue ScreenshotFreeAPI::JobFailedError => e
# Job transitioned to "failed" during polling
puts "Job #{e.job_id} failed: #{e.reason}"
rescue ScreenshotFreeAPI::JobTimeoutError => e
# Job did not complete within the polling timeout
puts "Job #{e.job_id} timed out after #{e.timeout_seconds}s"
rescue ScreenshotFreeAPI::ScreenshotFreeAPIError => e
# Catch-all for unexpected HTTP errors (5xx, etc.)
puts "API error #{e.status_code}: #{e.}"
end
Billing
# Get JWT first
jwt = client.auth.token(email: "you@example.com", password: "secret")["token"]
# List available plans (no auth needed)
plans = client.billing.plans
plans.each { |p| puts "#{p["name"]}: $#{p["price"]}/mo" }
# Current plan and usage
plan = client.billing.plan(jwt: jwt)
usage = client.billing.usage(jwt: jwt)
puts "Plan: #{plan["plan"]}, Used: #{plan["screenshotsUsed"]}"
# Upgrade
checkout = client.billing.upgrade(jwt: jwt, plan: "GROWTH")
puts "Complete payment at: #{checkout["checkoutUrl"]}"
# Verify payment after redirect
verified = client.billing.verify(jwt: jwt, transaction_ref: checkout["transactionRef"])
puts verified["message"]
# Cancel
client.billing.cancel(jwt: jwt)
Workspaces
jwt = client.auth.token(email: "you@example.com", password: "secret")["token"]
# Create
workspace = client.workspaces.create(jwt: jwt, name: "Acme Corp")
# List
workspaces = client.workspaces.list(jwt: jwt)
# Get details (includes members)
ws = client.workspaces.get(jwt: jwt, workspace_id: workspace["id"])
# Invite a member
client.workspaces.invite(
jwt: jwt,
workspace_id: workspace["id"],
email: "colleague@example.com",
role: "member" # "admin" | "member" | "viewer"
)
# Change role
client.workspaces.update_member(
jwt: jwt,
workspace_id: workspace["id"],
user_id: "usr_abc",
role: "admin"
)
# Remove member
client.workspaces.remove_member(
jwt: jwt,
workspace_id: workspace["id"],
user_id: "usr_abc"
)
App Monitors
jwt = client.auth.token(email: "you@example.com", password: "secret")["token"]
# Create a monitor — runs daily at 9 AM UTC
monitor = client.monitors.create(
jwt: jwt,
app_name: "Spotify",
platform: "both",
schedule: "0 9 * * *",
webhook_url: "https://yourapp.com/webhooks/monitor"
)
# List
client.monitors.list(jwt: jwt)
# Get
client.monitors.get(jwt: jwt, monitor_id: monitor["id"])
# Run history (last 20 entries)
history = client.monitors.history(jwt: jwt, monitor_id: monitor["id"], limit: 20)
# Delete
client.monitors.delete(jwt: jwt, monitor_id: monitor["id"])
Zapier Integration
# Subscribe (register a webhook for a Zapier trigger)
sub = client.integrations.zapier_subscribe(
target_url: "https://hooks.zapier.com/hooks/catch/...",
event: "screenshot.completed"
)
# Sample payload for Zap editor
sample = client.integrations.zapier_triggers(event: "screenshot.completed")
# Unsubscribe
client.integrations.zapier_unsubscribe(subscription_id: sub["id"])
Webhook Verification
ScreenshotFreeAPI signs all webhook payloads using HMAC-SHA256. Always verify the signature before processing the event.
# Standalone verification
require "screenshotfreeapi"
raw_body = request.body.read # raw bytes — do NOT parse yet
signature = request.headers["X-ScreenshotFree-Signature"]
secret = ENV["SCREENSHOTFREEAPI_WEBHOOK_SECRET"]
unless ScreenshotFreeAPI::Webhooks.verify_signature(raw_body, signature, secret)
render plain: "Invalid signature", status: :forbidden
return
end
event = JSON.parse(raw_body)
puts event["status"] # "completed" | "failed"
# Or use construct_event (raises ArgumentError on bad signature):
begin
event = ScreenshotFreeAPI::Webhooks.construct_event(raw_body, signature, secret)
rescue ArgumentError => e
render plain: e., status: :forbidden
return
end
Rails Integration
Create an initializer config/initializers/screenshotfreeapi.rb:
require "screenshotfreeapi"
SCREENSHOTFREEAPI_CLIENT = ScreenshotFreeAPI.new(
api_key: Rails.application.credentials.screenshotfreeapi_key!,
timeout: 45,
max_retries: 3
)
Then use SCREENSHOTFREEAPI_CLIENT anywhere in your application:
class ScreenshotJob < ApplicationJob
def perform(url)
result = SCREENSHOTFREEAPI_CLIENT.capture(url, format: "png", full_page: true)
url = result["screenshots"].first["url"]
Rails.logger.info "Screenshot ready: #{url}"
end
end
Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
api_key |
String | required | Your ScreenshotFreeAPI key (sfa_...) |
base_url |
String | https://api.screenshotfreeapi.com |
Override for staging / local dev |
timeout |
Integer | 30 |
HTTP open/read timeout (seconds) |
max_retries |
Integer | 3 |
Retries on 429 and 5xx errors |
License
MIT — see LICENSE file.