API Documentation
One endpoint: /v1/run. Send JSON, get screenshots. Sync by default.
Quick Start
Your First Screenshot
curl -X POST "https://api.riddledc.com/v1/run" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}' \
-o screenshot.pngGet your API key from your dashboard. PNG bytes returned directly.
Authentication
Two options: API keys (recommended) or JWT tokens.
API Keys (Recommended)
Create from your Dashboard or via API:
# Create an API key
curl -X POST "https://api.riddledc.com/billing/api-keys" \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Production Key"}'
# Response
{
"apiKey": "rdc_live_abc123********************xyz789",
"keyId": "key_0844fb72799dd7b3",
"warning": "Save this key now - you will not be able to see it again!"
}Use in the Authorization header:
Authorization: Bearer rdc_live_abc123********************xyz789JWT Tokens (Alternative)
Authenticate via AWS Cognito for short-lived tokens:
curl -X POST "https://cognito-idp.us-east-1.amazonaws.com/" \
-H "Content-Type: application/x-amz-json-1.1" \
-H "X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth" \
-d '{
"AuthFlow": "USER_PASSWORD_AUTH",
"ClientId": "7u1bt3r20v613j7eaki9dkbhve",
"AuthParameters": {
"USERNAME": "your-email@example.com",
"PASSWORD": "your-password"
}
}'
# Use the IdToken from the responseInput Modes
Four ways to use /v1/run. Pick based on complexity.
| Mode | Use Case | Example |
|---|---|---|
url | Single screenshot | { "url": "https://example.com" } |
urls | Batch screenshots | { "urls": ["https://a.com", "https://b.com"] } |
steps | Workflows (recommended for agents) | { "steps": [{ "goto": "..." }, { "click": "..." }] } |
script | Full Playwright (loops, conditionals) | { "script": "await page.goto(...)" } |
URL Mode
Simplest option. Returns PNG directly.
curl -X POST "https://api.riddledc.com/v1/run" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}' -o screenshot.pngBatch Mode
Multiple URLs in one job. More cost-efficient.
curl -X POST "https://api.riddledc.com/v1/run" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://example.com",
"https://example.com/pricing",
"https://example.com/docs"
]
}'Steps Mode (Recommended)
JSON-based workflows. Best for AI agents—no code generation needed.
curl -X POST "https://api.riddledc.com/v1/run" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"steps": [
{ "goto": "https://example.com/login" },
{ "fill": { "selector": "#email", "value": "user@example.com" } },
{ "fill": { "selector": "#password", "value": "secret" } },
{ "click": "button[type=submit]" },
{ "waitForUrl": "**/dashboard**" },
{ "screenshot": "dashboard" }
]
}'Script Mode
Full Playwright when you need loops, try/catch, or complex logic.
curl -X POST "https://api.riddledc.com/v1/run" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"script": "await page.goto('\''https://example.com'\''); const title = await page.title(); console.log('\''Title:'\''', title); await saveScreenshot('\''homepage'\''');"
}'Script Helpers
Scripts can wait on window hooks and emit structured JSON artifacts.
await waitForWindow("appReady", 30000);
const data = await page.evaluate(() => window.appState);
await saveJson("state", data);To return a JSON value directly in the response, set options.returnResult: true in the request.
Common Options
These work with any input mode:
{
"url": "https://example.com",
"timeout_sec": 60,
"options": {
"viewport": { "width": 1920, "height": 1080 },
"fullPage": true,
"cookies": [{ "name": "session", "value": "abc", "domain": "example.com" }],
"headers": { "Authorization": "Bearer APP_TOKEN" },
"localStorage": { "token": "xyz" }
}
}Steps Reference
All available step types for steps mode.
| Step | Description | Example |
|---|---|---|
goto | Navigate to URL | { "goto": "https://example.com" } |
screenshot | Capture screenshot | { "screenshot": "step1" } |
click | Click element | { "click": "button.submit" } |
fill | Fill input field | { "fill": { "selector": "#email", "value": "test@example.com" } } |
type | Type key-by-key | { "type": { "selector": "#search", "text": "query" } } |
select | Select dropdown | { "select": { "selector": "select#country", "value": "US" } } |
check | Check checkbox | { "check": "#agree" } |
uncheck | Uncheck checkbox | { "uncheck": "#newsletter" } |
hover | Hover element | { "hover": ".dropdown" } |
press | Press key | { "press": "Enter" } |
waitFor | Wait for selector | { "waitFor": ".loaded" } |
waitForUrl | Wait for URL match | { "waitForUrl": "**/success**" } |
waitForTimeout | Wait milliseconds | { "waitForTimeout": 2000 } |
waitForLoadState | Wait for load state | { "waitForLoadState": "networkidle" } |
assert | Assert conditions | { "assert": [{ "selectorExists": ".success" }] } |
saveHtml | Save page HTML | { "saveHtml": "page" } |
log | Log message | { "log": "Starting flow" } |
eval | Run Playwright code | { "eval": "await page.evaluate(() => localStorage.clear())" } |
Assertions
Check page state without screenshotting. Abort early on failure.
{
"steps": [
{ "goto": "https://example.com/checkout" },
{ "assert": [{ "selectorExists": ".checkout-form" }], "onFail": ["screenshot", "abort"] },
{ "fill": { "selector": "#card", "value": "4242424242424242" } },
{ "click": "button.pay" },
{ "assert": [{ "urlIncludes": "/confirmation" }], "onFail": ["screenshot", "abort"] },
{ "screenshot": "confirmation" }
]
}
// Assertion types: selectorExists, selectorMissing, urlIncludes, urlExcludes, textContainsEval Context
The eval step runs in Node.js with Playwright's page object. Browser code is auto-wrapped:
// Both work - browser code auto-wrapped:
{ "eval": "document.title" }
{ "eval": "await page.evaluate(() => document.title)" }
// Playwright APIs work directly:
{ "eval": "await page.click('.submit')" }Screenshot Options
Control what and how you capture.
Simple (Full Page)
{ "screenshot": "homepage" }Element Screenshot
Capture just a specific element:
{ "screenshot": { "label": "navbar", "selector": "nav.main-nav" } }Clipped Region
{ "screenshot": { "label": "hero", "clip": { "x": 0, "y": 0, "width": 1200, "height": 600 } } }Viewport Only
{ "screenshot": { "label": "above-fold", "fullPage": false } }Format Options
// JPEG with quality
{ "screenshot": { "label": "compressed", "type": "jpeg", "quality": 75 } }
// Retina scale
{ "screenshot": { "label": "retina", "scale": "device" } }
// Transparent background
{ "screenshot": { "label": "transparent", "omitBackground": true } }All Options
| Option | Type | Description |
|---|---|---|
label | string | Filename for screenshot |
selector | string | CSS selector to capture |
clip | object | { x, y, width, height } |
fullPage | boolean | Full page (true) or viewport (false) |
type | string | "png" or "jpeg" |
quality | number | JPEG quality 0-100 |
scale | string | "css" or "device" |
omitBackground | boolean | Transparent background |
Response & Debugging
Understanding what comes back and how to debug issues.
Sync Mode (Default)
Waits up to 28 seconds for completion, returns PNG directly. For longer jobs, use sync: false to get a job_id and poll.
Default Response (PNG)
HTTP/1.1 200 OK
Content-Type: image/png
X-Job-Id: job_abc123
X-Duration-Ms: 4200
[PNG bytes]Enhanced Response with include
Get screenshot + console logs + network HAR + script results in one call:
curl -X POST "https://api.riddledc.com/v1/run" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"url": "https://example.com",
"include": ["console", "har", "result"]
}'Shortcut: set inlineConsole, inlineHar, or inlineResult to true for sync responses.
Response:
{
"status": "completed",
"job_id": "job_abc123",
"duration_ms": 4200,
"screenshot": {
"name": "result.png",
"data": "data:image/png;base64,iVBORw0K...",
"size": 45678
},
"console": {
"summary": { "total_entries": 3, "error_count": 1 },
"entries": { "log": [...], "error": [...] }
},
"result": { "score": 42 },
"har": {
"log": { "entries": [...] }
}
}include: ["console", "har"] to debug failures without extra API calls.Async Mode
For jobs >28 seconds, use sync: false:
// 1. Submit
const { job_id } = await fetch("https://api.riddledc.com/v1/run", {
method: "POST",
headers: { "Authorization": "Bearer KEY", "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://example.com", sync: false })
}).then(r => r.json());
// 2. Poll until status is "completed"
const status = await fetch(`https://api.riddledc.com/v1/jobs/${job_id}`, {
headers: { "Authorization": "Bearer KEY" }
}).then(r => r.json());
// 3. Get artifacts
const artifacts = await fetch(`https://api.riddledc.com/v1/jobs/${job_id}/artifacts`, {
headers: { "Authorization": "Bearer KEY" }
}).then(r => r.json());Artifacts
Every job produces artifacts stored for 24 hours:
| Artifact | Description |
|---|---|
screenshot.png | Your captured screenshot(s) |
console.json | All console.log, warn, error output |
network.har | Full network trace (HAR format) |
result.json | Structured data returned by scripts (when enabled) |
proof.json | Execution proof with timing/metrics |
Debugging Workflow
- Screenshot looks wrong? Check
console.jsonfor JavaScript errors - Page didn't load? Check
network.harfor failed requests - Selector not found? Use
saveHtmlstep to see actual DOM
Webhooks
Get notified when jobs complete instead of polling.
{
"url": "https://example.com",
"webhook_url": "https://your-server.com/riddle-webhook"
}Payload
{
"event": "job.completed",
"job_id": "job_abc123",
"status": "completed",
"duration_ms": 5693,
"artifacts": [
{ "name": "screenshot.png", "url": "https://cdn.riddledc.com/..." }
],
"billing": { "actual_seconds": 30, "actual_cost_dollars": 0.004167 }
}Requirements
- HTTPS recommended (HTTP allowed for dev)
- Must return 2xx within 10 seconds
- 2 automatic retries on failure
Recipes
Copy-paste examples for common scenarios.
Feed Vision LLM
import requests, base64, openai
# Get screenshot
response = requests.post(
"https://api.riddledc.com/v1/run",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"url": "https://example.com"}
)
screenshot_b64 = base64.b64encode(response.content).decode()
# Send to GPT-4V
openai.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "What's on this page?"},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{screenshot_b64}"}}
]
}]
)Authenticated Screenshot
// With session cookie
{
"url": "https://app.example.com/dashboard",
"options": {
"cookies": [{ "name": "session_id", "value": "abc123", "domain": "app.example.com" }]
}
}
// With Bearer token
{
"url": "https://api.example.com/page",
"options": {
"headers": { "Authorization": "Bearer APP_TOKEN" }
}
}Mobile Screenshot
{
"url": "https://example.com",
"options": { "viewport": { "width": 375, "height": 667 } }
}Common: iPhone (375x667), Android (360x640), iPad (768x1024)
Batch for Cost Savings
// 4 screenshots, 1 job = ~$0.001 each instead of ~$0.004
{
"steps": [
{ "goto": "https://example.com" }, { "screenshot": "home" },
{ "goto": "https://example.com/pricing" }, { "screenshot": "pricing" },
{ "goto": "https://example.com/docs" }, { "screenshot": "docs" },
{ "goto": "https://example.com/about" }, { "screenshot": "about" }
]
}Wait for SPA
{
"steps": [
{ "goto": "https://spa-app.com" },
{ "waitForLoadState": "networkidle" },
{ "waitFor": "text=Dashboard" },
{ "screenshot": "loaded" }
]
}Multi-Step Form
{
"steps": [
{ "goto": "https://signup.example.com" },
{ "fill": { "selector": "input[name=email]", "value": "test@example.com" } },
{ "click": "text=Continue" },
{ "screenshot": "step1" },
{ "waitFor": "input[name=name]" },
{ "fill": { "selector": "input[name=name]", "value": "Test User" } },
{ "click": "text=Next" },
{ "screenshot": "step2" }
]
}Lambda Handler
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
exports.handler = async (event) => {
const response = await fetch("https://api.riddledc.com/v1/run", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.RIDDLE_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ url: event.url })
});
const imageBuffer = Buffer.from(await response.arrayBuffer());
await new S3Client({}).send(new PutObjectCommand({
Bucket: process.env.BUCKET,
Key: `screenshots/${Date.now()}.png`,
Body: imageBuffer
}));
return { statusCode: 200 };
};API Reference
/v1/runTake screenshots, run workflows. Accepts url, urls, steps, or script.
/v1/jobsList recent jobs. Query: ?limit=50
/v1/jobs/{job_id}Get job status. Returns queued, running, completed, or failed.
/v1/jobs/{job_id}/artifactsGet artifacts (screenshots, console.json, network.har). Available for 24 hours.
/v1/balanceCheck account balance in seconds and dollars.
/billing/api-keysCreate API key. Requires JWT auth.
/billing/api-keysList your API keys.
/billing/api-keys/{keyId}Revoke an API key.
Pricing
$0.50/hour browser time, billed per second. 30s minimum per job (~$0.004).
Error Codes
Check the error.code field in failed responses.
| Code | Meaning | Fix |
|---|---|---|
UNAUTHORIZED | Invalid/missing API key | Check Authorization header |
TIMEOUT | Job exceeded timeout | Increase timeout_sec or optimize |
NAVIGATION_FAILED | Page failed to load | Check URL, network errors |
SCRIPT_ERROR | Script threw exception | Check console.json |
INVALID_URL | URL blocked/malformed | No localhost, internal IPs |
INSUFFICIENT_BALANCE | Not enough credits | Add funds |
RATE_LIMIT_EXCEEDED | Too many requests | Exponential backoff |
INTERNAL_ERROR | System error | Retry, contact support if persists |