screenshotfreeapi 1.0.0

Official Rust client for ScreenshotFreeAPI — Screenshot-as-a-Service
Documentation

screenshotfreeapi

Crates.io docs.rs License: MIT CI

Official Rust client for the ScreenshotFreeAPI — Screenshot-as-a-Service for developers.

Capture pixel-perfect screenshots of any website, mobile app listing, or raw HTML in under 10 lines of Rust.


Install

[dependencies]

screenshotfreeapi = "1"

tokio = { version = "1", features = ["full"] }


Quick start

use screenshotfreeapi::ScreenshotFreeAPIClient;

#[tokio::main]
async fn main() -> screenshotfreeapi::Result<()> {
    let client = ScreenshotFreeAPIClient::new("sfa_your_api_key");
    let result = client.capture("https://stripe.com").await?;
    println!("{}", result.screenshots[0].url); // presigned S3 URL, 15-min TTL
    Ok(())
}

Features

  • Web screenshots — any public URL, with optional AI element targeting
  • AI targeting — describe what you want in plain English; Claude finds it
  • PDF generation — convert any URL to a print-quality PDF
  • HTML rendering — screenshot raw HTML strings (email templates, certificates)
  • Mobile app screenshots — App Store and Google Play listings
  • Full async/await — built on tokio + reqwest
  • Automatic retries — 3 attempts with exponential back-off (1 s / 2 s / 4 s)
  • Rate-limit handling — honours Retry-After header automatically
  • Webhook verification — HMAC-SHA256 in constant time
  • Progress callbacks — hook into polling with WaitOptions.on_progress
  • Zero-cost cloningScreenshotFreeAPIClient is Clone (Arc-backed)

Full API reference

Client construction

use screenshotfreeapi::ScreenshotFreeAPIClient;

// Production
let client = ScreenshotFreeAPIClient::new("sfa_your_api_key");

// Custom base URL (development / staging)
let client = ScreenshotFreeAPIClient::with_base_url("sfa_test_key", "http://localhost:3000");

Auth — client.auth

use screenshotfreeapi::{RegisterRequest, TokenRequest, RefreshRequest};

// Create an account (API key returned once — store it immediately)
let reg = client.auth.register(RegisterRequest {
    email: "you@example.com".into(),
    password: "secret123!".into(),
    name: "Your Name".into(),
}).await?;
println!("API key (save this!): {}", reg.api_key);

// Obtain a management JWT for billing / workspaces / monitors
let token = client.auth.token(TokenRequest {
    email: "you@example.com".into(),
    password: "secret123!".into(),
}).await?;
let jwt = token.access_token;

// Refresh an access token
let refreshed = client.auth.refresh(RefreshRequest {
    refresh_token: token.refresh_token,
}).await?;

Screenshots — client.screenshots

Web screenshot (non-blocking enqueue)

use screenshotfreeapi::WebScreenshotOptions;

let enq = client.screenshots.capture_web(WebScreenshotOptions {
    url: "https://stripe.com/pricing".into(),
    description: Some("the pricing comparison table".into()),
    format: Some("png".into()),
    ..Default::default()
}).await?;
println!("Job enqueued: {}", enq.job_id);

Web screenshot (enqueue + wait for result)

use screenshotfreeapi::{WebScreenshotOptions, WaitOptions};

let result = client.screenshots.capture_web_and_wait(
    WebScreenshotOptions {
        url: "https://github.com".into(),
        full_page: Some(true),
        ..Default::default()
    },
    WaitOptions::default(),
).await?;
println!("{}", result.screenshots[0].url);

Mobile app screenshot

use screenshotfreeapi::MobileScreenshotOptions;

let result = client.screenshots.capture_mobile_and_wait(
    MobileScreenshotOptions {
        app_name: Some("Instagram".into()),
        platform: Some("both".into()),
        include_store_listing: Some(true),
        ..Default::default()
    },
    WaitOptions::default(),
).await?;

