# screenshotfreeapi
[](https://crates.io/crates/screenshotfreeapi)
[](https://docs.rs/screenshotfreeapi)
[](LICENSE)
[](https://github.com/chinkauchenna2021/screenshotfreeapi-rs/actions)
Official Rust client for the **[ScreenshotFreeAPI](https://screenshotfreeapi.com)** — 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
```toml
[dependencies]
screenshotfreeapi = "1"
tokio = { version = "1", features = ["full"] }
```
---
## Quick start
```rust
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 cloning** — `ScreenshotFreeAPIClient` is `Clone` (Arc-backed)
---
## Full API reference
### Client construction
```rust
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`
```rust
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)
```rust
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)
```rust
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
```rust
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
```rust
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`
```rust
// 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`).
```rust
// 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.
```rust
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`
```rust
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`
```rust
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>`.
```rust
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
| `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.
```rust
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
```rust
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:
```rust
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`:
```rust
// (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](LICENSE).