# Browser Context Profiles API Source: https://docs.tinyfish.ai/agent-api/browser-context-profiles Create, set up, and use Browser Context Profiles from the Agent API Use the Browser Context Profiles API to create persistent browser state, set it up once, and reuse it in future Agent API runs. The happy path is simple: create a profile, save a logged-in setup session, then pass `use_profile: true` when you run automation. Learn when to use Browser Context Profile state Add stale-session repair ## API setup lifecycle Browser Context Profiles are API-controlled saved state. The hosted setup UI is a manual helper; API users should bring their own Playwright, Puppeteer, or CDP controller and connect to the setup session's `cdp_url`. Use `base_url` only for TinyFish HTTP session endpoints such as `/pages`; do not pass `base_url` to Playwright. Create a named Browser Context Profile with `/v1/profiles`, or list existing profiles and choose one. Call `POST /v1/profiles/{profileId}/setup-session` and keep the returned `session_id`, `cdp_url`, `base_url`, `timeout_seconds`, and `expires_at`. Connect your browser automation framework to `cdp_url`, then log in manually or programmatically. Save with `POST /v1/profiles/{profileId}/save` and `{ "session_id": "..." }`. Cancelling setup or letting it time out discards unsaved setup state. Pass `use_profile: true`, or pass both `use_profile: true` and `profile_id` for a specific profile. ## Create a profile ```bash theme={null} curl -X POST https://agent.tinyfish.ai/v1/profiles \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Salesforce Production", "set_as_default": true, "proxy_country_code": "US" }' ``` ```json theme={null} { "id": "prof_abc123def4567890", "name": "Salesforce Production", "proxy_country_code": "US", "fingerprint_seed": "12345678", "created_at": "2026-06-04T18:00:00.000Z" } ``` | Create field | Type | Notes | | - | - | - | | `name` | `string` | Required. 1–100 characters. | | `set_as_default` | `boolean` | Optional. Makes this the default profile for `use_profile: true`. | | `proxy_country_code` | `string \| null` | Optional. Supported country code, or `null` for no profile proxy. | ## Set up a profile Start a setup browser: ```bash theme={null} curl -X POST https://agent.tinyfish.ai/v1/profiles/prof_abc123def4567890/setup-session \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.example.com/login", "timeout_seconds": 900 }' ``` ```json theme={null} { "session_id": "sess_123", "cdp_url": "wss://...", "base_url": "https://...", "timeout_seconds": 900, "expires_at": "2026-06-04T18:15:00.000Z" } ``` Connect to `cdp_url` with Playwright, Puppeteer, Selenium, or another CDP client. For example, with Playwright: ```typescript TypeScript theme={null} import { chromium } from "playwright"; const CDP_URL = ""; const browser = await chromium.connectOverCDP(CDP_URL); const page = browser.contexts()[0].pages()[0]; await page.goto("https://app.example.com/login"); // Automate the login form here, or use your own CDP tooling to complete login. ``` ```python Python theme={null} import asyncio from playwright.async_api import async_playwright CDP_URL = "" async def main(): async with async_playwright() as p: browser = await p.chromium.connect_over_cdp(CDP_URL) page = browser.contexts[0].pages[0] await page.goto("https://app.example.com/login") # Automate the login form here, or use your own CDP tooling to complete login. asyncio.run(main()) ``` After login, save the setup session: ```bash theme={null} curl -X POST https://agent.tinyfish.ai/v1/profiles/prof_abc123def4567890/save \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "session_id": "sess_123" }' ``` ```json theme={null} { "domains_updated": ["example.com"], "domains_failed": [], "cookie_count": 12, "pages_captured": 1, "target_reads_skipped": 0, "domains_skipped": { "cookies_skipped": [], "local_storage_skipped": [], "session_storage_skipped": [], "blob_too_large": [] } } ``` ## Save, cancel, or time out The setup browser is temporary. If you do not save it before `expires_at`, or if you cancel it, TinyFish discards the unsaved setup state. Cancel explicitly when you know you do not want to save: ```bash theme={null} curl -X POST https://agent.tinyfish.ai/v1/profiles/prof_abc123def4567890/setup-session/cancel \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "session_id": "sess_123" }' ``` Cancelling a setup session that has already ended usually returns `{ "cancelled": true }`. Saving a session again after a successful save usually returns the first save's result. New sites you pass in `signed_in_sites` are still checked against what the first save captured, and recorded if they pass. If that result is no longer available, or the profile was deleted, you get the usual not-found error. ## Run with a Browser Context Profile Use your default Browser Context Profile: ```bash theme={null} curl -X POST https://agent.tinyfish.ai/v1/automation/run \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.example.com/dashboard", "goal": "Summarize the account health dashboard", "use_profile": true }' ``` Use a specific Browser Context Profile: ```bash theme={null} curl -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://app.example.com/dashboard", "goal": "Summarize the latest invoice, including the amount due and payment deadline.", "use_profile": true, "profile_id": "prof_abc123def4567890" }' ``` | Field | Type | Required | Notes | | - | - | - | - | | `use_profile` | `boolean` | No | Use Browser Context Profile state. Without `profile_id`, TinyFish uses your default Browser Context Profile. | | `profile_id` | `string` | No | Selects a specific profile. Requires `use_profile: true`. | If `use_profile: true` is set and no default profile exists, the run returns `400`. Create a profile and set it as default first. ## Pair with Vault For authenticated workflows, pair Browser Context Profiles with Vault so stale sessions can be repaired: ```json theme={null} { "url": "https://app.example.com/dashboard", "goal": "Check the dashboard and summarize anything urgent", "use_profile": true, "use_vault": true } ``` The Browser Context Profile starts the run with saved logged-in state. If the session expires and a matching Vault credential is available, TinyFish can use the credential to sign in again and repair the saved state for future runs. ## List profiles ```bash theme={null} curl https://agent.tinyfish.ai/v1/profiles \ -H "X-API-Key: $TINYFISH_API_KEY" ``` ## Inspect and update ```bash theme={null} # Inspect saved domains curl https://agent.tinyfish.ai/v1/profiles/prof_abc123def4567890 \ -H "X-API-Key: $TINYFISH_API_KEY" # Rename, make default, or update proxy location curl -X PATCH https://agent.tinyfish.ai/v1/profiles/prof_abc123def4567890 \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Salesforce", "set_as_default": true, "proxy_country_code": "US" }' # Delete one stored domain curl -X DELETE https://agent.tinyfish.ai/v1/profiles/prof_abc123def4567890/domains/example.com \ -H "X-API-Key: $TINYFISH_API_KEY" # Delete the profile curl -X DELETE https://agent.tinyfish.ai/v1/profiles/prof_abc123def4567890 \ -H "X-API-Key: $TINYFISH_API_KEY" ``` ## Upload cookies manually Use manual upload when you already have exported browser state and do not need to open a setup browser. ```bash theme={null} curl -X POST https://agent.tinyfish.ai/v1/profiles/prof_abc123def4567890/upload \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "cookies": [ { "name": "session", "value": "...", "domain": "example.com" } ] }' ``` Manual upload supports cookies plus local and session storage. | Limit | Value | | - | - | | Upload payload | 5 MB | | Cookies per upload | 3,000 | | Domains per profile | 100 | ## Common errors | Status | Meaning | Fix | | - | - | - | | `400` | Invalid body, missing default profile, or `profile_id` without `use_profile: true` | Fix the request or create a default profile | | `401` | Missing or invalid API key | Set `X-API-Key` | | `404` | Profile or saved domain not found | Check the profile ID or domain | | `409` | Setup/save/delete cleanup needs a retry, or the setup browser is no longer running | Retry the operation or start a new setup session | ## Profile endpoints All requests require `X-API-Key`. | Endpoint | Method | Purpose | | - | - | - | | `/v1/profiles` | `GET` | List profiles | | `/v1/profiles` | `POST` | Create a profile | | `/v1/profiles/{profileId}` | `GET` | Get profile details and saved domains | | `/v1/profiles/{profileId}` | `PATCH` | Rename a profile, set default, or update proxy location | | `/v1/profiles/{profileId}` | `DELETE` | Delete a profile | | `/v1/profiles/{profileId}/setup-session` | `POST` | Start an interactive setup browser | | `/v1/profiles/{profileId}/setup-session/cancel` | `POST` | Cancel a setup browser session without saving | | `/v1/profiles/{profileId}/save` | `POST` | Save a setup browser session | | `/v1/profiles/{profileId}/upload` | `POST` | Upload cookies and storage manually | | `/v1/profiles/{profileId}/domains/{domain}` | `DELETE` | Delete stored state for one domain | ## Next steps Learn when to use Browser Context Profile state Add stale-session repair # Agent API Source: https://docs.tinyfish.ai/agent-api/index Use natural-language goals to automate workflows on real websites The TinyFish Agent API lets you describe a task in natural language and have TinyFish execute it on a real website. Use it when you want goal-based automation rather than low-level browser scripting. The Agent API is the right choice when TinyFish should decide the browser actions. If you need direct browser control instead, use the [Browser API](/browser-api). ## Canonical Endpoints | Endpoint | Pattern | Best for | | - | - | - | | `/run` | Synchronous | Quick tasks and simple integrations | | `/run-async` | Start then poll | Long tasks and batch processing | | `/run-sse` | Live event stream | Real-time progress in user-facing apps | ```bash theme={null} POST https://agent.tinyfish.ai/v1/automation/run-sse ``` ## Before You Start Create an API key at [agent.tinyfish.ai/api-keys](https://agent.tinyfish.ai/api-keys). ```bash theme={null} export TINYFISH_API_KEY="your_api_key_here" ``` All requests require the `X-API-Key` header. See [Authentication](/authentication) for the full setup and troubleshooting guide. ## Your First Request ```python Python theme={null} from tinyfish import TinyFish client = TinyFish() with client.agent.stream( url="https://scrapeme.live/shop", goal="Extract the first 2 product names and prices. Return JSON.", ) as stream: for event in stream: print(event) ``` ```typescript TypeScript theme={null} import { TinyFish } from "@tiny-fish/sdk"; const client = new TinyFish(); const stream = await client.agent.stream({ url: "https://scrapeme.live/shop", goal: "Extract the first 2 product names and prices. Return JSON.", }); for await (const event of stream) { console.log(event); } ``` ```bash cURL theme={null} 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 2 product names and prices. Return JSON." }' ``` ## What Success Looks Like ```json theme={null} {"type":"STARTED","run_id":"abc123"} {"type":"PROGRESS","run_id":"abc123","purpose":"Visit the page to extract product information"} {"type":"PROGRESS","run_id":"abc123","purpose":"Extract the first two products and prices"} {"type":"COMPLETE","run_id":"abc123","status":"COMPLETED","result":{"products":[{"name":"Bulbasaur","price":"£63.00"},{"name":"Ivysaur","price":"£87.00"}]}} ``` The agent's answer comes back in `result`: a JSON object appears directly (as above), while a bare list or plain-text answer arrives under `result.result`. Pass an [`output_schema`](/key-concepts/structured-output) for guaranteed typed fields. If you want a blocking request that returns only the final result, use `/run`. If you want to start work and poll later, use `/run-async`. ## When to Use Agent vs the Other APIs * Use **Agent** when TinyFish should decide the browser actions from your goal. * Use **Browser** when you want to drive Playwright or CDP yourself. * Use **Fetch** when you already know the URLs and only need extracted page content. * Use **Search** when you need ranked search results rather than page automation. ## Writing Good Goals A goal is the plain-English instruction you pass in the `goal` field. TinyFish uses it to decide what to click, type, extract, and return. Good goals are: * specific about the output you want * explicit about the page or flow to follow * clear about response format when you need structured JSON ## Read Next Full request schema, run lifecycle, browser profiles, and errors Schema support, limits, and examples Choose between `/run`, `/run-async`, and `/run-sse` Understand status, polling, and completion Write more reliable automation instructions API key setup and troubleshooting # Agent API Reference Source: https://docs.tinyfish.ai/agent-api/reference Technical reference for goal-based TinyFish automation ## Endpoints | Endpoint | Method | Returns | Cancel support | | - | - | - | - | | `https://agent.tinyfish.ai/v1/automation/run` | `POST` | Final run result | No | | `https://agent.tinyfish.ai/v1/automation/run-async` | `POST` | `run_id` immediately | Yes | | `https://agent.tinyfish.ai/v1/automation/run-sse` | `POST` | SSE event stream | Yes | All requests require the `X-API-Key` header. See [Authentication](/authentication). ## Common Request Body All three automation endpoints accept the same JSON body. ```json theme={null} { "url": "https://example.com", "goal": "Find the pricing page and extract all plan details", "output_schema": { "type": "object", "properties": { "title": { "type": "string" }, "price": { "type": "number" } }, "required": ["title", "price"] }, "browser_profile": "lite", "proxy_config": { "enabled": true, "type": "tinyfish", "country_code": "US" }, "agent_config": { "mode": "strict", "max_steps": 50, "max_duration_seconds": 300 }, "webhook_url": "https://example.com/webhooks/tinyfish", "use_profile": true, "profile_id": "prof_abc123def4567890", "use_vault": true, "credential_item_ids": ["cred:a1b2c3d4:Work:item-xyz"] } ``` | Field | Type | Required | Notes | | - | - | - | - | | `url` | `string` | Yes | Target website URL to automate | | `goal` | `string` | Yes | Natural-language instruction for the automation | | `output_schema` | `object` | No | Structured-output schema subset for the run result. Unsupported fields are rejected before execution. See [Structured Output](/key-concepts/structured-output) | | `browser_profile` | `lite \| stealth` | No | Defaults to `lite` | | `proxy_config.enabled` | `boolean` | No | Enables proxying for the run | | `proxy_config.type` | `tinyfish \| custom` | No | `tinyfish` (default) uses the built-in TinyFish proxy; `custom` requires your own proxy URL | | `proxy_config.country_code` | `US \| GB \| CA \| DE \| FR \| JP \| AU` | No | Used with `type: "tinyfish"`. Defaults to `US` | | `proxy_config.url` | `string` | No | Required when `type: "custom"` | | `proxy_config.username` | `string` | No | Custom proxy username | | `proxy_config.password` | `string` | No | Custom proxy password | | `agent_config.mode` | `default \| strict` | No | Beta-gated. Use `strict` for fail-fast automation where the agent should stop instead of exploring workarounds | | `agent_config.max_steps` | `integer` | No | Beta-gated. Optional run step limit from 1 to 500. Omit the field to use EVA's default of 150 | | `agent_config.max_duration_seconds` | `integer` | No | Maximum wall-clock seconds before the agent stops. Defaults to no limit | | `webhook_url` | `string` | No | HTTPS URL to receive run lifecycle notifications. Must use `https://`. See [Webhooks](/webhooks) | | `capture_config` | `object` | No | Opt into extra run artifacts (`screenshots`, `recording`, `html`, `snapshots`, `elements`). Account-gated — returns `403` if your account is not enabled for a requested capability | | `use_profile` | `boolean` | No | Use the default saved [Browser Context Profile](/key-concepts/browser-context-profiles) for this run | | `profile_id` | `string` | No | Select a specific Browser Context Profile. Requires `use_profile: true` | | `use_vault` | `boolean` | No | Enable [vault credentials](/key-concepts/credentials) for this run. When `true`, TinyFish can log into sites using your connected password manager | | `credential_item_ids` | `string[]` | No | Scope the run to specific credential URIs from `GET /v1/vault/items`. Requires `use_vault: true`. If omitted, all synced items are available | **Beta:** `agent_config.mode: "strict"` is in beta — you must join the beta to use it (contact support to enable it for your account). Strict mode is best for precise tests and automations where unexpected page state should stop the run quickly instead of trying alternate paths. **Beta:** `agent_config.max_steps` is available only to beta-enabled users. Use it when a run needs a shorter or longer step budget than EVA's default. Non-beta API requests that include `max_steps` return `403 FORBIDDEN`; requests without the field are unaffected. Use `use_profile: true` for your default Browser Context Profile. Add `profile_id` when the run must use a specific saved profile. Create and set up profiles from the dashboard or the [Browser Context Profiles API](/agent-api/browser-context-profiles). See [Structured Output](/key-concepts/structured-output) for the supported keyword allowlist, size and depth limits, and common rewrites for invalid schemas. See [Browser Context Profiles](/key-concepts/browser-context-profiles), [Browser Context Profiles API](/agent-api/browser-context-profiles), [Browser Profiles](/key-concepts/browser-profiles), [Proxies](/key-concepts/proxies), and [Vault Credentials](/key-concepts/credentials) for operational guidance. ## `POST /v1/automation/run` Use this endpoint when you want the final result in one blocking response. ```json theme={null} { "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "COMPLETED", "started_at": "2024-01-01T00:00:00Z", "finished_at": "2024-01-01T00:00:30Z", "num_of_steps": 5, "result": { "product": "iPhone 15", "price": "$799" }, "error": null } ``` | Field | Type | Notes | | - | - | - | | `run_id` | `string \| null` | Unique identifier for the run | | `status` | `COMPLETED \| FAILED` | Final run state | | `started_at` | `string \| null` | ISO 8601 timestamp | | `finished_at` | `string \| null` | ISO 8601 timestamp | | `num_of_steps` | `number \| null` | Number of steps taken | | `result` | `object \| null` | Extracted JSON result | | `error` | `object \| null` | Present only when the run failed | Runs created via `/run` cannot be cancelled. ## `POST /v1/automation/run-async` Use this endpoint when you want a `run_id` immediately and will fetch the full run later. ```json theme={null} { "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "error": null } ``` | Field | Type | Notes | | - | - | - | | `run_id` | `string \| null` | Created run ID | | `error` | `object \| null` | Present only when run creation failed | Fetch the full run state later with `GET /v1/runs/{id}`. ## `POST /v1/automation/run-sse` Use this endpoint when you want a streaming event feed while the automation runs. Possible SSE event types: | Event type | Fields | | - | - | | `STARTED` | `type`, `run_id`, `timestamp` | | `STREAMING_URL` | `type`, `run_id`, `streaming_url`, `timestamp` | | `PROGRESS` | `type`, `run_id`, `purpose`, `timestamp` | | `HEARTBEAT` | `type`, `timestamp` | | `COMPLETE` | `type`, `run_id`, `status`, `result?`, `error?`, `timestamp` (on failure, `error` carries `help_url` / `help_message`) | Example stream: ```text theme={null} data: {"type":"STARTED","run_id":"run_123","timestamp":"2025-01-01T00:00:00Z"} data: {"type":"STREAMING_URL","run_id":"run_123","streaming_url":"https://...","timestamp":"..."} data: {"type":"PROGRESS","run_id":"run_123","purpose":"Clicking submit button","timestamp":"..."} data: {"type":"COMPLETE","run_id":"run_123","status":"COMPLETED","result":{...},"timestamp":"..."} ``` **Reconnection:** SSE streams do not support `Last-Event-ID` reconnection. If your client disconnects mid-stream, recover by polling `GET /v1/runs/{run_id}` until the run reaches a terminal status (`COMPLETED`, `FAILED`, or `CANCELLED`). ### Raw HTTP (no SDK) Parse SSE events directly without the TinyFish SDK: ```python theme={null} import os import httpx import json url = "https://agent.tinyfish.ai/v1/automation/run-sse" headers = {"X-API-Key": os.environ["TINYFISH_API_KEY"]} payload = {"url": "https://example.com", "goal": "Extract the page title"} with httpx.stream("POST", url, headers=headers, json=payload, timeout=120) as response: for line in response.iter_lines(): if line.startswith("data: "): event = json.loads(line[6:]) print(f"Event: {event['type']}") if event["type"] == "COMPLETE": print(f"Result: {event.get('result', {})}") ``` ## `GET /v1/runs/{id}` Use this endpoint to retrieve the current or final state of an async or streaming run. | Field | Type | Notes | | - | - | - | | `run_id` | `string` | Unique run identifier | | `status` | `PENDING \| RUNNING \| COMPLETED \| FAILED \| CANCELLED` | Current run state | | `goal` | `string` | Original goal text | | `created_at` | `string` | ISO 8601 timestamp | | `started_at` | `string \| null` | ISO 8601 timestamp | | `finished_at` | `string \| null` | ISO 8601 timestamp | | `num_of_steps` | `integer \| null` | Number of steps taken | | `result` | `object \| null` | Extracted JSON result | | `error` | `object \| null` | Error details if failed | | `profile_attached` | `boolean` | Whether a Browser Context Profile was attached to the run | | `profile_id` | `string \| null` | ID of the attached Browser Context Profile, if any | | `streaming_url` | `string \| null` | Live browser stream URL while running | | `browser_config` | `object \| null` | Proxy settings used for the run | | `video_url` | `string \| null` | Presigned run recording URL, if available | | `output_schema` | `object \| null` | Structured-output schema originally requested for the run | | `steps` | `array` | Recorded step events for the run | `error` may include: | Field | Type | Notes | | - | - | - | | `code` | `string` | Machine-readable error code | | `message` | `string` | Human-readable failure description | | `category` | `SYSTEM_FAILURE \| AGENT_FAILURE \| BILLING_FAILURE \| UNKNOWN` | Error class | | `retry_after` | `number \| null` | Suggested retry delay in seconds | | `help_url` | `string` | Troubleshooting link | | `help_message` | `string` | Human-readable guidance | ## `POST /v1/runs/{id}/cancel` Only runs created via `/run-async` or `/run-sse` can be cancelled. ```json theme={null} { "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "CANCELLED", "cancelled_at": "2026-01-14T10:30:55Z", "message": null } ``` | Field | Type | Notes | | - | - | - | | `run_id` | `string` | Run identifier | | `status` | `CANCELLED \| COMPLETED \| FAILED` | Actual status after the cancel attempt | | `cancelled_at` | `string \| null` | ISO 8601 timestamp | | `message` | `string \| null` | Idempotency or terminal-state message | ## Error Codes Common HTTP-level errors across automation endpoints: | Status | Code | Meaning | | - | - | - | | `400` | `INVALID_INPUT` | Invalid request body or missing required fields | | `401` | `MISSING_API_KEY`, `INVALID_API_KEY` | Missing or invalid API key | | `401` | `VAULT_RECONNECT_REQUIRED` | Reconnect the affected vault provider, then retry; see [error recovery](/error-codes) | | `403` | `FORBIDDEN` | Access denied | | `429` | `RATE_LIMIT_EXCEEDED` | Rate limit exceeded | | `500` | `INTERNAL_ERROR` | Internal server error | The `COMPLETE` SSE event or `GET /v1/runs/{id}` may also include run-level failures such as `TASK_FAILED`, `SITE_BLOCKED`, `MAX_STEPS_EXCEEDED`, `TIMEOUT`, or `BILLING_REJECTED`. ## Related First request, endpoint selection, and goal-writing basics Schema support, limits, and common rewrites Statuses, polling, and lifecycle behavior Improve automation reliability API key setup and troubleshooting Use password manager credentials in runs Reuse saved logged-in browser state # Agent Payments Source: https://docs.tinyfish.ai/agent-payments Let your agent top up its own TinyFish wallet with the Machine Payments Protocol (MPP) When your agent runs low on credit, it can add money to your TinyFish wallet itself and keep working. It pays with the wallet it already has, over the [Machine Payments Protocol (MPP)](https://mpp.dev/overview), the open standard Stripe and Tempo created for agents paying over HTTP. No dashboard needed. Whether you approve each payment is up to your wallet: some, like Stripe's Link agent wallet, ask you to approve each one. Agent payments are rolling out gradually, and need an account on wallet billing. If `POST /v1/wallet/top-up` answers `404` with `FEATURE_NOT_AVAILABLE`, they aren't available for your account yet. ## How It Works 1. Your agent asks TinyFish to top up, say \$10. 2. TinyFish answers with an HTTP `402` payment challenge: the amount, the currency, and who to pay. 3. Your agent's wallet issues a one-time [Shared Payment Token (SPT)](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens) for that amount. Your card details never reach the agent or TinyFish. 4. Your agent retries with the payment, Stripe charges it, and your wallet is credited in seconds, with a receipt. Any wallet that speaks MPP with the `stripe` payment method works, including Stripe's [Link agent wallet](https://docs.stripe.com/agentic-commerce/link-agent-wallet). | Detail | Value | | - | - | | Amount per top-up | $10.00 to $500.00, in whole cents | | Currency | USD | | Payment method | Card, through a Stripe Shared Payment Token | | Where | REST API and the TinyFish MCP server | *** ## REST API Top-ups use your normal API key in the `X-API-Key` header. The payment travels in a separate `Payment-Authorization` header. `GET /v1/wallet` includes `agent_top_up_url` when agent payments are available for your account, so an agent checking its balance can find the endpoint. A request refused with `402 INSUFFICIENT_CREDITS` carries the same field in `error.details` when a top-up would unblock it (not when the card on file needs fixing), so an agent that runs out mid-task knows where to top up. ```bash theme={null} curl -i -X POST https://agent.tinyfish.ai/v1/wallet/top-up \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"amount": "10.00"}' ``` The response is `402 Payment Required` with the challenge in the `WWW-Authenticate` header: ``` WWW-Authenticate: Payment id="…", realm="agent.tinyfish.ai", method="stripe", intent="charge", request="…", expires="…", header="Payment-Authorization" ``` Hand the challenge to your MPP wallet. Once the payment is approved (some wallets, like Link, ask you first), it returns a payment credential (`Payment `). The challenge expires after 30 minutes, which leaves time for that approval. Send the same request again, with the same amount, plus the credential: ```bash theme={null} curl -X POST https://agent.tinyfish.ai/v1/wallet/top-up \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Payment-Authorization: Payment eyJjaGFsbGVuZ2Ui…" \ -H "Content-Type: application/json" \ -d '{"amount": "10.00"}' ``` MPP clients that handle `402` responses, such as Stripe's Link agent wallet, run steps 2 and 3 for you. ### Responses | Status | Meaning | What your agent should do | | - | - | - | | `200` | Paid and credited. `available_balance` is your new balance (or `null` if it couldn't be read). | Carry on. | | `202` | Paid, and the credit lands within minutes. | Don't pay again. Check the balance shortly. | | `402` with `WWW-Authenticate` | No payment was sent. This is the challenge. | Pay it and retry. | | `402` without `WWW-Authenticate` | A payment was sent but not accepted. `detail` says why. | Don't pay again on your own (see below). | | `400` | The amount is invalid or outside $10 to $500. | Fix the amount. | | `404` | Agent payments aren't available for your account. | Top up from the [dashboard](https://agent.tinyfish.ai) instead. | A successful top-up looks like this: ```json theme={null} { "status": "settled", "amount": "10.00", "currency": "USD", "payment_reference": "pi_3QxYz2Ab", "available_balance": "16.44" } ``` The response also carries an MPP `Payment-Receipt` header. *** ## MCP If your agent connects to the TinyFish MCP server at `https://agent.tinyfish.ai/mcp` (see [MCP Integration](/mcp-integration)), it gets a `top_up_wallet` tool. It needs no API key: it tops up the wallet of the account you signed in with. The tool takes two calls: 1. Call `top_up_wallet` with an `amount`, like `"10.00"`. It returns `payment_required` and the challenge. 2. Pay the challenge, then call `top_up_wallet` again with the same amount and either: * `payment_credential`: the credential from your MPP wallet, or * `spt` plus `challenge`: a Stripe Shared Payment Token issued for the challenge, and the challenge itself. MCP clients that support MPP's payment binding pay the challenge automatically from the tool result. The result's `status` is `settled`, `pending` (paid, credit landing, don't pay again), or `unconfirmed` (don't pay again on your own). When a run is refused for insufficient balance and the tool is available to you, the refusal names `top_up_wallet` as an alternative to the dashboard link, unless the card on file needs fixing first. Your wallet asks you to approve the payment, the same as any other MPP charge. `top_up_wallet` is available on the main MCP server and the Claude connector. The ChatGPT app doesn't include it. *** ## Never Paying Twice TinyFish is built so one top-up is charged once: * **A replayed payment is never charged again.** Sending the same credential twice credits the wallet once. * **A failed paid retry never gets a new challenge.** Some failures can happen after a charge, for example a timeout while Stripe is processing. So TinyFish answers `402` without a challenge, and an MPP wallet has nothing to pay automatically. * **A charge that went through is credited automatically,** even if something fails right after it. If your agent gets a `402` without a challenge, a `202`, or a request that errors or times out after it sent a payment, it shouldn't start a new top-up on its own. If the balance hasn't increased after 10 minutes, it should ask you first. A declined card also comes back as a `402` without a challenge, and charges nothing. To try again, start a new top-up with a different payment method. *** ## Refunds For a refund of an agent top-up, contact support with the `payment_reference` from the response. ## Learn More * [Machine Payments Protocol](https://mpp.dev/overview) * [Stripe machine payments](https://docs.stripe.com/payments/machine) * [Link agent wallet](https://docs.stripe.com/agentic-commerce/link-agent-wallet) # Anti-Bot Guide Source: https://docs.tinyfish.ai/anti-bot-guide Diagnose and bypass bot detection when automating protected websites You've sent a run, it came back `COMPLETED`, but the result is empty or wrong. Or maybe it outright `FAILED`. Before you start rewriting your goal, check whether the site is blocking you — bot detection is the most common cause of silent failures, and the fix is usually two lines of code. This guide walks through the full process: confirm the problem, apply the right configuration, and tune your goal to behave more like a human. Examples use the Python SDK. The same parameters work across all SDKs and the REST API — see [API Reference](/api-reference) for TypeScript and cURL equivalents. ## Installation ```bash theme={null} pip install -U tinyfish ``` Set your API key as an environment variable so you don't have to pass it explicitly: ```bash theme={null} export TINYFISH_API_KEY="your-api-key" ``` *** ## Step 1: Confirm Anti-Bot Is the Problem Don't assume. Sites can fail for lots of reasons — slow JavaScript, unexpected layout changes, ambiguous goals. Anti-bot has specific fingerprints. Look for them first. ### Get the streaming URL and watch the browser Every run produces a `streaming_url` — a live browser preview you can open while the run is happening. This is the fastest way to see exactly what the agent encountered. Use `agent.stream()` to capture it as soon as it's available: ```python theme={null} from tinyfish import TinyFish, CompleteEvent client = TinyFish() with client.agent.stream( goal="Extract the product name and price", url="https://example.com/products", on_streaming_url=lambda e: print(f"Watch live: {e.streaming_url}"), on_progress=lambda e: print(f" > {e.purpose}"), ) as stream: for event in stream: if isinstance(event, CompleteEvent): print("Status:", event.status) print("Result:", event.result_json) ``` The `on_progress` callback shows each step the agent took — if it got stuck on a challenge page, you'll see it stop there. If you already started a run with `agent.queue()`, retrieve the streaming URL from the run object: ```python theme={null} run = client.runs.get("run_abc123") print(run.streaming_url) # open this in your browser ``` ### What to look for in the browser preview Open `streaming_url` in your browser. What you see tells you what happened: | What you see | Likely cause | | - | - | | Cloudflare challenge / "Checking your browser" | Cloudflare bot detection | | DataDome popup or redirect | DataDome protection | | Blank page or infinite spinner | IP-based block or JS fingerprinting | | CAPTCHA (reCAPTCHA, hCaptcha) | CAPTCHA gate — cannot be solved automatically | | "Access Denied" or 403 page | IP or User-Agent block | | Login page when you expected content | Session-based bot detection | ### Check the result — `COMPLETED` doesn't mean it worked The `result` field is a better indicator of agentic success. ```python theme={null} from tinyfish import TinyFish, RunStatus, CompleteEvent client = TinyFish() with client.agent.stream( goal="Extract the product name and price", url="https://example.com/products", ) as stream: for event in stream: if isinstance(event, CompleteEvent): if event.status == RunStatus.COMPLETED and event.result_json: # Anti-bot shows up here as null fields or explicit failure flags result = event.result_json if result.get("status") == "failure" or not any(result.values()): print("Blocked — result is empty despite COMPLETED status") elif event.status == RunStatus.FAILED: print("Run failed:", event.error.message if event.error else "unknown") ``` **Anti-bot signatures in the result:** * Fields are all `null` or empty arrays AND the streaming view shows the target content was never loaded * `result.reason` mentions "access denied", "blocked", or "could not find" If the streaming view shows a challenge page and the result is empty or a failure — you've confirmed anti-bot. Move to Step 2. *** ## Step 2: Enable Stealth Mode and Proxy Apply both together. Stealth changes the browser fingerprint; the proxy changes the IP. Sites that use anti-bot services correlate both signals — changing only one often isn't enough. ### Switch to stealth browser ```python theme={null} from tinyfish import TinyFish, BrowserProfile client = TinyFish() response = client.agent.run( goal="Extract the product name and price", url="https://protected-site.com/products", browser_profile=BrowserProfile.STEALTH, # was BrowserProfile.LITE or omitted ) ``` `BrowserProfile.STEALTH` is a modified browser with anti-detection techniques. The default (`BrowserProfile.LITE`) is faster but doesn't include these measures. ### Add a proxy ```python theme={null} from tinyfish import TinyFish, BrowserProfile, ProxyConfig, ProxyCountryCode client = TinyFish() response = client.agent.run( goal="Extract the product name and price", url="https://protected-site.com/products", browser_profile=BrowserProfile.STEALTH, proxy_config=ProxyConfig( enabled=True, country_code=ProxyCountryCode.US, # match the site's expected audience ), ) ``` **Choosing a country:** Pick the country where the site's primary users are. `country_code` accepts any ISO 3166-1 alpha-2 code (e.g. `"IN"`); the enum covers common ones: | Enum | Country | | - | - | | `ProxyCountryCode.US` | United States | | `ProxyCountryCode.GB` | United Kingdom | | `ProxyCountryCode.CA` | Canada | | `ProxyCountryCode.DE` | Germany | | `ProxyCountryCode.FR` | France | | `ProxyCountryCode.JP` | Japan | | `ProxyCountryCode.AU` | Australia | ### Verify what proxy was actually used After a run, `browser_config` on the run object confirms what was applied: ```python theme={null} run = client.runs.get("run_abc123") print(run.browser_config.proxy_enabled) # True/False print(run.browser_config.proxy_country_code) # "US" or None ``` ### Full example with both applied ```python theme={null} from tinyfish import TinyFish, BrowserProfile, ProxyConfig, ProxyCountryCode, CompleteEvent, RunStatus client = TinyFish() with client.agent.stream( goal="Extract the product name and price", url="https://protected-site.com/products", browser_profile=BrowserProfile.STEALTH, proxy_config=ProxyConfig(enabled=True, country_code=ProxyCountryCode.US), on_streaming_url=lambda e: print(f"Watch: {e.streaming_url}"), on_progress=lambda e: print(f" > {e.purpose}"), ) as stream: for event in stream: if isinstance(event, CompleteEvent): if event.status == RunStatus.COMPLETED: print("Result:", event.result_json) else: print("Failed:", event.error.message if event.error else "unknown") ``` Watch the streaming view again after this change. If the actual page loads instead of a challenge screen — you're through. Move to Step 3 to make the run more reliable at scale. TinyFish cannot solve CAPTCHAs (reCAPTCHA, hCaptcha, etc.). The configurations above — stealth mode, proxies, and human-like goal patterns — reduce the likelihood of CAPTCHAs being triggered, but if a site serves one, it's a hard limit for now. We're actively working on expanding our anti-detection capabilities. *** ## Step 3: Guide the Agent to Behave More Like a Human Stealth and proxy get you past the door. But some sites layer behavioral analysis on top of fingerprinting — they watch for robotic patterns like instant form submissions, missing cookie consent dismissals, or zero mouse dwell time. Your goal controls a lot of this behavior. ### Handle cookie and consent banners Bot detection systems often look at whether a user interacted with a consent banner before the main content. Always dismiss it explicitly: ```python theme={null} goal = """ Close any cookie consent or GDPR banner that appears before doing anything else. Then extract the product name, current price, and availability status. Return as JSON: { "name": string, "price": number, "available": boolean } """ ``` ### Add deliberate pauses at suspicious checkpoints Sites with aggressive behavioral detection (checkout pages, login flows) flag runs that move too fast: ```python theme={null} goal = """ 1. Wait for the page to fully load before interacting with anything. 2. Close any cookie banner. 3. Wait for the banner to disappear before proceeding. 4. Scroll down to view the pricing section. 5. Wait for the pricing section to fully render, then extract all plan names and monthly prices. Return as JSON array: [{ "plan": string, "price_monthly": number }] """ ``` ### Describe elements visually, not by selector Automation-aware selectors are sometimes deliberately changed to trip scrapers. Visual descriptions are more resilient: ```python theme={null} # Fragile — may be intentionally changed by the site goal = "Click the button with id='add-to-cart-btn'" # Resilient — describes what a human would see goal = "Click the blue 'Add to Cart' button directly below the product price" ``` ### Use numbered steps for multi-step flows For login flows or multi-page workflows, numbered steps give the agent explicit decision points rather than leaving it to guess: ```python theme={null} goal = """ 1. Wait for the page to fully load (spinner should disappear). 2. If a cookie consent banner is visible, click 'Accept' or 'Accept All'. 3. Locate the search bar at the top of the page and type "running shoes". 4. Wait for autocomplete suggestions to appear, then press Enter. 5. Wait for results to load. 6. Extract the first 10 results: product name, price, and product URL. Stop after 10 results. Do not paginate. Return as JSON array. """ ``` ### Add explicit fallback instructions Protected sites sometimes show intermediate pages (challenge passed, now redirecting). Tell the agent how to handle them: ```python theme={null} goal = """ Extract the product price from this page. If a loading screen or redirect page appears, wait for it to complete before extracting. If an 'Access Denied' page appears, return { "error": "access_denied" }. If the price shows 'Contact Us', return { "price": null, "contact_required": true }. Return: { "price": number or null, "currency": string } """ ``` *** ## Putting It All Together A complete hardened run for a protected site: ```python theme={null} from tinyfish import ( TinyFish, BrowserProfile, ProxyConfig, ProxyCountryCode, CompleteEvent, RunStatus, ) client = TinyFish() with client.agent.stream( url="https://protected-site.com/pricing", browser_profile=BrowserProfile.STEALTH, proxy_config=ProxyConfig(enabled=True, country_code=ProxyCountryCode.US), goal=""" 1. Wait for the page to fully load. 2. Close any cookie consent or GDPR banner that appears. 3. Wait 1 second before proceeding. 4. Locate the pricing section — it typically shows plan names in a grid or table. 5. For each plan, extract: plan name, monthly price, and annual price if shown. If a Cloudflare or security check page appears, wait for it to complete automatically. If you see an 'Access Denied' or CAPTCHA page, return { "error": "blocked" }. Do not click any purchase or checkout buttons. Return as JSON array: [{ "plan": "Pro", "monthly_price": 49, "annual_price": 39 }] """, on_streaming_url=lambda e: print(f"Watch run: {e.streaming_url}"), on_progress=lambda e: print(f" > {e.purpose}"), ) as stream: for event in stream: if isinstance(event, CompleteEvent): if event.status == RunStatus.COMPLETED: print("Result:", event.result_json) else: print("Failed:", event.error.message if event.error else "unknown") ``` *** ## Decision Tree ``` Run returned empty or wrong result? │ ├── Open streaming_url (from on_streaming_url callback or runs.get()) │ ├── Challenge / "Checking your browser" page → Anti-bot confirmed │ ├── Access Denied / 403 → Anti-bot confirmed │ ├── Blank page → Likely anti-bot (fingerprint-based) │ └── Page loaded but result wrong → Goal issue, not anti-bot │ └── Anti-bot confirmed? ├── Add browser_profile=BrowserProfile.STEALTH ├── Add proxy_config=ProxyConfig(enabled=True, country_code=ProxyCountryCode.US) ├── Re-run and watch stream again │ ├── Page loads → Add goal hardening (Step 3) for reliability at scale │ └── Still blocked → Site likely requires CAPTCHA (hard limit) └── Done ``` *** ## Creative Solutions and Iteration When a site is actively defended, the most effective approach is often to rethink the workflow rather than force the original one. **Watch yourself do it first.** Before writing your goal, navigate to the target site yourself and think through what a human would actually do. Use that as your script. The streaming view is also useful here — watch a run or two to understand exactly what the agent encounters before committing to a final goal. **Start at the front door.** Linking directly to a filtered search results page or a deep URL can look robotic. Starting at `target.com` and navigating to your destination — searching for a product, clicking through a category — often succeeds where a direct deep link fails. **Go to the source.** If the formatted data you need lives behind anti-bot and paywalls, ask whether the underlying raw data is available elsewhere. Aggregator sites are often heavily protected; their primary sources may not be. Synthesizing from multiple simpler sources is frequently more reliable than fighting for one complex one. **Check for a public API or feed first.** Some sites that actively block scraping also publish APIs, RSS feeds, or sitemaps. Five minutes checking saves a lot of iteration. **Keep dwell time low.** The longer an agent stays on a site, the higher the likelihood of detection. Balancing human-like navigation with speed matters — break large workflows into focused, smaller tasks that can be handled by multiple agents running in parallel. You get both the human-like pacing and the throughput. Scale is one of TinyFish's superpowers. **Time your runs intentionally.** Anti-bot systems are sensitive to traffic volume. Running during off-peak hours for your target site (for US-based sites, late morning to early afternoon PST often works well) can reduce the likelihood of triggering rate-based challenges. If you're testing a new workflow, start with a single run during a quiet period before scaling up. **Vary your entry points at scale.** If you're running hundreds of batch jobs against the same site, uniform traffic patterns can themselves become a fingerprint — even across different IPs. Mixing up how runs navigate to their destination (some via homepage search, some via category pages, some direct) makes the aggregate traffic look more organic. Runs have a 10-minute timeout, so this also naturally encourages breaking complex workflows into smaller, parallelizable pieces. Design goals to complete their core task well within that limit. **Manage concurrency intentionally.** TinyFish does not throttle runs by domain — if you enqueue a large batch against the same site, they will fire in parallel up to your account's concurrency limit. For sensitive sites, consider staggering your jobs in your own queuing logic rather than enqueuing everything at once. *** ## Authenticated Sessions Logging in is one of the most effective anti-bot techniques. Logged-in users are treated very differently by most protection systems, so an authenticated session often sails past challenges that block anonymous traffic — and it unlocks gated content at the same time. [Connect your password manager](/vault-setup), then set `use_vault: true` on a run and TinyFish signs in with the credentials you select. See [Vault Credentials](/key-concepts/credentials) for details. Vault credentials are supported across the Playground, REST API, Python SDK, TypeScript SDK, and CLI. See [Connect Your Vault](/vault-setup) to get started. *** ## What's Coming TinyFish continuously improves browser behavior and anti-detection performance across the web. If a site blocked you on a previous project, it's worth trying again — the same run that failed before may work without any changes on your end. We're also actively expanding our anti-detection capabilities. *** ## Need Help? If you're stuck on a specific site, share the URL and your current configuration with us — we can often diagnose the issue quickly. * **Email:** [support@tinyfish.io](mailto:support@tinyfish.io) * **Discord:** [discord.gg/tinyfish](https://discord.gg/tinyfish) # Attach to research progress Source: https://docs.tinyfish.ai/api-reference/attach-to-research-progress https://agent.tinyfish.ai/v1/openapi/research get /v1/research-run/{research_run_id}/events Reconnect with Last-Event-ID. Disconnecting does not cancel research. Retained events expire; poll the run for durable results. # Run browser automation synchronously Source: https://docs.tinyfish.ai/api-reference/automation/run-browser-automation-synchronously https://agent.tinyfish.ai/v1/openapi/main post /v1/automation/run Execute a browser automation task synchronously and wait for completion. Returns the final result once the automation finishes (success or failure). Use this endpoint when you need the complete result in a single response. Note: Runs created via this endpoint cannot be cancelled. If you need cancellation support, use `/v1/automation/run-async` or `/v1/automation/run-sse` instead. # Run browser automation with SSE streaming Source: https://docs.tinyfish.ai/api-reference/automation/run-browser-automation-with-sse-streaming https://agent.tinyfish.ai/v1/openapi/main post /v1/automation/run-sse Execute a browser automation task with Server-Sent Events (SSE) streaming. Returns a real-time event stream with automation progress, browser streaming URL, and final results. # Start automation asynchronously Source: https://docs.tinyfish.ai/api-reference/automation/start-automation-asynchronously https://agent.tinyfish.ai/v1/openapi/main post /v1/automation/run-async Creates and enqueues an automation run, returning the run_id immediately without waiting for completion. Use this for long-running automations where you want to poll for results separately. # Start multiple automations asynchronously Source: https://docs.tinyfish.ai/api-reference/automation/start-multiple-automations-asynchronously https://agent.tinyfish.ai/v1/openapi/main post /v1/automation/run-batch Creates and enqueues multiple automation runs in a single request, returning run_ids immediately without waiting for completion. Maximum 100 runs per request. **Atomic creation:** Run creation is all-or-nothing. Either all runs are created successfully, or none are (returns error). **Idempotency:** This endpoint does not currently support idempotency keys. Retrying a failed request may create duplicate runs. # Cancel a profile setup session Source: https://docs.tinyfish.ai/api-reference/browser-context-profiles/cancel-a-profile-setup-session https://agent.tinyfish.ai/v1/openapi/main post /v1/profiles/{profileId}/setup-session/cancel Cancels a running Browser Context Profile setup session and discards unsaved setup state. Requires the session_id returned by setup-session. # Create a Browser Context Profile Source: https://docs.tinyfish.ai/api-reference/browser-context-profiles/create-a-browser-context-profile https://agent.tinyfish.ai/v1/openapi/main post /v1/profiles Create a Browser Context Profile # Create a profile setup session Source: https://docs.tinyfish.ai/api-reference/browser-context-profiles/create-a-profile-setup-session https://agent.tinyfish.ai/v1/openapi/main post /v1/profiles/{profileId}/setup-session Starts a temporary setup browser for a Browser Context Profile. The response returns session_id, cdp_url, base_url, timeout_seconds, and expires_at. Connect your Playwright, Puppeteer, or CDP client to cdp_url, log in, then save the setup session with session_id. Unsaved setup state is discarded if the session is cancelled or times out. # Delete a Browser Context Profile Source: https://docs.tinyfish.ai/api-reference/browser-context-profiles/delete-a-browser-context-profile https://agent.tinyfish.ai/v1/openapi/main delete /v1/profiles/{profileId} Delete a Browser Context Profile # Delete a saved profile domain Source: https://docs.tinyfish.ai/api-reference/browser-context-profiles/delete-a-saved-profile-domain https://agent.tinyfish.ai/v1/openapi/main delete /v1/profiles/{profileId}/domains/{domain} Delete a saved profile domain # Get a Browser Context Profile Source: https://docs.tinyfish.ai/api-reference/browser-context-profiles/get-a-browser-context-profile https://agent.tinyfish.ai/v1/openapi/main get /v1/profiles/{profileId} Get a Browser Context Profile # List Browser Context Profiles Source: https://docs.tinyfish.ai/api-reference/browser-context-profiles/list-browser-context-profiles https://agent.tinyfish.ai/v1/openapi/main get /v1/profiles List Browser Context Profiles # Save a profile setup session Source: https://docs.tinyfish.ai/api-reference/browser-context-profiles/save-a-profile-setup-session https://agent.tinyfish.ai/v1/openapi/main post /v1/profiles/{profileId}/save Captures cookies, local storage, and session storage from a running setup session into the Browser Context Profile. Requires the session_id returned by setup-session. # Update a Browser Context Profile Source: https://docs.tinyfish.ai/api-reference/browser-context-profiles/update-a-browser-context-profile https://agent.tinyfish.ai/v1/openapi/main patch /v1/profiles/{profileId} Update a Browser Context Profile # Upload Browser Context Profile state Source: https://docs.tinyfish.ai/api-reference/browser-context-profiles/upload-browser-context-profile-state https://agent.tinyfish.ai/v1/openapi/main post /v1/profiles/{profileId}/upload Upload Browser Context Profile state # Cancel research run by ID Source: https://docs.tinyfish.ai/api-reference/cancel-research-run-by-id https://agent.tinyfish.ai/v1/openapi/research post /v1/research-run/{research_run_id}/cancel Cancel a running research run. Idempotent: an already-terminal run returns 200 with cancelled false and its current status. # Create a remote browser session Source: https://docs.tinyfish.ai/api-reference/create-a-remote-browser-session https://agent.tinyfish.ai/v1/openapi/browser post / Creates a remote tf-browser session and returns CDP connection details. Connect to the browser via CDP WebSocket at `cdp_url`. By default the session egresses through the TinyFish residential proxy in the US, pinned to one exit IP for the life of the session. Use `proxy_config` to pick another country (`country_code`, any ISO 3166-1 alpha-2), bring your own proxy (`type: "custom"`), or disable the proxy (`enabled: false`). Use `timeout_seconds` to set a custom inactivity timeout (5–86400 seconds). If omitted, null, or greater than your plan maximum, the plan maximum is used (15 min on free tier, 60 min on paid). # Fetch and extract content from URLs Source: https://docs.tinyfish.ai/api-reference/fetch-and-extract-content-from-urls https://agent.tinyfish.ai/v1/openapi/fetch post / Fetches web pages, renders JavaScript-heavy pages when needed, and returns clean extracted content in your preferred format. Submit up to 10 URLs, get back structured content. Per-URL failures appear in `errors[]` and do not fail the entire request. **Per-URL error codes** (in `errors[].error`): - `target_http_error` — target server returned a non-2xx HTTP status other than 404/410; the raw status code is in `errors[].status` - `page_not_found` — target URL returned HTTP 404 or 410; the raw status code is in `errors[].status` - `target_unreachable` — connection refused, TLS failure, DNS failure, or other network error - `timeout` — request timed out - `proxy_error` — proxy tunnel failure - `bot_blocked` — bot-challenge page detected (Cloudflare, etc.) - `empty_content` — page loaded but no extractable text was found - `login_required` — the target redirected to a login or account wall; retrying without credentials will not help - `content_too_large` — the document exceeds the 20MB size limit for generic documents (PDF and CSV have their own 50MB budgets); not retried in the browser - `invalid_url` — malformed URL or SSRF-blocked address - `invalid_redirect_url` — redirect target rejected before fetch - `conditional_unsupported` — conditional requests (`if_none_match` / `if_modified_since`) are supported on the fast path only; this URL requires browser rendering - `selector_not_matched` — no elements matching any `include_selectors` entry remained after `exclude_selectors` was applied; the error carries `unmatched_selectors` plus `candidate_selectors` retry hints (a partial miss is not an error — it's reported on the result's `unmatched_selectors`) - `selector_unsupported` — `include_selectors` / `exclude_selectors` sent for a URL that resolves to a direct PDF/CSV download (no HTML to scope) # Get research run by ID Source: https://docs.tinyfish.ai/api-reference/get-research-run-by-id https://agent.tinyfish.ai/v1/openapi/research get /v1/research-run/{research_run_id} Retrieve one saved research run, including the plan, evidence, and per-iteration working state. # List browser session usage Source: https://docs.tinyfish.ai/api-reference/list-browser-session-usage https://agent.tinyfish.ai/v1/openapi/browser get /usage List Tetra browser session usage for the authenticated user. Returns session telemetry including duration, data transfer, mode, and status. # List fetch usage Source: https://docs.tinyfish.ai/api-reference/list-fetch-usage https://agent.tinyfish.ai/v1/openapi/fetch get /usage List fetch usage records for the authenticated user. Returns paginated results with URL, format, status, and metadata for each fetch operation. # List research runs Source: https://docs.tinyfish.ai/api-reference/list-research-runs https://agent.tinyfish.ai/v1/openapi/research get /v1/research-run List saved research runs with optional filtering by status, query text, and date range. Cursors are only valid with the sort direction used to generate them. # List search usage Source: https://docs.tinyfish.ai/api-reference/list-search-usage https://agent.tinyfish.ai/v1/openapi/search get /usage List search usage records for the authenticated user. Returns paginated results with query details, status, and result counts. # Run research with SSE streaming Source: https://docs.tinyfish.ai/api-reference/run-research-with-sse-streaming https://agent.tinyfish.ai/v1/openapi/research post /v1/automation/run-research Search, evaluate sources, and synthesize a cited report from a natural-language query. Accepted requests always return a Server-Sent Events stream, including when stream is false. # Cancel multiple runs by IDs Source: https://docs.tinyfish.ai/api-reference/runs/cancel-multiple-runs-by-ids https://agent.tinyfish.ai/v1/openapi/main post /v1/runs/batch/cancel Cancel multiple runs by their IDs in a single request. Returns per-run results including cancelled runs, already-terminal runs, and not-found IDs. Maximum 100 IDs per request. Idempotent: calling twice returns consistent results. # Cancel run by ID Source: https://docs.tinyfish.ai/api-reference/runs/cancel-run-by-id https://agent.tinyfish.ai/v1/openapi/main post /v1/runs/{id}/cancel Cancel a run by ID. Only runs created via `/v1/automation/run-async` or `/v1/automation/run-sse` can be cancelled. Runs created via the synchronous `/v1/automation/run` endpoint cannot be cancelled. # Get multiple runs by IDs Source: https://docs.tinyfish.ai/api-reference/runs/get-multiple-runs-by-ids https://agent.tinyfish.ai/v1/openapi/main post /v1/runs/batch Retrieve multiple runs by their IDs in a single request. Returns found runs and lists any IDs that were not found or not owned. Maximum 100 IDs per request. # Get run by ID Source: https://docs.tinyfish.ai/api-reference/runs/get-run-by-id https://agent.tinyfish.ai/v1/openapi/main get /v1/runs/{id} Get detailed information about a specific automation run by its ID. # Get step HTML snapshot Source: https://docs.tinyfish.ai/api-reference/runs/get-step-html-snapshot https://agent.tinyfish.ai/v1/openapi/main get /v1/runs/{id}/steps/{stepId}/html Serve the full-page HTML snapshot captured at a specific step via API key. Returns raw HTML. Only available when the run was started with capture_config.html: true. # Get step screenshot Source: https://docs.tinyfish.ai/api-reference/runs/get-step-screenshot https://agent.tinyfish.ai/v1/openapi/main get /v1/runs/{id}/steps/{stepId}/screenshot Serve the JPEG screenshot captured at a specific step via API key. Returns binary image data. Note: screenshots=url on GET /v1/runs/{id} returns browser-accessible URLs at /runs/{id}/steps/{stepId}/screenshot (session auth). # List and search runs Source: https://docs.tinyfish.ai/api-reference/runs/list-and-search-runs https://agent.tinyfish.ai/v1/openapi/main get /v1/runs List automation runs with optional filtering by status, goal text, and date range. Returns paginated results with total count. Default sort order is newest first. # Search the web Source: https://docs.tinyfish.ai/api-reference/search-the-web https://agent.tinyfish.ai/v1/openapi/search get / Search the web and get structured results. Returns ranked results with titles, snippets, and URLs. **Location and language resolution:** - If `location` is set but `language` is not, the language auto-resolves to the most predominantly used language in that country. - If `language` is set but `location` is not, the location auto-resolves to the country where that language is most predominantly used. - If neither `location` nor `language` is set, defaults to `US` and `en`. # Start research asynchronously Source: https://docs.tinyfish.ai/api-reference/start-research-asynchronously https://agent.tinyfish.ai/v1/openapi/research post /v1/automation/run-research-async Enqueue research and return its ID after durable dispatch. Poll GET /v1/research-run/{id} or attach to its events endpoint. # Terminate a browser session Source: https://docs.tinyfish.ai/api-reference/terminate-a-browser-session https://agent.tinyfish.ai/v1/openapi/browser delete /{sessionId} Terminates a remote browser session immediately. If the session is already ended this call is idempotent and still returns 204. # Connect a vault provider Source: https://docs.tinyfish.ai/api-reference/vault/connect-a-vault-provider https://agent.tinyfish.ai/v1/openapi/main post /v1/vault/connections Connect a supported password manager and immediately sync display-safe credential metadata. # Disconnect a vault provider Source: https://docs.tinyfish.ai/api-reference/vault/disconnect-a-vault-provider https://agent.tinyfish.ai/v1/openapi/main delete /v1/vault/connections/{connectionId} Disconnect a vault provider and remove its stored enabled items. # List vault connections Source: https://docs.tinyfish.ai/api-reference/vault/list-vault-connections https://agent.tinyfish.ai/v1/openapi/main get /v1/vault/connections List all connected vault providers for the authenticated user. # List vault items Source: https://docs.tinyfish.ai/api-reference/vault/list-vault-items https://agent.tinyfish.ai/v1/openapi/main get /v1/vault/items List all vault items currently available from connected providers. # Sync vault items Source: https://docs.tinyfish.ai/api-reference/vault/sync-vault-items https://agent.tinyfish.ai/v1/openapi/main post /v1/vault/items/sync Sync items from connected providers and return merged item state plus sync counters. # Get wallet Source: https://docs.tinyfish.ai/api-reference/wallet/get-wallet https://agent.tinyfish.ai/v1/openapi/main get /v1/wallet Get the caller's wallet: current balance, auto-reload configuration, per-product contract rates, and any in-flight top-up. # Top up wallet (agent-paid) Source: https://docs.tinyfish.ai/api-reference/wallet/top-up-wallet-agent-paid https://agent.tinyfish.ai/v1/openapi/main post /v1/wallet/top-up Add money to the wallet, paid by the calling agent over the Machine Payments Protocol (MPP). The first call answers 402 with a payment challenge in WWW-Authenticate; retry with a Payment-Authorization credential to settle the charge and credit the wallet. # Authentication Source: https://docs.tinyfish.ai/authentication How to authenticate with the TinyFish API TinyFish uses different authentication methods depending on how you're accessing the API: | Access Method | Auth Type | When to Use | | - | - | - | | REST API | API Key | Direct HTTP requests from your code | | MCP Integration | OAuth 2.1 | AI assistants (Claude, Cursor) | *** ## REST API Authentication All REST API requests require an API key passed in the `X-API-Key` header. ### Getting Your API Key Visit [agent.tinyfish.ai/api-keys](https://agent.tinyfish.ai/api-keys) Click "Create API Key" Copy and store your key securely API keys are shown only once. Store them securely and never commit them to version control. ### Using Your API Key Pass your API key when making requests. The Python SDK reads `TINYFISH_API_KEY` from your environment automatically: ```python Python theme={null} from tinyfish import TinyFish client = TinyFish() # Reads TINYFISH_API_KEY from environment result = client.agent.run( url="https://example.com", goal="Extract the page title", ) ``` ```typescript TypeScript theme={null} import { TinyFish } from "@tiny-fish/sdk"; const client = new TinyFish(); // Reads TINYFISH_API_KEY from environment const response = await client.agent.run({ url: "https://example.com", goal: "Extract the page title", }); ``` ```bash cURL theme={null} curl -X POST https://agent.tinyfish.ai/v1/automation/run \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com", "goal": "Extract the page title"}' ``` ### Environment Variables Store your API key in an environment variable: ```bash theme={null} # Add to your shell profile (.bashrc, .zshrc, etc.) export TINYFISH_API_KEY="your_api_key_here" ``` For Node.js projects, use a `.env` file: ```bash theme={null} # .env TINYFISH_API_KEY=your_api_key_here ``` Add `.env` to your `.gitignore` to prevent accidental commits. *** ## MCP Authentication The MCP endpoint uses OAuth 2.1 for secure authentication with AI assistants. ### How It Works Add the TinyFish MCP server to your AI client configuration. See the [MCP Integration guide](/mcp-integration) for setup instructions. When you first use the tool, a browser window opens for authentication Log in with your TinyFish account Authorization is cached for future sessions You need a TinyFish account. Search and Fetch are free within a daily allowance; Agent and Browser draw from your wallet (plan credits on legacy accounts). [Sign up here](https://agent.tinyfish.ai/api-keys). *** ## Error Responses Authentication errors return standard HTTP status codes with a JSON error body. See [Error Codes](/error-codes) for the full reference. The request is missing the `X-API-Key` header. ```json theme={null} { "error": { "code": "MISSING_API_KEY", "message": "X-API-Key header is required" } } ``` **How to fix:** * Add the `X-API-Key` header to your request * Check the header name is exactly `X-API-Key` (case-sensitive) The API key in the request is not valid. ```json theme={null} { "error": { "code": "INVALID_API_KEY", "message": "The provided API key is invalid" } } ``` **How to fix:** * Verify your API key is correct * Ensure no extra whitespace around the key * Check if the key has been revoked or regenerated ```bash theme={null} # Debug: Check your key is set echo $TINYFISH_API_KEY # Debug: Test authentication curl -I -X POST https://agent.tinyfish.ai/v1/automation/run \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com", "goal": "test"}' ``` Authentication succeeded, but you lack credits or an active subscription. This applies to legacy credit/subscription accounts — wallet accounts get a 402 instead, see below. ```json theme={null} { "error": { "code": "FORBIDDEN", "message": "Insufficient credits or no active subscription" } } ``` **How to fix:** * Check your account at [agent.tinyfish.ai/api-keys](https://agent.tinyfish.ai/api-keys) * Add credits to your account Your TinyFish wallet balance is too low to start this run. Non-retryable until money is added. ```json theme={null} { "error": { "code": "INSUFFICIENT_CREDITS", "message": "Run not started: the user's TinyFish wallet balance is too low ($-2.50). This is not retryable and will keep failing until money is added. Tell the user to add money at https://agent.tinyfish.ai/wallet (minimum top-up $10), then try again.", "details": { "status": "failed", "error": "Run not started: the user's TinyFish wallet balance is too low ($-2.50). This is not retryable and will keep failing until money is added. Tell the user to add money at https://agent.tinyfish.ai/wallet (minimum top-up $10), then try again.", "reason": "wallet_out_of_funds", "non_retryable": true, "balance": "-2.50", "currency": "usd", "wallet_url": "https://agent.tinyfish.ai/wallet", "minimum_top_up": "10.00", "auto_reload_needs_payment_fix": false } } } ``` **How to fix:** * Check your balance with `GET /v1/wallet` (or `client.wallet.get()`, `tinyfish wallet status`) * Add money at [agent.tinyfish.ai/wallet](https://agent.tinyfish.ai/wallet) — minimum top-up is \$10 * If `auto_reload_needs_payment_fix` is `true`, auto-reload is also paused on a declined payment method — update it on the same page Not sure whether your account is on wallet or legacy billing? `GET /v1/wallet` returns 404 with `error.code: "FEATURE_NOT_AVAILABLE"` for legacy accounts — see [Error Codes](/error-codes#feature-not-available) for the full discriminator. *** ## Security Best Practices Never hardcode API keys in source code Regenerate keys periodically and after team changes Use separate keys for development and production Review API usage in your dashboard for anomalies *** ## Related Run your first automation Full error code reference # Browser Examples Source: https://docs.tinyfish.ai/browser-api/examples Real-world examples for the Browser API ## Create a Session Start a remote browser session and get the CDP connection URL: ```python Python theme={null} from tinyfish import TinyFish client = TinyFish() session = client.browser.sessions.create(url="https://example.com") print(f"Session: {session.session_id}") print(f"CDP URL: {session.cdp_url}") print(f"Base URL: {session.base_url}") ``` ```typescript TypeScript theme={null} import { TinyFish } from "@tiny-fish/sdk"; const client = new TinyFish(); const session = await client.browser.sessions.create({ url: "https://example.com", }); console.log(`Session: ${session.session_id}`); console.log(`CDP URL: ${session.cdp_url}`); console.log(`Base URL: ${session.base_url}`); ``` ```bash cURL theme={null} curl -s -X POST https://api.browser.tinyfish.ai \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com"}' ``` **Output:** ```json theme={null} { "session_id": "tf-84ac6714-0f08-4592-b626-e51105a079f9", "cdp_url": "wss://ip-52-53-175-49.tetra-data.tinyfish.io/tf-84ac6714-...", "base_url": "https://ip-52-53-175-49.tetra-data.tinyfish.io/tf-84ac6714-..." } ``` ## Connect with Playwright Create a session and control it with Playwright via CDP: ```python Python theme={null} import asyncio from playwright.async_api import async_playwright from tinyfish import TinyFish async def main(): client = TinyFish() session = client.browser.sessions.create(url="https://scrapeme.live/shop") async with async_playwright() as p: browser = await p.chromium.connect_over_cdp(session.cdp_url) page = browser.contexts[0].pages[0] await asyncio.sleep(2) await page.wait_for_load_state("domcontentloaded") title = await page.title() print(f"Page title: {title}") await page.screenshot(path="shop.png") print("Screenshot saved to shop.png") asyncio.run(main()) ``` ```typescript TypeScript theme={null} import { chromium } from "playwright"; import { TinyFish } from "@tiny-fish/sdk"; async function main() { const client = new TinyFish(); const session = await client.browser.sessions.create({ url: "https://scrapeme.live/shop", }); const browser = await chromium.connectOverCDP(session.cdp_url); const page = browser.contexts()[0].pages()[0]; await page.waitForTimeout(2000); await page.waitForLoadState("domcontentloaded"); const title = await page.title(); console.log(`Page title: ${title}`); await page.screenshot({ path: "shop.png" }); console.log("Screenshot saved to shop.png"); } main(); ``` Session creation takes 10-30 seconds. Set your HTTP client timeout to at least 60 seconds. The browser navigates to the URL asynchronously — wait for `domcontentloaded` before interacting. ## Scrape with Playwright Extract structured data from a page using Playwright selectors: ```python Python theme={null} import asyncio, json from playwright.async_api import async_playwright from tinyfish import TinyFish async def main(): client = TinyFish() session = client.browser.sessions.create(url="https://scrapeme.live/shop") async with async_playwright() as p: browser = await p.chromium.connect_over_cdp(session.cdp_url) page = browser.contexts[0].pages[0] await asyncio.sleep(2) await page.wait_for_load_state("domcontentloaded") products = await page.evaluate(""" () => [...document.querySelectorAll('.product')].slice(0, 5).map(el => ({ name: el.querySelector('.woocommerce-loop-product__title')?.textContent, price: el.querySelector('.price .amount')?.textContent, })) """) print(json.dumps(products, indent=2)) asyncio.run(main()) ``` ```typescript TypeScript theme={null} import { chromium } from "playwright"; import { TinyFish } from "@tiny-fish/sdk"; async function main() { const client = new TinyFish(); const session = await client.browser.sessions.create({ url: "https://scrapeme.live/shop", }); const browser = await chromium.connectOverCDP(session.cdp_url); const page = browser.contexts()[0].pages()[0]; await page.waitForTimeout(2000); await page.waitForLoadState("domcontentloaded"); const products = await page.evaluate(() => [...document.querySelectorAll(".product")].slice(0, 5).map((el) => ({ name: el.querySelector(".woocommerce-loop-product__title")?.textContent, price: el.querySelector(".price .amount")?.textContent, })) ); console.log(JSON.stringify(products, null, 2)); } main(); ``` **Output:** ```json theme={null} [ { "name": "Bulbasaur", "price": "£63.00" }, { "name": "Ivysaur", "price": "£87.00" }, { "name": "Venusaur", "price": "£105.00" }, { "name": "Charmander", "price": "£48.00" }, { "name": "Charmeleon", "price": "£165.00" } ] ``` ## Browse from a Specific Country Pass `proxy_config.country_code` to exit from any country (ISO 3166-1 alpha-2). The session keeps the same residential IP across all of its connections: ```python Python theme={null} import asyncio, os import httpx from playwright.async_api import async_playwright async def main(): resp = httpx.post( "https://api.browser.tinyfish.ai", headers={"X-API-Key": os.environ["TINYFISH_API_KEY"]}, json={"url": "https://ipinfo.io/json", "proxy_config": {"country_code": "JP"}}, timeout=60, ) resp.raise_for_status() session = resp.json() async with async_playwright() as p: browser = await p.chromium.connect_over_cdp(session["cdp_url"]) page = browser.contexts[0].pages[0] await page.wait_for_load_state("domcontentloaded") print(await page.inner_text("body")) # "country": "JP" asyncio.run(main()) ``` ```typescript TypeScript theme={null} import { chromium } from "playwright"; const res = await fetch("https://api.browser.tinyfish.ai", { method: "POST", headers: { "X-API-Key": process.env.TINYFISH_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://ipinfo.io/json", proxy_config: { country_code: "JP" }, }), }); const session = await res.json(); const browser = await chromium.connectOverCDP(session.cdp_url); const page = browser.contexts()[0].pages()[0]; await page.waitForLoadState("domcontentloaded"); console.log(await page.innerText("body")); // "country": "JP" ``` To route through your own proxy instead, send `{"type": "custom", "url": "http://proxy.example.com:8080", "username": "...", "password": "..."}`. To connect without a proxy, send `{"enabled": false}`. See the [reference](/browser-api/reference#parameters) for all fields. *** ## Related Session parameters, lifecycle, and debugging Standard and stealth browser modes # Browser API Source: https://docs.tinyfish.ai/browser-api/index Create a remote browser session and control it programmatically The Browser API creates a remote browser session and returns a WebSocket connection URL. Use it when you need direct, low-level browser control — scripting page interactions, running your own automation framework, or tasks that go beyond the [automation run API](/key-concepts/endpoints). Browser API sessions are isolated and do not persist cookies or storage by themselves. For reusable login state, use [Browser Context Profiles](/agent-api/browser-context-profiles): create a setup session, connect your controller to `cdp_url`, save it, then run Agent API automations with `use_profile: true`. ```bash theme={null} POST https://api.browser.tinyfish.ai ``` *** ## Before You Start Create a key at [agent.tinyfish.ai/api-keys](https://agent.tinyfish.ai/api-keys). ```bash theme={null} export TINYFISH_API_KEY="your_api_key_here" ``` ```bash Python theme={null} pip install -U tinyfish playwright playwright install chromium ``` ```bash Node theme={null} npm install @tiny-fish/sdk@latest playwright npx playwright install chromium ``` All requests require the `X-API-Key` header. See [Authentication](/authentication) for the full setup and troubleshooting guide. ## Your First Request Session creation typically takes 10-30 seconds. Set your HTTP client timeout to at least 60 seconds. ```python Python theme={null} from tinyfish import TinyFish client = TinyFish() session = client.browser.sessions.create(url="https://www.tinyfish.ai") print(session.session_id) print(session.cdp_url) ``` ```typescript TypeScript theme={null} import { TinyFish } from "@tiny-fish/sdk"; const client = new TinyFish(); const session = await client.browser.sessions.create({ url: "https://www.tinyfish.ai", }); console.log(session.session_id); console.log(session.cdp_url); ``` ```bash cURL theme={null} curl -X POST https://api.browser.tinyfish.ai \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.tinyfish.ai"}' ``` ## What Success Looks Like ```json theme={null} { "session_id": "br-a1b2c3d4-e5f6-7890-abcd-ef1234567890", "cdp_url": "wss://example.tinyfish.io/cdp", "base_url": "https://example.tinyfish.io" } ``` Then connect with Playwright using `cdp_url`: ```python Python theme={null} import asyncio from playwright.async_api import async_playwright CDP_URL = "" async def main(): async with async_playwright() as p: browser = await p.chromium.connect_over_cdp(CDP_URL) await asyncio.sleep(2) # let startup navigation settle page = browser.contexts[0].pages[0] await page.wait_for_load_state("domcontentloaded") print(await page.title()) asyncio.run(main()) ``` ```typescript TypeScript theme={null} import { chromium } from 'playwright'; const CDP_URL = ''; const browser = await chromium.connectOverCDP(CDP_URL); await new Promise(r => setTimeout(r, 2000)); // let startup navigation settle const page = browser.contexts()[0].pages()[0]; await page.waitForLoadState('domcontentloaded'); console.log(await page.title()); ``` Pass `cdp_url` (the WebSocket URL) to `connect_over_cdp`. Do not use `base_url` — it is for polling session status via `/pages`, not for Playwright connections. ## Proxies By default, every session connects through the TinyFish residential proxy in the US and keeps the same exit IP across all of its connections. Use `proxy_config` to pick another country, bring your own proxy, or connect directly: ```bash Another country theme={null} curl -X POST https://api.browser.tinyfish.ai \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://www.tinyfish.ai", "proxy_config": {"country_code": "DE"}}' ``` ```bash Your own proxy theme={null} curl -X POST https://api.browser.tinyfish.ai \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"proxy_config": {"type": "custom", "url": "http://proxy.example.com:8080", "username": "user", "password": "pass"}}' ``` ```bash No proxy theme={null} curl -X POST https://api.browser.tinyfish.ai \ -H "X-API-Key: $TINYFISH_API_KEY" \ -H "Content-Type: application/json" \ -d '{"proxy_config": {"enabled": false}}' ``` See the [reference](/browser-api/reference#parameters) for every `proxy_config` field. ## When to Use Browser vs the Other APIs * Use **Browser** when you want direct Playwright or CDP control. * Use **Agent** when TinyFish should decide the browser actions from a goal. * Use **Fetch** when you only need extracted content from one or more URLs. * Use **Search** when you need ranked search results, not a browser session. *** ## Session Lifecycle | Behavior | Details | | - | - | | **Startup navigation** | If `url` was provided at session creation, the browser navigates there immediately. The 201 response is returned before navigation completes — the page may still be loading when you connect. | | **Inactivity timeout** | Sessions automatically terminate after the inactivity timeout — `timeout_seconds` if set, otherwise your plan maximum (15 minutes on free plans, 60 minutes on paid). A session is considered inactive when no CDP commands are being sent. | | **Explicit termination** | Send `DELETE https://api.browser.tinyfish.ai/{session_id}` to end a session immediately. See the [reference](/browser-api/reference). | | **Network** | Sessions connect through the TinyFish residential proxy in the US by default, with one exit IP per session. See [Proxies](#proxies). | | **Session isolation** | Each direct Browser API session is isolated. For reusable cookies and storage, set up a [Browser Context Profile](/agent-api/browser-context-profiles) instead. | *** ## Read Next Request and response schema API key setup Persist login state for future Agent API runs Configure browser runtime behavior for automation runs Understand when to use Agent vs Browser One page that routes an agent to the right TinyFish API # Browser API Reference Source: https://docs.tinyfish.ai/browser-api/reference Complete reference for the Browser API endpoints ## Endpoints | Method | Path | Description | | - | - | - | | `POST` | `https://api.browser.tinyfish.ai` | Create a browser session | | `DELETE` | `https://api.browser.tinyfish.ai/{session_id}` | Terminate a browser session | All requests require an `X-API-Key` header. See [Authentication](/authentication). The SDKs (`tinyfish` for Python and `@tiny-fish/sdk` for TypeScript) target `https://agent.tinyfish.ai/v1/browser` by default, while the REST host documented here is `https://api.browser.tinyfish.ai`. Both hosts are supported and behave identically. Session creation typically takes 10-30 seconds. Set your HTTP client timeout to at least 60 seconds. Direct Browser API sessions are isolated; use [Browser Context Profiles](/agent-api/browser-context-profiles) when you need reusable cookies or storage. *** ## Request ```json theme={null} { "url": "https://www.tinyfish.ai", "timeout_seconds": 300, "proxy_config": { "country_code": "DE" } } ``` ### Parameters Target URL the session will navigate to on startup. Bare domains (e.g. `tinyfish.ai`) are automatically prefixed with `https://`. Omit to start at `about:blank`. Inactivity timeout in seconds (5–86400). Defaults to your account's maximum. How the session connects to the internet. Omit to use the TinyFish residential proxy in the US. | Field | Type | Default | Description | | - | - | - | - | | `enabled` | `boolean` | `true` | Set `false` to connect directly, with no proxy. Other fields are ignored. | | `type` | `"tinyfish"` \| `"custom"` | `"tinyfish"` | `tinyfish` uses the TinyFish residential proxy. `custom` routes through your own proxy. | | `country_code` | `string` | `"US"` | Exit country for the `tinyfish` proxy. Any [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) code, case-insensitive (e.g. `DE`, `jp`). Not allowed with `custom`. | | `url` | `string` | — | Your proxy URL. Required with `custom`. Must be `http://` or `https://`. | | `username` | `string` | — | Username for your custom proxy. | | `password` | `string` | — | Password for your custom proxy. | Unknown fields are rejected with `400`. ### Proxy Behavior * **Sticky exit IP.** With the `tinyfish` proxy, a session keeps the same exit IP across all its connections, including after idle periods. Different sessions get different IPs. Residential IPs can occasionally go offline; the session then moves to a new IP in the same country. * **Default.** Sessions created without `proxy_config` use the `tinyfish` proxy in the US. * **Custom proxies.** TinyFish connects to your proxy as given and does not rotate or pin its IP. Only `http://` and `https://` proxies are supported. *** ## Response On success, returns `201 Created` with a JSON body containing `session_id`, `cdp_url`, and `base_url`. ```json theme={null} { "session_id": "br-a1b2c3d4-e5f6-7890-abcd-ef1234567890", "cdp_url": "wss://example.tinyfish.io/cdp", "base_url": "https://example.tinyfish.io" } ``` Unique identifier for this session. WebSocket URL for browser connection. Pass this to Playwright's `connect_over_cdp` or any CDP client. HTTPS base URL for the session. Use to access session endpoints such as `/pages`. *** ## DELETE — Terminate a Session ```bash theme={null} DELETE https://api.browser.tinyfish.ai/{session_id} ``` Terminates the session immediately. If the session is already ended this call is a no-op and still returns `204`. ### Path Parameters The `session_id` returned when the session was created. ### Response Returns `204 No Content` with an empty body on success. ### Error Codes | HTTP Status | Error Code | Cause | | - | - | - | | 400 | `INVALID_INPUT` | `session_id` is missing or empty. | | 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Missing or invalid `X-API-Key`. | | 404 | `NOT_FOUND` | Session does not exist or belongs to a different API key. | | 409 | `RETRY_REQUIRED` | Conflicting operation on the session. Termination not confirmed — retry the delete shortly. | | 429 | `RATE_LIMIT_EXCEEDED` | Browser infrastructure rate limit. Honor `Retry-After`. | | 500 | `INTERNAL_ERROR` | Unexpected or non-retryable infrastructure failure. Termination is not confirmed — the session may still be running; retry the delete. | | 503 | `SERVICE_BUSY` | Browser infrastructure temporarily unavailable. Termination not confirmed — retry, honoring `Retry-After`. | | 504 | `TIMEOUT` | Browser infrastructure timed out terminating the session. Retry. | Infrastructure errors include `error.details.upstream_status` — the backend status when one was received, otherwise the status TinyFish synthesized (502 for transport failures, 504 for timeouts). Every error also carries `request_id`, echoed in the `X-Request-ID` header; quote it when contacting support. *** ## Debugging — Open DevTools Inspector Poll `GET {base_url}/pages` and open the `devtoolsFrontendUrl` of the first non-blank page to inspect the live browser session. ```python Python theme={null} async def get_inspector_url(base_url: str) -> str: async with httpx.AsyncClient() as client: for _ in range(20): pages = (await client.get(f"{base_url}/pages", timeout=5)).json() nav = next((p for p in pages if p.get("url") not in ("", "about:blank", "about:newtab")), None) if nav: return nav["devtoolsFrontendUrl"] await asyncio.sleep(1) raise TimeoutError("navigation never completed") ``` ```typescript TypeScript theme={null} async function getInspectorUrl(baseUrl: string): Promise { for (let i = 0; i < 20; i++) { const res = await fetch(`${baseUrl}/pages`); const pages: { url: string; devtoolsFrontendUrl: string }[] = await res.json(); const nav = pages.find(p => !['', 'about:blank', 'about:newtab'].includes(p.url)); if (nav) return nav.devtoolsFrontendUrl; await new Promise(r => setTimeout(r, 1000)); } throw new Error('navigation never completed'); } ``` The page starts at `about:blank` and navigates asynchronously — skip blank pages when polling to get the correct inspector URL. *** ## Session Lifecycle | Behavior | Details | | - | - | | **Startup navigation** | If `url` was provided at session creation, the browser navigates there immediately. The 201 response is returned before navigation completes — the page may still be loading when you connect. | | **Inactivity timeout** | Sessions automatically terminate after the configured inactivity timeout. A session is considered inactive when no CDP commands are being sent. | | **Explicit termination** | Send `DELETE https://api.browser.tinyfish.ai/{session_id}` to terminate a session immediately. Returns `204 No Content` on success. Deleting an already-ended session is idempotent. | | **Session isolation** | Each direct Browser API session is isolated. For reusable cookies and storage, use [Browser Context Profiles](/agent-api/browser-context-profiles). | *** ## SDK Methods ```python Python theme={null} from tinyfish import TinyFish client = TinyFish() session = client.browser.sessions.create(url="https://www.tinyfish.ai") print(session.session_id) print(session.cdp_url) ``` ```typescript TypeScript theme={null} import { TinyFish } from "@tiny-fish/sdk"; const client = new TinyFish(); const session = await client.browser.sessions.create({ url: "https://www.tinyfish.ai", }); console.log(session.session_id); console.log(session.cdp_url); ``` *** ## End-to-End Example Create a session, connect with Playwright, take a screenshot, and extract the page title. ```python Python theme={null} import asyncio from tinyfish import TinyFish from playwright.async_api import async_playwright client = TinyFish() async def main(): # 1. Create a browser session session = client.browser.sessions.create(url="https://scrapeme.live/shop") # 2. Connect via Playwright CDP async with async_playwright() as p: browser = await p.chromium.connect_over_cdp(session.cdp_url) await asyncio.sleep(2) # let startup navigation settle page = browser.contexts[0].pages[0] await page.wait_for_load_state("domcontentloaded") # 3. Interact with the page print(await page.title()) await page.screenshot(path="shop.png") # Session ends after its inactivity timeout, or DELETE it explicitly asyncio.run(main()) ``` ```typescript TypeScript theme={null} import { TinyFish } from "@tiny-fish/sdk"; import { chromium } from "playwright"; const client = new TinyFish(); // 1. Create a browser session const session = await client.browser.sessions.create({ url: "https://scrapeme.live/shop", }); // 2. Connect via Playwright CDP const browser = await chromium.connectOverCDP(session.cdp_url); await new Promise((r) => setTimeout(r, 2000)); // let startup navigation settle const page = browser.contexts()[0].pages()[0]; await page.waitForLoadState("domcontentloaded"); // 3. Interact with the page console.log(await page.title()); await page.screenshot({ path: "shop.png" }); // Session ends after its inactivity timeout, or DELETE it explicitly ``` *** ## Error Reference | HTTP Status | Error Code | Cause | Resolution | | - | - | - | - | | 400 | `INVALID_INPUT` | `url` field is not a valid URL, or `proxy_config` is invalid (unknown field, unknown `country_code`, `custom` without `url`, non-HTTP(S) proxy URL). | Check the `details` field in the error response for specifics. | | 401 | `MISSING_API_KEY` / `INVALID_API_KEY` | Missing or invalid `X-API-Key` header. | Verify your API key at the [dashboard](https://agent.tinyfish.ai/api-keys). | | 402 | `INSUFFICIENT_CREDITS` | Your TinyFish wallet balance is too low to start this run. | Check `GET /v1/wallet` and add money at the [wallet page](https://agent.tinyfish.ai/wallet) — minimum top-up \$10. | | 404 | `NOT_FOUND` | Browser API is not enabled for your account. | Contact support to enable access. | | 409 | `RETRY_REQUIRED` | Upstream conflict creating the session. | Retry after a brief delay. | | 429 | `RATE_LIMIT_EXCEEDED` | Browser infrastructure rate limit. | Back off and honor the `Retry-After` header. | | 500 | `INTERNAL_ERROR` | Unexpected server error, including non-retryable infrastructure failures. | Not resolved by retrying. If persistent, check [agent.tinyfish.ai/status](https://agent.tinyfish.ai/status). | | 503 | `SERVICE_BUSY` | Browser infrastructure temporarily unavailable. Session creation was not confirmed. | Retry, honoring the `Retry-After` header. `error.details.upstream_status` carries the backend status, or the status TinyFish synthesized when the call never completed. | | 504 | `TIMEOUT` | Browser infrastructure timed out starting the session. | Retry after a brief delay. | ## Related First request, success shape, and product routing API key setup Persist login state for future Agent API runs Full list of API error codes Understand where Browser fits in the overall API surface # CLI Commands Source: https://docs.tinyfish.ai/cli/commands Full reference for TinyFish CLI commands ## `tinyfish search query` Run a web search. Returns ranked results with titles, URLs, and snippets. ```bash theme={null} tinyfish search query "best React state management libraries" ``` ### Flags | Flag | Description | | - | - | | `--location ` | Location hint for geo-targeted results (e.g. `"Vietnam"`) | | `--language ` | Language hint (e.g. `"en"`) | | `--pretty` | Human-readable output | ### Output ```json theme={null} { "query": "best React state management libraries", "total_results": 10, "results": [ { "position": 1, "site_name": "Reddit", "title": "Best State Management for React in 2026", "url": "https://reddit.com/r/reactjs/...", "snippet": "Zustand and Jotai are the most popular choices..." } ] } ``` ### Examples ```bash Basic query theme={null} tinyfish search query "tinyfish web automation" ``` ```bash Geo-targeted theme={null} tinyfish search query "best pho in Saigon" --location "Vietnam" --language "en" ``` ```bash Human-readable theme={null} tinyfish search query "tinyfish web automation" --pretty ``` *** ## `tinyfish search usage` List your past search requests. ```bash theme={null} tinyfish search usage [--status completed|failed] [--limit ] [--page ] ``` ### Flags | Flag | Description | | - | - | | `--start-after ` | Only items created after this ISO 8601 timestamp | | `--end-before ` | Only items created before this ISO 8601 timestamp | | `--status ` | Filter by status (completed\|failed) | | `--limit ` | Items per page, 1-1000 (default 100) | | `--page ` | Page number, starting from 1 | | `--pretty` | One line per item, then the page count | JSON output is the paged response: `{ "items": [...], "total", "limit", "page", "total_pages", "has_more" }`. ## `tinyfish fetch content get` Fetch clean, extracted content from one or more URLs. Strips ads, navigation, and boilerplate — returns just the content. ```bash theme={null} tinyfish fetch content get "https://example.com/article" ``` Accepts up to 10 URLs in a single call. Multiple URLs are fetched in parallel server-side. ### Flags | Flag | Description | | - | - | | `--format ` | Output format: `markdown` (default), `html`, or `json` | | `--links` | Include extracted links from the page | | `--image-links` | Include extracted image URLs | | `--per-url-timeout-ms ` | Per-URL timeout budget, from `1` to `110000` milliseconds | | `--pretty` | Human-readable output | ### Output ```json theme={null} { "results": [ { "url": "https://example.com/article", "final_url": "https://example.com/article/", "title": "Article Title", "description": "A brief description", "language": "en", "author": "Jane Doe", "published_date": "2026-01-15", "format": "markdown", "text": "# Article Title\n\nThe full article content in clean markdown...", "latency_ms": 1430.39 } ], "errors": [] } ``` When `--links` or `--image-links` are set, each result also includes: ```json theme={null} { "links": ["https://example.com/about", "https://example.com/contact"], "image_links": ["https://example.com/hero.png"] } ``` ### Examples ```bash Single URL theme={null} tinyfish fetch content get --format markdown "https://example.com/article" ``` ```bash Multiple URLs theme={null} tinyfish fetch content get "https://site-a.com" "https://site-b.com" "https://site-c.com" ``` ```bash With link extraction theme={null} tinyfish fetch content get --links --image-links "https://example.com" ``` ```bash Custom timeout theme={null} tinyfish fetch content get --per-url-timeout-ms 45000 "https://example.com" ``` ```bash HTML format theme={null} tinyfish fetch content get --format html "https://example.com" ``` *** ## `tinyfish fetch usage` List your past fetch requests. ```bash theme={null} tinyfish fetch usage [--status completed|failed] [--limit ] [--page ] ``` ### Flags | Flag | Description | | - | - | | `--start-after ` | Only items created after this ISO 8601 timestamp | | `--end-before ` | Only items created before this ISO 8601 timestamp | | `--status ` | Filter by status (completed\|failed) | | `--limit ` | Items per page, 1-1000 (default 100) | | `--page ` | Page number, starting from 1 | | `--pretty` | One line per item, then the page count | JSON output is the paged response: `{ "items": [...], "total", "limit", "page", "total_pages", "has_more" }`. ## `tinyfish agent run` Execute a browser automation. By default the output streams as newline-delimited JSON — one object per event. ```bash theme={null} tinyfish agent run "goal" --url example.com ``` ### Flags | Flag | Description | | - | - | | `--url ` | Target URL to automate (required) | | `--sync` | Wait for the run to complete and return the full result | | `--async` | Submit only — return the `run_id` immediately without waiting | | `--output-schema ` | Inline JSON Schema object for structured output | | `--output-schema-file ` | Path to a JSON file containing the output schema | | `--browser-profile ` | Browser profile for execution (`lite` or `stealth`) | | `--max-steps ` | Maximum tool-call steps before stopping (`1`-`500`) | | `--mode ` | Agent behavior mode (`default` or `strict`). `strict` is in **beta** — join the beta to use it (contact support to enable it). | | `--max-duration-seconds ` | Maximum wall-clock seconds before stopping | | `--cursor-style