HTML string rendering

use screenshotfreeapi::HtmlScreenshotOptions;

let result = client.screenshots.capture_html_and_wait(
    HtmlScreenshotOptions {
        html: "<h1 style='color:red'>Hello World</h1>".into(),
        format: Some("png".into()),
        ..Default::default()
    },
    WaitOptions::default(),
).await?;

Jobs — client.jobs

// Poll status
let status = client.jobs.status("clxyz123").await?;
println!("{}{}%", status.status, status.progress.unwrap_or(0));

// Fetch result (only valid once status == "completed")
let result = client.jobs.result("clxyz123").await?;

Billing — client.billing

All billing methods require a management JWT (jwt: &str).

// List plans (public, no JWT required)
let plans = client.billing.plans().await?;

// Current subscription
let plan = client.billing.plan(&jwt).await?;

// 30-day usage history
let usage = client.billing.usage(&jwt).await?;
println!("{}/{} screenshots used", usage.screenshots_used, usage.screenshots_limit);

// Upgrade (returns Flutterwave checkout URL)
use screenshotfreeapi::UpgradeRequest;
let upgrade = client.billing.upgrade(UpgradeRequest { plan_id: "growth".into() }, &jwt).await?;
if let Some(url) = upgrade.redirect_url {
    println!("Redirect user to: {url}");
}

// Verify payment after checkout redirect
let verified = client.billing.verify(&jwt).await?;

// Cancel subscription
let cancelled = client.billing.cancel(&jwt).await?;

Workspaces — client.workspaces

All workspace methods require a JWT.

use screenshotfreeapi::{CreateWorkspaceRequest, InviteRequest, UpdateRoleRequest};

// Create
let ws = client.workspaces.create(CreateWorkspaceRequest { name: "My Team".into() }, &jwt).await?;

// List
let workspaces = client.workspaces.list(&jwt).await?;

// Detail (includes members)
let detail = client.workspaces.get(&ws.id, &jwt).await?;

// Invite
client.workspaces.invite(&ws.id, InviteRequest {
    email: "alice@example.com".into(),
    role: "member".into(),
}, &jwt).await?;

// Change role
client.workspaces.update_member_role(&ws.id, "user_id_here",
    UpdateRoleRequest { role: "admin".into() }, &jwt).await?;

// Remove member
client.workspaces.remove_member(&ws.id, "user_id_here", &jwt).await?;

Monitors — client.monitors

use screenshotfreeapi::CreateMonitorRequest;

// Create a daily monitor
let monitor = client.monitors.create(CreateMonitorRequest {
    app_id: "com.instagram.android".into(),
    platform: "android".into(),
    schedule: "0 9 * * *".into(), // 09:00 UTC daily
    webhook_url: Some("https://your-app.com/webhook".into()),
    diff_threshold: None,
    label: Some("Instagram".into()),
}, &jwt).await?;

// List monitors
let monitors = client.monitors.list(&jwt).await?;

// Get one
let m = client.monitors.get(&monitor.id, &jwt).await?;

// History
let history = client.monitors.history(&monitor.id, &jwt).await?;

// Delete
client.monitors.delete(&monitor.id, &jwt).await?;

Integrations — client.integrations

use screenshotfreeapi::ZapierSubscribeRequest;

// Subscribe
let sub = client.integrations.zapier_subscribe(ZapierSubscribeRequest {
    trigger_event: "job.completed".into(),
    target_url: "https://hooks.zapier.com/hooks/catch/...".into(),
}).await?;

// Sample payload (for Zapier setup UI)
let sample = client.integrations.zapier_trigger_sample("job.completed").await?;

// Unsubscribe
client.integrations.zapier_unsubscribe(&sub.id).await?;

Error handling

All methods return screenshotfreeapi::Result<T> — an alias for std::result::Result<T, ScreenshotFreeAPIError>.

