screenshotfreeapi 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +442 -0
- data/lib/screenshotfreeapi/client.rb +67 -0
- data/lib/screenshotfreeapi/errors.rb +89 -0
- data/lib/screenshotfreeapi/http_client.rb +180 -0
- data/lib/screenshotfreeapi/resources/auth.rb +58 -0
- data/lib/screenshotfreeapi/resources/billing.rb +77 -0
- data/lib/screenshotfreeapi/resources/integrations.rb +52 -0
- data/lib/screenshotfreeapi/resources/jobs.rb +59 -0
- data/lib/screenshotfreeapi/resources/monitors.rb +88 -0
- data/lib/screenshotfreeapi/resources/screenshots.rb +166 -0
- data/lib/screenshotfreeapi/resources/workspaces.rb +96 -0
- data/lib/screenshotfreeapi/version.rb +5 -0
- data/lib/screenshotfreeapi/webhooks.rb +78 -0
- data/lib/screenshotfreeapi.rb +47 -0
- metadata +64 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a74c7308a606f4b35000f23f5acd86ddbe75e8b924babc380474d75d758a1d32
|
|
4
|
+
data.tar.gz: fa76ec1108119c0279dde70d5498b5f818dc5aa14fb446e173ae64b2e6bf8943
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 686cb5b19cb4b36f59d180164c355a2474d690d3e1735e48a9be0fa708e00ea6c1a7afe37e387f7c5a4faaa0daeb995194865932f043a124c4fc7896a8329274
|
|
7
|
+
data.tar.gz: 5e6951a5d5310c3eb0fa78b05b156140461ca7627da59e03fae588d3354a65283569df390f15564dc35dcd64b1e17c6154131cc2ae9448226d5247cfd33d75f6
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScreenshotFreeAPI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# ScreenshotFreeAPI — Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby client for [ScreenshotFreeAPI](https://api.screenshotfreeapi.com) — screenshot-as-a-service.
|
|
4
|
+
|
|
5
|
+
- Capture web pages, mobile app store listings, and HTML strings
|
|
6
|
+
- Async job polling with configurable timeout
|
|
7
|
+
- Webhook signature verification (HMAC-SHA256)
|
|
8
|
+
- Billing, workspace, and monitor management
|
|
9
|
+
- Zero runtime dependencies — stdlib only (`net/http`, `json`, `openssl`)
|
|
10
|
+
- Ruby 2.7+ compatible
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### RubyGems
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
gem install screenshotfreeapi
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Bundler
|
|
23
|
+
|
|
24
|
+
Add to your `Gemfile`:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem "screenshotfreeapi", "~> 1.0"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then run:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
require "screenshotfreeapi"
|
|
42
|
+
|
|
43
|
+
client = ScreenshotFreeAPI.new(api_key: ENV["SCREENSHOTFREEAPI_KEY"])
|
|
44
|
+
|
|
45
|
+
# Capture a URL and wait for the result (blocks until complete)
|
|
46
|
+
result = client.capture("https://stripe.com/pricing")
|
|
47
|
+
|
|
48
|
+
puts result["screenshots"].first["url"]
|
|
49
|
+
# => "https://s3.amazonaws.com/...?X-Amz-Expires=900"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## All Screenshot Types
|
|
55
|
+
|
|
56
|
+
### Web Screenshot
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
client = ScreenshotFreeAPI.new(api_key: ENV["SCREENSHOTFREEAPI_KEY"])
|
|
60
|
+
|
|
61
|
+
# Enqueue (returns immediately with a job receipt)
|
|
62
|
+
job = client.screenshots.web(
|
|
63
|
+
url: "https://stripe.com/pricing",
|
|
64
|
+
format: "png", # "png" | "jpeg" | "webp" | "pdf"
|
|
65
|
+
full_page: true, # capture the full scrollable page
|
|
66
|
+
dimensions: { width: 1440, height: 900 },
|
|
67
|
+
block_ads: true, # STARTER+
|
|
68
|
+
accept_cookies: true, # STARTER+
|
|
69
|
+
webhook_url: "https://yourapp.com/webhooks/screenshot-events"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
puts job["jobId"] # => "clxyz123"
|
|
73
|
+
puts job["statusUrl"] # => "/jobs/clxyz123/status"
|
|
74
|
+
|
|
75
|
+
# --- AI-targeted element capture ---
|
|
76
|
+
job = client.screenshots.web(
|
|
77
|
+
url: "https://stripe.com/pricing",
|
|
78
|
+
description: "the pricing comparison table" # triggers Claude vision path
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# --- CSS selector targeting ---
|
|
82
|
+
job = client.screenshots.web(
|
|
83
|
+
url: "https://github.com",
|
|
84
|
+
element: ".hero-section"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# --- Enqueue and block until done ---
|
|
88
|
+
result = client.screenshots.web_and_wait(
|
|
89
|
+
url: "https://example.com",
|
|
90
|
+
format: "png"
|
|
91
|
+
)
|
|
92
|
+
puts result["screenshots"].first["url"]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Mobile App Screenshot
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
# Store listing screenshots
|
|
99
|
+
job = client.screenshots.mobile(
|
|
100
|
+
app_name: "Instagram",
|
|
101
|
+
platform: "both", # "ios" | "android" | "both"
|
|
102
|
+
bundle_id: "com.instagram.android",
|
|
103
|
+
include_store_listing: true,
|
|
104
|
+
device_emulation: "iPhone 12"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Block until complete
|
|
108
|
+
result = client.screenshots.mobile_and_wait(
|
|
109
|
+
app_name: "Spotify",
|
|
110
|
+
platform: "ios"
|
|
111
|
+
)
|
|
112
|
+
puts result["screenshots"].map { |s| s["url"] }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### HTML String Screenshot
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
html_content = <<~HTML
|
|
119
|
+
<!DOCTYPE html>
|
|
120
|
+
<html>
|
|
121
|
+
<body style="font-family: sans-serif; padding: 40px;">
|
|
122
|
+
<h1>Invoice #1042</h1>
|
|
123
|
+
<p>Total: <strong>$149.00</strong></p>
|
|
124
|
+
</body>
|
|
125
|
+
</html>
|
|
126
|
+
HTML
|
|
127
|
+
|
|
128
|
+
result = client.screenshots.html_and_wait(
|
|
129
|
+
html: html_content,
|
|
130
|
+
format: "png",
|
|
131
|
+
full_page: true,
|
|
132
|
+
dimensions: { width: 800, height: 600 }
|
|
133
|
+
)
|
|
134
|
+
puts result["screenshots"].first["url"]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Manual Polling
|
|
140
|
+
|
|
141
|
+
If you want to enqueue a job and check on it yourself:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
# 1. Enqueue
|
|
145
|
+
job = client.screenshots.web(url: "https://example.com")
|
|
146
|
+
job_id = job["jobId"]
|
|
147
|
+
|
|
148
|
+
# 2. Poll manually
|
|
149
|
+
loop do
|
|
150
|
+
status = client.jobs.status(job_id)
|
|
151
|
+
puts "#{status["status"]} — #{status["progress"]}%"
|
|
152
|
+
break if status["status"] == "completed"
|
|
153
|
+
raise "Job failed: #{status["error"]}" if status["status"] == "failed"
|
|
154
|
+
sleep 2
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# 3. Fetch result
|
|
158
|
+
result = client.jobs.result(job_id)
|
|
159
|
+
puts result["screenshots"].first["url"]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Or use the built-in `wait` method with a progress callback:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
result = client.screenshots.wait(job_id, poll_interval: 3, timeout: 180) do |status|
|
|
166
|
+
puts "[#{status["status"]}] #{status["progress"]}%"
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Authentication
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# Register a new account (returns apiKey shown once only)
|
|
176
|
+
account = client.auth.register(
|
|
177
|
+
email: "you@example.com",
|
|
178
|
+
password: "securepassword123!",
|
|
179
|
+
name: "Your Name"
|
|
180
|
+
)
|
|
181
|
+
puts account["apiKey"] # store this securely
|
|
182
|
+
|
|
183
|
+
# Get a management JWT (needed for billing / workspaces / monitors)
|
|
184
|
+
token_data = client.auth.token(
|
|
185
|
+
email: "you@example.com",
|
|
186
|
+
password: "securepassword123!"
|
|
187
|
+
)
|
|
188
|
+
jwt = token_data["token"]
|
|
189
|
+
|
|
190
|
+
# Refresh an expiring JWT
|
|
191
|
+
new_token = client.auth.refresh(refresh_token: "your_refresh_token")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Error Handling
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
require "screenshotfreeapi"
|
|
200
|
+
|
|
201
|
+
client = ScreenshotFreeAPI.new(api_key: ENV["SCREENSHOTFREEAPI_KEY"])
|
|
202
|
+
|
|
203
|
+
begin
|
|
204
|
+
result = client.capture("https://example.com")
|
|
205
|
+
|
|
206
|
+
rescue ScreenshotFreeAPI::AuthenticationError => e
|
|
207
|
+
# 401 — API key is missing or invalid
|
|
208
|
+
puts "Authentication failed: #{e.message}"
|
|
209
|
+
|
|
210
|
+
rescue ScreenshotFreeAPI::PaymentRequiredError => e
|
|
211
|
+
# 402 — Subscription cancelled or payment overdue
|
|
212
|
+
puts "Payment required: #{e.message}"
|
|
213
|
+
|
|
214
|
+
rescue ScreenshotFreeAPI::ForbiddenError => e
|
|
215
|
+
# 403 — API key revoked or account suspended
|
|
216
|
+
puts "Forbidden: #{e.message}"
|
|
217
|
+
|
|
218
|
+
rescue ScreenshotFreeAPI::NotFoundError => e
|
|
219
|
+
# 404 — Job not found or not owned by this API key
|
|
220
|
+
puts "Not found: #{e.message}"
|
|
221
|
+
|
|
222
|
+
rescue ScreenshotFreeAPI::ValidationError => e
|
|
223
|
+
# 400 — Request body failed server-side validation
|
|
224
|
+
puts "Validation error: #{e.message}"
|
|
225
|
+
puts "Details: #{e.details.inspect}" if e.details
|
|
226
|
+
|
|
227
|
+
rescue ScreenshotFreeAPI::RateLimitError => e
|
|
228
|
+
# 429 per-minute rate limit
|
|
229
|
+
puts "Rate limited. Retry after #{e.retry_after}s"
|
|
230
|
+
sleep(e.retry_after || 60)
|
|
231
|
+
retry
|
|
232
|
+
|
|
233
|
+
rescue ScreenshotFreeAPI::QuotaExceededError => e
|
|
234
|
+
# 429 monthly quota exhausted
|
|
235
|
+
puts "Monthly quota exceeded: #{e.message}"
|
|
236
|
+
|
|
237
|
+
rescue ScreenshotFreeAPI::JobFailedError => e
|
|
238
|
+
# Job transitioned to "failed" during polling
|
|
239
|
+
puts "Job #{e.job_id} failed: #{e.reason}"
|
|
240
|
+
|
|
241
|
+
rescue ScreenshotFreeAPI::JobTimeoutError => e
|
|
242
|
+
# Job did not complete within the polling timeout
|
|
243
|
+
puts "Job #{e.job_id} timed out after #{e.timeout_seconds}s"
|
|
244
|
+
|
|
245
|
+
rescue ScreenshotFreeAPI::ScreenshotFreeAPIError => e
|
|
246
|
+
# Catch-all for unexpected HTTP errors (5xx, etc.)
|
|
247
|
+
puts "API error #{e.status_code}: #{e.message}"
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Billing
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# Get JWT first
|
|
257
|
+
jwt = client.auth.token(email: "you@example.com", password: "secret")["token"]
|
|
258
|
+
|
|
259
|
+
# List available plans (no auth needed)
|
|
260
|
+
plans = client.billing.plans
|
|
261
|
+
plans.each { |p| puts "#{p["name"]}: $#{p["price"]}/mo" }
|
|
262
|
+
|
|
263
|
+
# Current plan and usage
|
|
264
|
+
plan = client.billing.plan(jwt: jwt)
|
|
265
|
+
usage = client.billing.usage(jwt: jwt)
|
|
266
|
+
puts "Plan: #{plan["plan"]}, Used: #{plan["screenshotsUsed"]}"
|
|
267
|
+
|
|
268
|
+
# Upgrade
|
|
269
|
+
checkout = client.billing.upgrade(jwt: jwt, plan: "GROWTH")
|
|
270
|
+
puts "Complete payment at: #{checkout["checkoutUrl"]}"
|
|
271
|
+
|
|
272
|
+
# Verify payment after redirect
|
|
273
|
+
verified = client.billing.verify(jwt: jwt, transaction_ref: checkout["transactionRef"])
|
|
274
|
+
puts verified["message"]
|
|
275
|
+
|
|
276
|
+
# Cancel
|
|
277
|
+
client.billing.cancel(jwt: jwt)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Workspaces
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
jwt = client.auth.token(email: "you@example.com", password: "secret")["token"]
|
|
286
|
+
|
|
287
|
+
# Create
|
|
288
|
+
workspace = client.workspaces.create(jwt: jwt, name: "Acme Corp")
|
|
289
|
+
|
|
290
|
+
# List
|
|
291
|
+
workspaces = client.workspaces.list(jwt: jwt)
|
|
292
|
+
|
|
293
|
+
# Get details (includes members)
|
|
294
|
+
ws = client.workspaces.get(jwt: jwt, workspace_id: workspace["id"])
|
|
295
|
+
|
|
296
|
+
# Invite a member
|
|
297
|
+
client.workspaces.invite(
|
|
298
|
+
jwt: jwt,
|
|
299
|
+
workspace_id: workspace["id"],
|
|
300
|
+
email: "colleague@example.com",
|
|
301
|
+
role: "member" # "admin" | "member" | "viewer"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Change role
|
|
305
|
+
client.workspaces.update_member(
|
|
306
|
+
jwt: jwt,
|
|
307
|
+
workspace_id: workspace["id"],
|
|
308
|
+
user_id: "usr_abc",
|
|
309
|
+
role: "admin"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Remove member
|
|
313
|
+
client.workspaces.remove_member(
|
|
314
|
+
jwt: jwt,
|
|
315
|
+
workspace_id: workspace["id"],
|
|
316
|
+
user_id: "usr_abc"
|
|
317
|
+
)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## App Monitors
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
jwt = client.auth.token(email: "you@example.com", password: "secret")["token"]
|
|
326
|
+
|
|
327
|
+
# Create a monitor — runs daily at 9 AM UTC
|
|
328
|
+
monitor = client.monitors.create(
|
|
329
|
+
jwt: jwt,
|
|
330
|
+
app_name: "Spotify",
|
|
331
|
+
platform: "both",
|
|
332
|
+
schedule: "0 9 * * *",
|
|
333
|
+
webhook_url: "https://yourapp.com/webhooks/monitor"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# List
|
|
337
|
+
client.monitors.list(jwt: jwt)
|
|
338
|
+
|
|
339
|
+
# Get
|
|
340
|
+
client.monitors.get(jwt: jwt, monitor_id: monitor["id"])
|
|
341
|
+
|
|
342
|
+
# Run history (last 20 entries)
|
|
343
|
+
history = client.monitors.history(jwt: jwt, monitor_id: monitor["id"], limit: 20)
|
|
344
|
+
|
|
345
|
+
# Delete
|
|
346
|
+
client.monitors.delete(jwt: jwt, monitor_id: monitor["id"])
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Zapier Integration
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
# Subscribe (register a webhook for a Zapier trigger)
|
|
355
|
+
sub = client.integrations.zapier_subscribe(
|
|
356
|
+
target_url: "https://hooks.zapier.com/hooks/catch/...",
|
|
357
|
+
event: "screenshot.completed"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Sample payload for Zap editor
|
|
361
|
+
sample = client.integrations.zapier_triggers(event: "screenshot.completed")
|
|
362
|
+
|
|
363
|
+
# Unsubscribe
|
|
364
|
+
client.integrations.zapier_unsubscribe(subscription_id: sub["id"])
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Webhook Verification
|
|
370
|
+
|
|
371
|
+
ScreenshotFreeAPI signs all webhook payloads using HMAC-SHA256. Always verify the
|
|
372
|
+
signature before processing the event.
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
# Standalone verification
|
|
376
|
+
require "screenshotfreeapi"
|
|
377
|
+
|
|
378
|
+
raw_body = request.body.read # raw bytes — do NOT parse yet
|
|
379
|
+
signature = request.headers["X-ScreenshotFree-Signature"]
|
|
380
|
+
secret = ENV["SCREENSHOTFREEAPI_WEBHOOK_SECRET"]
|
|
381
|
+
|
|
382
|
+
unless ScreenshotFreeAPI::Webhooks.verify_signature(raw_body, signature, secret)
|
|
383
|
+
render plain: "Invalid signature", status: :forbidden
|
|
384
|
+
return
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
event = JSON.parse(raw_body)
|
|
388
|
+
puts event["status"] # "completed" | "failed"
|
|
389
|
+
|
|
390
|
+
# Or use construct_event (raises ArgumentError on bad signature):
|
|
391
|
+
begin
|
|
392
|
+
event = ScreenshotFreeAPI::Webhooks.construct_event(raw_body, signature, secret)
|
|
393
|
+
rescue ArgumentError => e
|
|
394
|
+
render plain: e.message, status: :forbidden
|
|
395
|
+
return
|
|
396
|
+
end
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Rails Integration
|
|
402
|
+
|
|
403
|
+
Create an initializer `config/initializers/screenshotfreeapi.rb`:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
require "screenshotfreeapi"
|
|
407
|
+
|
|
408
|
+
SCREENSHOTFREEAPI_CLIENT = ScreenshotFreeAPI.new(
|
|
409
|
+
api_key: Rails.application.credentials.screenshotfreeapi_key!,
|
|
410
|
+
timeout: 45,
|
|
411
|
+
max_retries: 3
|
|
412
|
+
)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Then use `SCREENSHOTFREEAPI_CLIENT` anywhere in your application:
|
|
416
|
+
|
|
417
|
+
```ruby
|
|
418
|
+
class ScreenshotJob < ApplicationJob
|
|
419
|
+
def perform(url)
|
|
420
|
+
result = SCREENSHOTFREEAPI_CLIENT.capture(url, format: "png", full_page: true)
|
|
421
|
+
url = result["screenshots"].first["url"]
|
|
422
|
+
Rails.logger.info "Screenshot ready: #{url}"
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Configuration Reference
|
|
430
|
+
|
|
431
|
+
| Option | Type | Default | Description |
|
|
432
|
+
|---------------|---------|--------------------------------|--------------------------------------|
|
|
433
|
+
| `api_key` | String | required | Your ScreenshotFreeAPI key (`sfa_...`) |
|
|
434
|
+
| `base_url` | String | `https://api.screenshotfreeapi.com` | Override for staging / local dev |
|
|
435
|
+
| `timeout` | Integer | `30` | HTTP open/read timeout (seconds) |
|
|
436
|
+
| `max_retries` | Integer | `3` | Retries on 429 and 5xx errors |
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## License
|
|
441
|
+
|
|
442
|
+
MIT — see LICENSE file.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenshotFreeAPI
|
|
4
|
+
# Main entry point for the ScreenshotFreeAPI Ruby SDK.
|
|
5
|
+
#
|
|
6
|
+
# Instantiate once (e.g., in a Rails initializer) and reuse across requests.
|
|
7
|
+
# All resource objects share the same underlying HttpClient instance.
|
|
8
|
+
#
|
|
9
|
+
# Quick start:
|
|
10
|
+
#
|
|
11
|
+
# client = ScreenshotFreeAPI::Client.new(api_key: ENV["SCREENSHOTFREEAPI_KEY"])
|
|
12
|
+
# result = client.capture("https://stripe.com", format: "png")
|
|
13
|
+
# puts result["screenshots"].first["url"]
|
|
14
|
+
#
|
|
15
|
+
class Client
|
|
16
|
+
attr_reader :screenshots, :jobs, :auth, :billing, :workspaces, :monitors, :integrations
|
|
17
|
+
|
|
18
|
+
# @param api_key [String] Your ScreenshotFreeAPI key (sfa_...)
|
|
19
|
+
# @param base_url [String] Override base URL for staging / local dev
|
|
20
|
+
# @param timeout [Integer] HTTP open/read timeout in seconds (default: 30)
|
|
21
|
+
# @param max_retries [Integer] Max retry attempts on 429 / 5xx (default: 3)
|
|
22
|
+
def initialize(
|
|
23
|
+
api_key:,
|
|
24
|
+
base_url: HttpClient::DEFAULT_BASE_URL,
|
|
25
|
+
timeout: HttpClient::DEFAULT_TIMEOUT,
|
|
26
|
+
max_retries: HttpClient::DEFAULT_RETRIES
|
|
27
|
+
)
|
|
28
|
+
@http = HttpClient.new(
|
|
29
|
+
api_key: api_key,
|
|
30
|
+
base_url: base_url,
|
|
31
|
+
timeout: timeout,
|
|
32
|
+
max_retries: max_retries
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@screenshots = Resources::Screenshots.new(@http)
|
|
36
|
+
@jobs = Resources::Jobs.new(@http)
|
|
37
|
+
@auth = Resources::Auth.new(@http)
|
|
38
|
+
@billing = Resources::Billing.new(@http)
|
|
39
|
+
@workspaces = Resources::Workspaces.new(@http)
|
|
40
|
+
@monitors = Resources::Monitors.new(@http)
|
|
41
|
+
@integrations = Resources::Integrations.new(@http)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Convenience method: capture a URL and wait for the result.
|
|
45
|
+
#
|
|
46
|
+
# Equivalent to `client.screenshots.web_and_wait({ url: url }.merge(opts))`.
|
|
47
|
+
#
|
|
48
|
+
# @param url [String] URL to capture
|
|
49
|
+
# @param poll_interval [Numeric] Seconds between status polls (default: 2)
|
|
50
|
+
# @param timeout [Numeric] Maximum seconds to wait (default: 120)
|
|
51
|
+
# @param opts [Hash] Any additional web screenshot options
|
|
52
|
+
# @yieldparam status [Hash] Progress callback, called on each poll
|
|
53
|
+
#
|
|
54
|
+
# @return [Hash] Full result hash with "screenshots" array
|
|
55
|
+
def capture(url, poll_interval: 2, timeout: 120, **opts, &on_progress)
|
|
56
|
+
options = { url: url }.merge(opts)
|
|
57
|
+
@screenshots.web_and_wait(options, poll_interval: poll_interval, timeout: timeout, &on_progress)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check API health (no authentication required).
|
|
61
|
+
#
|
|
62
|
+
# @return [Hash] { "status" => "ok", "version" => "..." }
|
|
63
|
+
def health
|
|
64
|
+
@http.request(:get, "/health", auth_header: "")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScreenshotFreeAPI
|
|
4
|
+
# Base error class for all ScreenshotFreeAPI errors that originate from HTTP responses.
|
|
5
|
+
class ScreenshotFreeAPIError < StandardError
|
|
6
|
+
attr_reader :status_code, :error_code, :details
|
|
7
|
+
|
|
8
|
+
def initialize(status_code, error_code, message, details = nil)
|
|
9
|
+
super(message)
|
|
10
|
+
@status_code = status_code
|
|
11
|
+
@error_code = error_code
|
|
12
|
+
@details = details
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# 401 — Invalid or missing API key / JWT.
|
|
17
|
+
class AuthenticationError < ScreenshotFreeAPIError
|
|
18
|
+
def initialize(msg = "Invalid or missing API key")
|
|
19
|
+
super(401, "Unauthorized", msg)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# 403 — API key revoked or account suspended.
|
|
24
|
+
class ForbiddenError < ScreenshotFreeAPIError
|
|
25
|
+
def initialize(msg = "Access forbidden")
|
|
26
|
+
super(403, "Forbidden", msg)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# 404 — Resource not found or not owned by this key.
|
|
31
|
+
class NotFoundError < ScreenshotFreeAPIError
|
|
32
|
+
def initialize(msg = "Resource not found")
|
|
33
|
+
super(404, "NotFound", msg)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 400 — Request body failed Zod validation on the server.
|
|
38
|
+
class ValidationError < ScreenshotFreeAPIError
|
|
39
|
+
def initialize(msg = "Validation error", details = nil)
|
|
40
|
+
super(400, "ValidationError", msg, details)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 402 — Subscription cancelled or payment overdue.
|
|
45
|
+
class PaymentRequiredError < ScreenshotFreeAPIError
|
|
46
|
+
def initialize(msg = "Payment required — subscription cancelled or payment overdue")
|
|
47
|
+
super(402, "PaymentRequired", msg)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 429 — Per-minute rate limit exceeded. Includes `retry_after` seconds.
|
|
52
|
+
class RateLimitError < ScreenshotFreeAPIError
|
|
53
|
+
attr_reader :retry_after
|
|
54
|
+
|
|
55
|
+
def initialize(retry_after = nil, msg = "Rate limit exceeded")
|
|
56
|
+
super(429, "RateLimitExceeded", msg)
|
|
57
|
+
@retry_after = retry_after
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 429 with quota body — monthly screenshot quota exhausted.
|
|
62
|
+
class QuotaExceededError < ScreenshotFreeAPIError
|
|
63
|
+
def initialize(msg = "Monthly screenshot quota exceeded")
|
|
64
|
+
super(429, "QuotaExceeded", msg)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Raised when polling a job that has transitioned to the FAILED state.
|
|
69
|
+
class JobFailedError < StandardError
|
|
70
|
+
attr_reader :job_id, :reason
|
|
71
|
+
|
|
72
|
+
def initialize(job_id, reason)
|
|
73
|
+
super("Job #{job_id} failed: #{reason}")
|
|
74
|
+
@job_id = job_id
|
|
75
|
+
@reason = reason
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Raised when a job does not complete within the configured polling timeout.
|
|
80
|
+
class JobTimeoutError < StandardError
|
|
81
|
+
attr_reader :job_id, :timeout_seconds
|
|
82
|
+
|
|
83
|
+
def initialize(job_id, timeout_seconds)
|
|
84
|
+
super("Job #{job_id} did not complete within #{timeout_seconds}s")
|
|
85
|
+
@job_id = job_id
|
|
86
|
+
@timeout_seconds = timeout_seconds
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|