API Documentation
One endpoint: /v1/run. Send Playwright scripts or JSON, get screenshots. Sync by default.
GPT Actions OpenAPI:/riddledc-actions-openapi.yaml(JSON,.well-known)
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.
Using MCP? See Riddle MCP modes and setup.
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"] } |
script | Full Playwright (recommended) | { "script": "await page.goto(...)" } |
steps | JSON workflows (simple automation) | { "steps": [{ "goto": "..." }, { "click": "..." }] } |
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" ] }'Script Mode (Recommended)
Full Playwright API. Write the same code you'd write locally—we run it on our infrastructure.
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.
Steps Mode
JSON-based workflows. Good for simple automation—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" } ] }'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" } } }Stealth Mode
Enable stealth mode to use the Patchright engine, which bypasses common bot detection systems including Cloudflare, Vercel, and Datadome. Available on any tool — just add "stealth": true.
Trade-off: stealth mode disables console capture, so you won't get browser console logs in the response.
curl -X POST "https://api.riddledc.com/v1/run" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://protected-site.com",
"stealth": true,
"options": {
"fullPage": true
}
}'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. Simple screenshot requests can return PNG bytes directly. For JSON with screenshots and debugging context, use include. 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 screenshots + console logs + network HAR + script results in one JSON call:
curl -X POST "https://api.riddledc.com/v1/run" \\ -H "Authorization: Bearer YOUR_API_KEY" \\ -d '{ "url": "https://example.com", "include": ["screenshots", "console", "har", "result"] }'Shortcut: set inlineConsole, inlineHar, or inlineResult to true for sync responses.
Screenshots come back in JSON when you include screenshots; without it, sync mode may return raw PNG bytes for simple screenshot requests.
Response:
{ "status": "completed", "job_id": "job_abc123", "duration_ms": 4200, "screenshots": [ { "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: ["screenshots", "console", "har"] to keep visual evidence and debugging context in the same sync JSON response.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.
/v1/scrapeExtract structured content from a URL (title, description, markdown, links, headings).
/v1/mapDiscover all URLs on a website by crawling from a starting URL.
/v1/crawlCrawl and extract content from each page into a dataset (JSONL, JSON, CSV, ZIP).
/v1/visual-diffPixel-level visual comparison of two URLs. Returns change percentage and diff images.
/v1/previewDeploy static files to an ephemeral preview URL (24-hour expiry).
/v1/preview/{id}Delete a preview site immediately instead of waiting for auto-expiry.
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 |
Guides
Workflow docs for choosing the right Riddle surface and producing evidence humans can review.
Tools
Specialized endpoints for common tasks. Each has its own dedicated docs page.