
You've written Playwright scripts that break every time a website redesigns its nav. You've managed headless Chrome on a server and watched it eat memory at 3 AM. You've set up proxies, handled CAPTCHAs, dealt with dynamic content that only exists in the DOM after six JavaScript events fire in the right order.
TinyFish is a different approach. You describe what you want in plain English. A remote agent handles the browser, the proxies, the anti-bot layer, and the JavaScript. You get back clean JSON.
This guide gets you from zero to a working web agent in under 10 minutes. No browser setup. No proxy configuration. One API call.
How to pick your path:
Requirements: Python 3.8+ or Node.js 18+ if using the SDK. The curl examples work with any shell.
Playwright and Selenium automate a browser you control and maintain. You write selectors that break when sites redesign. You manage proxies separately. One task at a time unless you build your own concurrency layer. The infrastructure overhead is real, but you get maximum control and the lowest per-run cost on simple, static pages.

Browserbase gives you a managed cloud browser: you still write the automation logic yourself (typically using their Stagehand framework), but they handle provisioning. Good fit if you want raw browser control without the server management.
TinyFish takes it further: you pass a URL and a plain English goal, and the agent handles the full execution — navigation, dynamic content, bot protection, and result extraction. The trade-off is meaningful: less fine-grained control per step, but dramatically less code to write and maintain.
The honest breakdown:
Go to agent.tinyfish.ai/sign-up and create a free account. No credit card required. You start with 500 free steps.
Once you're in, go to the API Keys page, click Create API Key, and copy it somewhere safe. Keys are shown only once.
Set it as an environment variable:
export TINYFISH_API_KEY=sk-tinyfish-your-key-hereThat's the only setup. Your first call works with just curl — no SDK needed yet.
This calls the agent on a demo e-commerce site. The -N flag streams events as they arrive:
curl -N -X POST https://agent.tinyfish.ai/v1/automation/run-sse \
-H "X-API-Key: $TINYFISH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://scrapeme.live/shop",
"goal": "Extract the first 3 product names and prices. Return as JSON array: [{\"name\": str, \"price\": str}]"
}'You'll see events streaming in your terminal as the agent works:
data: {"type": "STARTED", "run_id": "abc123", "timestamp": "2026-01-01T00:00:00Z"}
data: {"type": "STREAMING_URL", "run_id": "abc123", "streaming_url": "https://tf-abc123.fra0-tinyfish.unikraft.app/stream/0", "timestamp": "..."}
data: {"type": "PROGRESS", "run_id": "abc123", "purpose": "Visit the page to find product listings", "timestamp": "..."}
data: {"type": "PROGRESS", "run_id": "abc123", "purpose": "Extract product names and prices from the grid", "timestamp": "..."}
data: {"type": "COMPLETE", "run_id": "abc123", "status": "COMPLETED", "result": {
"products": [
{"name": "Bulbasaur", "price": "$63.00"},
{"name": "Ivysaur", "price": "$87.00"},
{"name": "Venusaur", "price": "$105.00"}
]
}, "timestamp": "..."}Each SSE event follows the format documented in the Agent API Reference. The event types arrive in this order: STARTED → STREAMING_URL → PROGRESS (one or more) → COMPLETE.
The streaming_url is a live browser preview — open it to watch the agent navigate in real time. It stays valid for 24 hours after the run completes.
That run costs approximately 3–5 steps from your free 500-step balance.
The curl approach is fine for testing. For anything running in production, use the SDK. It handles SSE parsing, retries, and event routing.
pip install tinyfishMac users: If you see externally-managed-environment error, macOS doesn't allow installing packages into the system Python. Create a virtual environment first:
python3 -m venv venv
source venv/bin/activate
pip install tinyfishYou'll see (venv) in your terminal prompt when the virtual environment is active. Use this every time you open a new terminal to run TinyFish scripts.
# first_agent.py
from tinyfish import TinyFish, CompleteEvent, RunStatus
client = TinyFish() # Reads TINYFISH_API_KEY from environment
# The SDK's stream() method wraps the /run-sse endpoint.
# It parses SSE events for you and yields typed Python objects.
with client.agent.stream(
url="https://scrapeme.live/shop",
goal="Extract the first 5 product names and prices. Return as JSON array: [{\"name\": str, \"price\": str}]",
) as stream:
for event in stream:
if isinstance(event, CompleteEvent):
# COMPLETED means the run finished, not necessarily that your goal succeeded.
# Always check for goal-level failure in the result. See Step 4 for why.
if event.status == RunStatus.COMPLETED:
result = event.result_json # Returns a dict parsed from the agent's JSON output
if result and result.get("status") != "failure":
print(result)
else:
print("Goal failed:", result.get("reason") if result else "No result returned")
else:
print("Run failed:", event.error)Run it:
python3 first_agent.pyRequires Python 3.8+. On Mac, use python3 — the python command doesn't exist by default. This example uses the synchronous TinyFish client. For async usage (needed for batch processing in Step 6), use AsyncTinyFish instead — see the Async Bulk Requests example.
npm install @tiny-fish/sdk// firstAgent.ts
import { TinyFish, EventType, RunStatus } from "@tiny-fish/sdk";
const client = new TinyFish(); // Reads TINYFISH_API_KEY from environment
const stream = await client.agent.stream({
url: "https://scrapeme.live/shop",
goal: 'Extract the first 5 product names and prices. Return as JSON array: [{"name": string, "price": string}]',
});
for await (const event of stream) {
if (event.type === EventType.COMPLETE) {
// Same two-layer check: infrastructure status, then goal status
if (event.status === RunStatus.COMPLETED) {
const result = event.result;
if (result && result.status !== "failure") {
console.log(result);
} else {
console.error("Goal failed:", result?.reason ?? "No result returned");
}
} else {
console.error("Run failed:", event.error?.message);
}
}
}Requires Node.js 18+. The SDK also supports a callback style with onStarted, onProgress, and onComplete handlers — see the Endpoints documentation for that pattern.
Here's the thing most developers miss until they hit it in production: COMPLETED means the infrastructure worked — not that your goal succeeded.
TinyFish has two separate failure modes:
Layer 1 — Infrastructure failure (FAILED status): The browser couldn't launch, network timeout, or an unrecoverable error. You'll see status: "FAILED" and a message in the error field. The error object includes a code, message, and category (one of SYSTEM_FAILURE, AGENT_FAILURE, BILLING_FAILURE, or UNKNOWN). Some errors include a retry_after value suggesting when to retry.
Layer 2 — Goal failure (COMPLETED status, failure in result): The browser ran fine, but the agent couldn't accomplish the goal — the login page was unexpected, the product wasn't found, a CAPTCHA blocked the extraction. The run returns COMPLETED, but the result object looks like this:
{
"status": "COMPLETED",
"result": {
"status": "failure",
"reason": "Could not find any products on the page"
}
}If you only check status === "COMPLETED" and print result, you'll silently consume a successful API call that returned nothing useful. In a batch job running across 50 URLs, this is hard to notice and expensive to debug.
The correct pattern — always check both layers:
# Python: two-layer failure handling
with client.agent.stream(url=url, goal=goal) as stream:
for event in stream:
if isinstance(event, CompleteEvent):
if event.status == RunStatus.COMPLETED:
result = event.result_json
# Check for goal-level failure inside a COMPLETED run
if not result or result.get("status") == "failure":
handle_goal_failure(result)
else:
process_result(result)
else:
# Infrastructure failure — check event.error for details
handle_infrastructure_failure(event.error)// TypeScript: same pattern
if (event.type === EventType.COMPLETE) {
if (event.status === RunStatus.COMPLETED) {
const result = event.result;
if (!result || result.status === "failure") {
handleGoalFailure(result?.reason);
} else {
processResult(result);
}
} else {
handleInfrastructureFailure(event.error?.message);
}
}This is from the Runs documentation, which covers the full run lifecycle, the result object schema, and a more comprehensive handleRunResult switch pattern in TypeScript.
The goal parameter is where most first-time users get inconsistent results. According to TinyFish's Goal Prompting Guide, specific goals complete 4.9x faster and return 16x less unnecessary data than vague goals for the same task.
The difference is the output schema. If you don't specify it, the agent makes its own choices about structure, field names, and what counts as relevant.
Weak goal:
Get the products
Production-ready goal:
Extract all products visible on this page. For each product return: - name: string (full product title) - price: number (no currency symbol) - currency: string (3-letter code, e.g. "USD") - in_stock: boolean If a cookie banner appears, close it first. Do not click any purchase or add-to-cart buttons. Do not proceed to checkout or enter any payment details. If price shows "Contact us", set price to null. Return as JSON array: [{"name": str, "price": number|null, "currency": str, "in_stock": bool}]
Seven components make goals reliable, according to the prompting guide:
| Component | Purpose | Example |
|---|---|---|
| Objective | What to achieve | "Extract pricing information" |
| Target | Where to focus | "from the pricing table on the page" |
| Fields | What to extract | "name, price, availability" |
| Schema | Output structure | "Return as JSON with keys: name, price" |
| Steps | Ordered actions | "Close the cookie banner first" |
| Guardrails | What NOT to do | "Do not click purchase buttons" |
| Edge cases | Handle unexpected states | "If price shows 'Contact Us', set to null" |
Simple tasks need only 2–3 components. Production extractions benefit from all seven. The guardrails component is especially important for e-commerce and transactional sites — always include explicit instructions like "Do not proceed to checkout" or "Do not modify or cancel any orders" to prevent the agent from triggering unintended actions.
For multi-step workflows, number your steps explicitly:
1. Search for "standing desk" in the search bar 2. Filter results by price: under $500 3. Sort by "Best Seller" 4. Extract the top 5 results: name, price, rating (number), review count, URL Return as JSON array.
Numbered steps give the agent a clear checklist. Each step must complete before the next one starts. For longer workflows, you can also tell the agent to remember data across steps: "Note the confirmation number from step 3 — you'll need it in step 5."
Runs have an approximate 5-minute timeout. For complex multi-step workflows, ensure your goal can complete within this window or break it into smaller runs.
Three endpoints, three different situations:
| Endpoint | Pattern | Best For |
|---|---|---|
| /run | Synchronous — wait for result | Quick tasks under 30 seconds, scripts |
| /run-async | Submit then poll | Long tasks, batch processing |
| /run-sse | Live event stream | User-facing apps, debugging, development |
/run-sse is the right default for development — you see every action the agent takes, plus the live preview URL. The examples above all use it.
One important difference: runs created via /run cannot be cancelled — the request blocks until completion. If you need the ability to cancel a run mid-execution, use /run-async or /run-sse instead. See the Endpoints documentation for the full comparison.
For production batch jobs, /run-async is cleaner. Submit all tasks at once, get run IDs back immediately, poll for results as they complete. This example submits three URLs in parallel using the async client:
# Async batch: submit all at once, poll independently
# This uses AsyncTinyFish (not TinyFish) because we need asyncio for parallel submission.
# See: https://docs.tinyfish.ai/examples/bulk-requests-async
import asyncio
from tinyfish import AsyncTinyFish, RunStatus
async def wait_for_completion(client, run_id, poll_interval=2):
"""Poll a run until it reaches a terminal status."""
while True:
run = await client.runs.get(run_id)
if run.status in (RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED):
return run
await asyncio.sleep(poll_interval)
async def main():
client = AsyncTinyFish() # Async client for concurrent operations
urls = [
"https://site1.com/pricing",
"https://site2.com/pricing",
"https://site3.com/pricing",
]
goal = 'Extract plan names and monthly prices. Return as JSON array: [{"plan": str, "price": number}]'
# Submit all at once using client.agent.queue() — this calls /run-async
submit_tasks = [
client.agent.queue(url=url, goal=goal)
for url in urls
]
responses = await asyncio.gather(*submit_tasks)
run_ids = [r.run_id for r in responses]
print(f"Submitted {len(run_ids)} runs")
# Poll until all complete
completion_tasks = [
wait_for_completion(client, run_id)
for run_id in run_ids
]
results = await asyncio.gather(*completion_tasks)
# Process results (two-layer check still applies)
for run in results:
if run.status == RunStatus.COMPLETED:
# run.result is the parsed result object from the completed run
if run.result and run.result.get("status") != "failure":
print(f"{run.run_id}: {run.result}")
else:
reason = run.result.get("reason") if run.result else "no result"
print(f"{run.run_id}: goal failed — {reason}")
else:
print(f"{run.run_id}: infrastructure failed — {run.error}")
asyncio.run(main())This pattern matches the official Async Bulk Requests example in the TinyFish docs. The key differences from the synchronous examples in Steps 3–4: you use AsyncTinyFish instead of TinyFish, and client.agent.queue() instead of client.agent.stream().
Full async examples with webhook-based notification (instead of polling) are in the TinyFish Cookbook on GitHub.
Add proxy routing. For geo-restricted sites or country-specific content, add proxy_config to your request body. The country_code field accepts US, GB, CA, DE, FR, JP, or AU. Residential proxies are included at no extra charge on all plans.
{
"url": "https://example.com",
"goal": "Extract product data",
"proxy_config": {
"enabled": true,
"country_code": "US"
}
}See the Proxy documentation for the full list of supported countries and custom proxy setup.
Handle bot-protected sites. For sites that block standard automation, set "browser_profile": "stealth" to trigger deeper fingerprinting. The default is "lite". You can combine stealth mode with proxy routing for maximum coverage. See the Anti-Bot Guide.
Connect to your AI agent stack. TinyFish has an MCP integration that lets Claude and Cursor call web agents directly. The n8n node adds it to no-code workflows without any code at all.
500 free steps. No credit card. Your first agent runs in under a minute.
→ Get your API key at agent.tinyfish.ai
The free tier is pay-as-you-go at $0.015/step after the first 500. A Starter plan at $15/month includes 1,650 steps and 10 concurrent agents. See full pricing at tinyfish.ai/pricing.
command not found: python or command not found: pip (Mac) macOS doesn't include python or pip by default. Use python3 and pip3 instead. If pip3 also fails with an externally-managed-environment error, create a virtual environment first:
python3 -m venv venv
source venv/bin/activate
pip install tinyfish
python3 first_agent.pyModuleNotFoundError: No module named 'tinyfish' You haven't installed the SDK yet, or your virtual environment isn't activated. Run pip install tinyfish (or pip3 install tinyfish). If you're using a virtual environment, make sure you see (venv) in your terminal prompt before running the script.
Invalid API key or 401 Unauthorized Your TINYFISH_API_KEY environment variable isn't set or has the wrong value. Double-check with echo $TINYFISH_API_KEY in your terminal. The key should start with sk-tinyfish-. If you're using a .env file, make sure there are no extra spaces or quotes around the value.
npm: command not found (TypeScript) Node.js isn't installed. Download it from nodejs.org — the LTS version is fine. Restart your terminal after installing.
What is a "step" in TinyFish billing?
A step is one action the agent takes — a page load, a click, a form fill, a scroll. Simple single-page extractions are typically 2–5 steps. Multi-step workflows with login and navigation run 10–20 steps. The free 500-step trial gives you real room to test against your actual target sites before choosing a plan.
Does TinyFish work on sites that require login?
Yes. Pass credentials in the goal: "Log in with email user@example.com and password mypassword, then navigate to the dashboard and extract the subscription status." For recurring production workflows, Browser Profiles let you reuse authenticated sessions so you don't log in on every run. TinyFish also supports Vault Credentials for securely managing login details without exposing them in goal text.
How do I scrape a different website — for example, Amazon?
Change the url and goal parameters in the API call. These are the only two fields that define what the agent does. For example, to extract product data from an Amazon search page:
with client.agent.stream(
url="https://www.amazon.com/s?k=wireless+earbuds",
goal=(
"Extract the first 5 product names, prices, and ratings. "
"Do not click any product or add anything to cart. "
'Return as JSON array: [{"name": str, "price": str, "rating": str}]'
),
) as stream:If the site has bot protection and the run fails or returns empty results, add browser_profile="stealth" to the call. For geo-restricted sites, add proxy_config={"enabled": True, "country_code": "US"}. The rest of the code — event handling, error checking — stays exactly the same regardless of which site you're targeting.
My run returned COMPLETED but the result is empty or wrong — what happened?
This is the two-layer failure pattern described in Step 4. COMPLETED means the infrastructure worked, not that your goal succeeded. Check result.status — if it's "failure", the agent finished but couldn't accomplish the goal. The reason field explains why. Most common fixes: make the goal more explicit, add edge case handling (e.g., "if no products found, return empty array"), or switch to stealth mode if bot protection is involved.
How is this different from Playwright or Browserbase?
Playwright gives you a local browser you fully control — maximum flexibility, maintenance overhead. Browserbase gives you a managed cloud browser — you still write the automation logic yourself. TinyFish abstracts both: you pass a goal, get back JSON. No selectors, no session management. The right choice depends on how much control you need vs. how much code you want to write and maintain.
Can I run multiple agents at the same time?
Yes. The PAYG tier supports 2 concurrent agents; Starter supports 10; Pro supports 50. Use the /run-async endpoint to submit multiple tasks without waiting for each one to finish. See the Concurrent Requests example and Async Bulk Requests example in the docs.
What happens if the agent fails mid-task? Runs return a FAILED status with an error object containing a code, message, and category for infrastructure failures. Goal-level failures return COMPLETED with result.status === "failure" and a reason. Workflows never hard-stop on billing overages — they continue at the overage rate and only fail on genuine execution errors.
What if I need to stop a run that's already started? Runs created via /run-async or /run-sse can be cancelled by sending a POST to /v1/runs/{id}/cancel. Runs created via the synchronous /run endpoint cannot be cancelled. See the Runs documentation for cancellation behavior.
No credit card. No setup. Run your first operation in under a minute.