use screenshotfreeapi::{ScreenshotFreeAPIClient, ScreenshotFreeAPIError};

match client.capture("https://example.com").await {
    Ok(result) => println!("{}", result.screenshots[0].url),

    Err(ScreenshotFreeAPIError::Authentication { message }) => {
        eprintln!("Bad API key: {message}");
    }
    Err(ScreenshotFreeAPIError::RateLimit { retry_after_seconds }) => {
        eprintln!("Rate limited — retry after {retry_after_seconds}s");
    }
    Err(ScreenshotFreeAPIError::QuotaExceeded) => {
        eprintln!("Monthly quota exhausted — upgrade your plan");
    }
    Err(ScreenshotFreeAPIError::JobFailed { job_id, reason }) => {
        eprintln!("Job {job_id} failed: {reason}");
    }
    Err(ScreenshotFreeAPIError::JobTimeout { timeout_ms }) => {
        eprintln!("Job did not finish within {timeout_ms}ms");
    }
    Err(e) => eprintln!("Unexpected error: {e}"),
}

Error variants

Variant HTTP status When
Authentication 401 Invalid or missing API key
Forbidden 403 Key revoked, account suspended
NotFound 404 Job not found or not owned by this key
Validation 400 Request body failed validation
RateLimit 429 Per-minute rate limit hit
QuotaExceeded 429 Monthly screenshot quota exhausted
PaymentRequired 402 Subscription cancelled or overdue
JobFailed Job reached failed terminal state
JobTimeout Polling exceeded WaitOptions.timeout_ms
InvalidSignature Webhook HMAC verification failed
Unexpected other Any other HTTP status
Http reqwest transport error

Webhook verification

Every webhook POST from the API includes an X-ScreenshotFree-Signature header containing HMAC-SHA256(body, secret) as a lowercase hex string.

use screenshotfreeapi::verify_webhook_signature;

// In your HTTP handler (pseudocode):
let body: &[u8] = request.body_bytes();
let signature: &str = request.header("X-ScreenshotFree-Signature");
let secret: &str = "your_webhook_signing_secret";

verify_webhook_signature(body, signature, secret)?;
// Signature matched — safe to parse and process the event

The comparison is performed in constant time to prevent timing attacks.


WaitOptions with progress callback

use screenshotfreeapi::{WaitOptions, WebScreenshotOptions};

let result = client.screenshots.capture_web_and_wait(
    WebScreenshotOptions {
        url: "https://stripe.com".into(),
        ..Default::default()
    },
    WaitOptions {
        interval_ms: 1_500,
        timeout_ms: 90_000,
        on_progress: Some(Box::new(|status| {
            println!(
                "[{}] progress: {}%",
                status.job_id,
                status.progress.unwrap_or(0)
            );
        })),
    },
).await?;

Concurrent captures

Use tokio::join! for a fixed number of concurrent captures:

use screenshotfreeapi::{ScreenshotFreeAPIClient, WebScreenshotOptions, WaitOptions};

let client = ScreenshotFreeAPIClient::new("sfa_your_api_key");
let c = client.clone();

let (r1, r2, r3) = tokio::join!(
    client.capture("https://stripe.com"),
    client.capture("https://github.com"),
    c.capture("https://vercel.com"),
);

println!("{}", r1?.screenshots[0].url);
println!("{}", r2?.screenshots[0].url);
println!("{}", r3?.screenshots[0].url);

For a dynamic list, collect futures and await them with futures::future::join_all:

// (add `futures = "0.3"` to Cargo.toml)
use futures::future::join_all;

let urls = vec!["https://a.com", "https://b.com", "https://c.com"];
let client = ScreenshotFreeAPIClient::new("sfa_your_api_key");

let futures: Vec<_> = urls.iter()
    .map(|url| client.capture(url))
    .collect();

let results = join_all(futures).await;
for result in results {
    println!("{}", result?.screenshots[0].url);
}

License

MIT — see LICENSE.