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)
 = client.auth.register(
  email:    "you@example.com",
  password: "securepassword123!",
  name:     "Your Name"
)
puts ["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.message}"

rescue ScreenshotFreeAPI::PaymentRequiredError => e
  # 402 — Subscription cancelled or payment overdue
  puts "Payment required: #{e.message}"

rescue ScreenshotFreeAPI::ForbiddenError => e
  # 403 — API key revoked or account suspended
  puts "Forbidden: #{e.message}"

rescue ScreenshotFreeAPI::NotFoundError => e
  # 404 — Job not found or not owned by this API key
  puts "Not found: #{e.message}"

rescue ScreenshotFreeAPI::ValidationError => e
  # 400 — Request body failed server-side validation
  puts "Validation error: #{e.message}"
  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.message}"

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.message}"
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.message, 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.