Browsa API
The identity layer for AI agents. Provision a CDP-attached cloud browser identity in two seconds; drive it with Playwright, Puppeteer, or raw CDP. Or skip the wiring entirely and tell us what to do in plain English — the agent does the rest.
Two modes, one endpoint
- Burner — ephemeral, 5 min–1 hour TTL, self-destructs, pay-per-call. Best for scrape jobs, one-off automations, and CI runs.
- Persistent — durable, vault-synced profile (cookies, localStorage, fingerprint), monthly-billed, sessions opened on demand. Best for long-running agents that maintain logged-in state across runs.
https://browsa.io.
Authenticate with X-API-Key: agt_live_… — mint one in the
dashboard.
Quickstart
Three lines to a cloud browser the agent can drive. No infrastructure, no fingerprint tuning, no proxies to wire.
# Provision a burner agent — returns CDP URL you can attach to. curl https://browsa.io/v1/agents \ -H "X-API-Key: agt_live_…" \ -H "Content-Type: application/json" \ -d '{"ttl":"10m","initial_url":"https://example.com"}'
# pip install browsa from browsa import Agents from playwright.sync_api import sync_playwright client = Agents(api_key="agt_live_…") agent = client.create(ttl="10m", initial_url="https://example.com") with sync_playwright() as p: browser = p.chromium.connect_over_cdp(agent.cdp_url) page = browser.contexts[0].pages[0] print(page.title())
// npm i browsa playwright import { Agents } from "browsa"; import { chromium } from "playwright"; const client = new Agents({ apiKey: "agt_live_…" }); const agent = await client.create({ ttl: "10m", initial_url: "https://example.com" }); const browser = await chromium.connectOverCDP(agent.cdp_url); const page = browser.contexts()[0].pages()[0]; console.log(await page.title());
That's it. The agent gets destroyed when the TTL hits zero (or when you DELETE it). You're only charged for the time it was actually running.
Authentication
Every request needs an API key in the X-API-Key header. Keys are minted in the dashboard and look like agt_live_ followed by 24 hex characters.
X-API-Key: agt_live_aabbccddeeff00112233445566778899
Bearer auth is supported too (same key shape) for clients that always set the Authorization header.
sha256(secret). If a key leaks, revoke it from the dashboard and mint a new one.
Idempotency
Every POST /v1/quick/* endpoint accepts an Idempotency-Key header (Stripe-compatible). Pass a UUID per logical request and the platform caches the full response for 24h. A retry with the same key returns the cached body byte-for-byte with Idempotent-Replay: true — no second burner provisioned, no second charge.
curl -X POST https://browsa.io/v1/quick/scrape \ -H "X-API-Key: agt_live_…" \ -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com"}'
Use this for any retry path your client library does automatically (e.g. requests.Session with backoff). Without it, a transient network blip can re-execute the scrape and charge twice.
Request IDs
Every response (success or error) carries a X-Request-Id header. Error envelopes also include request_id in the JSON body. Quote it to support@browsa.io for any incident — it short-circuits triage by ~30 minutes.
Agents
An agent is one cloud browser identity. Burner agents run for a short TTL and self-destruct; persistent agents stick around and you open sessions on them as needed.
Provision an agent
201 with CDP URL ready to attach
Mode is auto-detected: if you set persona or warmup_recipe without a TTL, the agent is provisioned persistent. Otherwise it's a burner. Override with explicit mode.
Request body
| Field | Type | Notes |
|---|---|---|
ttl | string | Duration: "5m", "300s", "1h". Burner only, capped at 1h. |
ttl_seconds | integer | Numeric alternative to ttl. |
mode | string | burner or persistent. |
initial_url | string | URL the browser navigates to on boot. |
country | string | ISO-2, e.g. "us", "de". Picks the proxy region. |
os_type | string | OS_TYPE_MACOS, OS_TYPE_WINDOWS, OS_TYPE_LINUX. Empty = backend default (macOS). |
persona | string | Persistent mode — freeform label, e.g. "us-shopper-30s". |
warmup_recipe | string | amazon, google, social, or empty. |
Response
{
"id": "agt_aabbccddeeff00112233",
"status": "running",
"mode": "burner",
"cdp_url": "wss://browsa.io/api/v1/cdp/<profile>/ws/page/<page-id>",
"cdp_http": "https://browsa.io/api/v1/cdp/<profile>",
"expires_at": "2026-06-06T15:32:00Z",
"trust_score": 920,
"cost_credits": 1
}
List agents
{agents: [...]}, newest first
Workspace-scoped automatically. Supports ?mode=burner or ?mode=persistent to filter, and ?limit=1..200 (default 50). The dashboard's Identities view is built on top of this.
Get an agent
Returns the same shape as the create response. status transitions queued → running → destroyed, or failed on a provisioning error.
Destroy an agent
Returns 204. Burner agents self-destruct on TTL expiry, so you only need this if you finish early and want to stop the meter.
Get CDP URLs
Returns cdp_url, cdp_http, profile_id, expires_at. 409 if the agent isn't currently running (destroyed, queued, or failed).
Open a session on a persistent identity
Opens a fresh cloud session against a persistent identity. The profile (cookies, localStorage, fingerprint) is restored from the vault; only the cloud session is new. Charges PersistentSessionCredits.
Trust score
Every agent returns a trust_score between 0 and 1000. It's the platform's running estimate of how likely the browser is to clear anti-bot detection at the moment it was provisioned. It rolls up the latest pass rate on our open detection harness across the five reference sites, weighted by the matching os_type / country / proxy combination.
| Score | Band | Reading |
|---|---|---|
≥ 850 | Undetected | Clears Cloudflare / Akamai / iphey baseline checks. Safe for production scraping. |
700 – 849 | Suspicious | Passes most basic checks but flagged by stricter heuristics (e.g. FPJS Pro Bot detection). Usable; expect a small failure rate. |
< 700 | Flagged | One or more detectors are firing. Try a different os_type or country, or switch the burner to persistent mode with a warmup recipe. |
The dashboard's Playground displays the score live during a task run so you can see what the platform delivered for the picks you made.
Trust score breakdown
Returns the labeled list of inputs that produced the agent's trust_score — baseline, mode, OS bonus, warmup, country penalty, age, session count, harness signal. Tells you which knob to turn to raise the score for the next provision (e.g. add a warmup_recipe, or promote burner → persistent).
curl https://browsa.io/v1/agents/agt_…/trust \ -H "X-API-Key: agt_live_…"
Fingerprint preview
Returns the navigator + screen + WebGL + timezone + canvas + audio + TLS fingerprint preview the burner will emit. Lets you pin an identity and validate it against your own anti-fingerprint test suite before running tasks. See the full per-OS spec on the anti-detect spec page.
curl https://browsa.io/v1/agents/agt_…/fingerprint \ -H "X-API-Key: agt_live_…"
Detection-harness signal
Returns the most-recent detection-harness pass rate for the identity's fingerprint shape — the empirical evidence behind the trust_score. Includes sites_tested + sites_passed + per-site verdicts when available. Returns {signal: null} when no recent harness data exists (then the score falls back to the static formula). The harness publishes new signals daily.
curl https://browsa.io/v1/agents/agt_…/detection-signal \ -H "X-API-Key: agt_live_…"
AI tasks
If you don't want to wire Playwright yourself, hand the task to us as natural language. We provision a burner, point a Claude or GPT agent at it, and return the structured result.
curl https://browsa.io/v1/tasks \ -H "X-API-Key: agt_live_…" \ -H "Content-Type: application/json" \ -d '{ "task": "On Amazon, search wireless earbuds under $50, return top 5 ASINs.", "llm": "anthropic:claude-haiku-4-5", "max_steps": 20, "initial_url": "https://www.amazon.com", "os_type": "OS_TYPE_MACOS", "country": "us" }'
Request body
| Field | Type | Notes |
|---|---|---|
task | string | Required. Natural-language goal. |
llm | string | anthropic:claude-haiku-4-5, openai:gpt-4o-mini, etc. Defaults to gpt-4.1-mini. |
llm_api_key | string | Required (BYOK). Your Anthropic / OpenAI key. Used once, never stored. |
max_steps | integer | How many browser actions before the agent gives up. Default 20, cap 100. |
initial_url | string | URL the browser opens before the agent starts. |
os_type | string | Burner fingerprint family: OS_TYPE_MACOS, OS_TYPE_WINDOWS, OS_TYPE_LINUX. Empty = default (macOS). |
country | string | ISO-2 ("us", "de", …). Routes egress through the matching geo proxy; +1 credit non-US. |
Response is a Job envelope (see Jobs) with kind: "task", plus framework, live_url (noVNC for the running burner), agent_id, and step_count. To inspect the burner's anti-detect quality, GET /v1/agents/{agent_id} for the trust_score — see Trust score below.
Quick scrape
One-call shape for "give me the content of this URL". Pass async=true for a job queue + webhook flow; otherwise the call blocks until the page is rendered (60s cap).
curl https://browsa.io/v1/quick/scrape \ -H "X-API-Key: agt_live_…" \ -H "Content-Type: application/json" \ -d '{ "url": "https://news.ycombinator.com", "formats": ["html", "links"], "async": true }'
Formats (default ["html"]):
| Value | Returns |
|---|---|
html | Rendered HTML of the landed URL as a string. |
text | Visible text only, capped at max_chars (default 100,000). |
links | Array of every href the page exposed. |
screenshot | Full-page PNG, base64-encoded in screenshot_png_b64. |
Unknown format strings are silently ignored. markdown is not currently supported — convert from html client-side if you need it.
Quick extract
Returns the rendered page's clean text plus either the JSON schema or natural-language prompt you supplied — ready for a client-side LLM extraction pass. Either schema or prompt is required (not both). The LLM step happens client-side using your BYOK key; the platform doesn't consume LLM tokens here.
curl https://browsa.io/v1/quick/extract \ -H "X-API-Key: agt_live_…" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "prompt": "return the H1 text" }'
Or use a strict schema:
{
"url": "https://example.com",
"schema": {
"type": "object",
"properties": {
"title": { "type": "string" },
"h1": { "type": "string" }
}
}
}
Response includes url (landed), text (cleaned page text), schema (echoed/derived), agent_id, trust_score, cost_credits.
Quick screenshot
Returns a base64-encoded PNG in screenshot_png_b64. Useful for visual regression / archival. Default full_page: true grabs the entire scrollable area; pass false for viewport-only.
curl https://browsa.io/v1/quick/screenshot \ -H "X-API-Key: agt_live_…" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "full_page": true }'
Quick batch
Submits up to a few hundred URLs in one call. Always async — returns a job_id; poll /v1/jobs/{id} for progress and aggregate results. Per-URL failures don't fail the batch; each result row carries its own status.
curl https://browsa.io/v1/quick/batch \ -H "X-API-Key: agt_live_…" \ -H "Content-Type: application/json" \ -d '{ "urls": [ "https://example.com", "https://example.org" ], "formats": ["text", "links"] }'
Response includes job_id, kind: "batch", progress_total (URL count), status_url, result_url. Concurrency is capped server-side; oversized batches are accepted but stream through 4-8 parallel workers per workspace.
Smart router
If you don't know whether your task needs a full LLM agent or just a plain fetch, send it to POST /v1/auto and the platform picks the fastest path. Fetch-only prompts return in ~5 seconds (skips the LLM agent entirely); interactive prompts fall through to the regular /v1/tasks async flow.
curl https://browsa.io/v1/auto \ -H "X-API-Key: agt_live_…" \ -H "Content-Type: application/json" \ -d '{"task": "Go to https://example.com and return the h1 text."}'
The response carries routed_via + routing_reason so you can branch your client code:
| routed_via | HTTP | Body | Latency |
|---|---|---|---|
"scrape" | 200 OK | Synchronous scrape result (url, http_status, html, text, agent_id, trust_score, cost_credits, duration_ms) | ~5 s |
"task" | 202 Accepted | Job envelope — same shape as POST /v1/tasks. Poll /v1/jobs/{id}. | job-dependent |
Heuristic: the platform routes to scrape when the task contains a single URL plus a fetch verb (go to, visit, fetch, return, extract, read, navigate to, open) AND has none of the interactive verbs that need a real agent: click, type, fill, submit, log in, sign up, scroll, press, select, choose, hover, search for, filter, sort by, add to cart, checkout. Any whiff of interactivity falls through to the full agent. False negatives (we routed to task when scrape would have worked) waste 25-60s; false positives (we routed to scrape when the task needed clicks) would produce a wrong answer — the heuristic is conservative on purpose.
The fetch-only path does not require an llm_api_key (no LLM call is made). The interactive path requires it, same as /v1/tasks.
Jobs
Every async call (/v1/tasks, /v1/quick/scrape with async=true, batch, crawl) creates a job that runs in the background. Poll it, or subscribe to job.completed / job.failed webhooks.
Get a job
Returns the full Job envelope. status ∈ queued | running | completed | failed | cancelled | input_required. On completed, the result object is populated with the kind-specific payload. On failed, error_code + error_message describe the failure.
Happy path — valid UUID, terminal job:
HTTP/1.1 200 OK
content-type: application/json
x-request-id: c40ac45a-34ed-48a4-b2ab-8059c3527a12
{
"id": "252e8eac-ea4f-415c-bd8f-242e33c78422",
"status": "completed",
"kind": "task",
"framework": "browser-use",
"step_count": 18,
"progress_done": 18,
"progress_total": 25,
"credits_charged": 6,
"started_at": "2026-06-08T16:18:36Z",
"completed_at": "2026-06-08T16:22:11Z",
"live_url": "https://browsa.io/stream-view/.../...",
"result": {
"final_result": "Top HN story: 'Show HN: …'",
"trust_score": 863,
"steps_taken": 18
}
}
Error envelope — same shape across every non-2xx:
GET /v1/jobs/not-a-uuid → HTTP/1.1 400 Bad Request
{
"code": "INVALID_ID",
"error": "job id must be a UUID (e.g. 252e8eac-…)",
"request_id": "1d79bb6e-91f1-4f07-8b30-f6b1567977df"
}
Every error carries code + error + request_id and the same x-request-id header — log that pair on your side; support tickets are way faster to triage with it.
Long-poll — pass ?wait=<seconds> (1–120) to block server-side until the job reaches a terminal state. Saves you a polling loop when you can afford to wait:
curl "$API/v1/jobs/$ID?wait=30" -H "X-API-Key: $KEY"
Poll lightweight status
Returns { id, status, kind, progress_done, progress_total } only — no result body. Use when you're polling in a tight loop and don't want to pull the full result every tick.
Cancel a job
The agent stops between steps once it sees the cancel flag. You're billed only for steps actually taken. Returns 409 if the job is already terminal.
Webhooks
Register a callback URL and we'll POST signed JSON every time a job hits a terminal state — no polling required.
job_id (POST /v1/tasks, /v1/quick/* with async: true, /v1/quick/batch, /v1/quick/crawl). Synchronous /v1/quick/* calls do NOT fire webhooks — the customer already has the response in-hand, so there is nothing to push. Use async: true on quick endpoints if you need webhook notifications.
curl https://browsa.io/v1/webhooks \ -H "X-API-Key: agt_live_…" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/hooks/browsa", "events": ["job.completed", "job.failed"] }'
Verify the signature
Every delivery carries an X-Agents-Signature header: an HMAC-SHA256 over the raw request body, keyed by your workspace's signing secret. Fetch the secret with GET /v1/webhooks/signing-secret (or copy it from the dashboard).
import hmac, hashlib def verify(raw_body: bytes, header: str, secret: str) -> bool: expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header)
import { createHmac, timingSafeEqual } from "crypto"; export function verify(rawBody: Buffer, header: string, secret: string): boolean { const expected = createHmac("sha256", secret).update(rawBody).digest("hex"); return timingSafeEqual(Buffer.from(expected), Buffer.from(header)); }
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" ) func verify(rawBody []byte, header, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(rawBody) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(header)) }
id field of each event payload.
Event types
Every event delivery is a POST with this envelope:
{
"id": "evt_a1b2c3d4e5f6…",
"type": "job.completed",
"created_at": "2026-06-09T08:12:44.523Z",
"workspace_id": "<uuid>",
"data": { ...event-specific shape... }
}
The full event catalogue:
| type | fires when | data shape |
|---|---|---|
agent.provisioned |
An agent finished booting and is ready to drive | {agent_id, mode, profile_id, trust_score, expires_at, cost_credits, cdp_url?, live_url?} |
agent.destroyed |
TTL expired or DELETE /v1/agents/{id} was called |
{agent_id, mode, destroyed_at, reason} |
agent.failed |
Provisioning failed before the agent reached running |
{agent_id, error_code, error_message} |
agent.session.started |
Persistent-mode: new cloud session opened on a saved profile | {agent_id, session_id, cdp_url, started_at, cost_credits} |
agent.session.ended |
Persistent-mode: cloud session ended (TTL or stop) | {agent_id, session_id, ended_at, reason} |
agent.renewed |
Persistent agent's monthly billing cycle completed | {agent_id, renewal_credits, next_renewal_at} |
agent.renewal_failed |
Persistent agent renewal failed (usually insufficient credits) — agent will be destroyed | {agent_id, error_code, error_message} |
job.completed |
Any async job (/v1/tasks, /v1/quick/* async=true, batch, crawl) reached terminal success |
{job_id, kind, progress_done, progress_total, credits_charged, result} |
job.failed |
Async job ended in failed state |
{job_id, kind, error_code, error_message} — error_code is one of the job failure codes |
webhook.test |
You called POST /v1/webhooks/{id}/ping (or /test — alias) |
{message} — synthetic ping payload |
Filtering: pass events: ["job.completed", "agent.session.started"] on registration to receive only those types. Empty array ([]) or unset = all event types delivered.
Test a delivery
POST /v1/webhooks/{id}/ping fires a synthetic webhook.test event at the URL so you can verify your handler before relying on it in production. (Aliased to POST /v1/webhooks/{id}/test; both work identically.)
Inspect deliveries
Returns the last limit (default 50, max 200) delivery attempts: event_id, event_type, status_code, delivered_at, duration_ms, error, attempt. Use this to debug a receiver that's silently 5xx'ing.
Credits
Returns your current balance in credits, plus the price table for each agent kind (burner, persistent session, task step). Fresh accounts get 100 credits on signup, no card required.
Ledger
Returns the recent ledger entries with running balance — what was spent, when, and why. Mirrors the Credits tab.
API keys
Mint, list, and revoke from the dashboard or programmatically.
sha256(secret). If you lose it, revoke + mint a new one.
Each list item carries a _links.revoke hint — an HTTP method + relative path you can hit directly. The field is present only on active keys (omitted from rows where revoked_at is non-null). Client typing pattern:
{
"id": "7a4185d4-…",
"name": "ci-pipeline",
"prefix": "agt_live_9328d",
"created_at": "2026-06-09T04:42:37Z",
"last_used_at": null,
"revoked_at": null,
"_links": {
"revoke": { "method": "DELETE", "href": "/v1/keys/7a4185d4-…" }
}
}
SDKs
Zero-dependency wrappers around the REST API with typed responses, long-poll waitForJob, HMAC webhook signature verify, and the full {code, error, request_id} envelope mapped to typed errors. Identical surface in Python and TypeScript.
Python
pip install browsa
from browsa import Client c = Client(api_key="agt_live_...") job = c.run_task( task="Go to news.ycombinator.com and return the top story title.", llm="claude-opus-4-7", llm_api_key="sk-ant-...", ) print(job.final_result) print("watch live:", job.live_url)
Source: sdks/neout-agents-python — Python 3.9+, zero deps, MIT.
TypeScript / Node
npm i browsa
import { Client } from "browsa"; const c = new Client({ apiKey: "agt_live_..." }); const job = await c.runTask({ task: "Go to news.ycombinator.com and return the top story title.", llm: "claude-opus-4-7", llmApiKey: "sk-ant-...", }); console.log(job.finalResult); console.log("watch live:", job.liveUrl);
Source: sdks/neout-agents-ts — Node 18+, zero deps, ESM + CJS, full .d.ts, MIT.
MCP server — Claude Desktop, Cursor, Cline
Drive the platform from any MCP-compatible client with one config block. The agent gets 9 tools: run_task, get_job, cancel_job, respond_to_job, credits, list_webhooks, create_webhook, delete_webhook, list_identities.
{
"mcpServers": {
"browsa": {
"command": "npx",
"args": ["-y", "@browsa/mcp"],
"env": {
"BROWSA_API_KEY": "agt_live_...",
"BROWSA_LLM_API_KEY": "sk-ant-..."
}
}
}
}
Drop this in ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows), then restart Claude Desktop. Ask: "Go to news.ycombinator.com and tell me the top story title."
Source: sdks/neout-mcp-agents — works in Claude Desktop, Cursor, Cline, Continue, Zed, MIT.
Examples
Working scripts on GitHub: python examples — quickstart, webhook receiver. TypeScript examples — same surface area.
OpenAPI spec
Import into Postman, Insomnia, Swagger UI, or any OpenAPI 3.1 client — the full surface (18 endpoints) is published at https://browsa.io/openapi.yaml.
# Generate a typed client in any language with openapi-generator-cli
npx @openapitools/openapi-generator-cli generate \
-i https://browsa.io/openapi.yaml \
-g typescript-axios -o ./neout-client
Errors
All error responses share a single envelope:
{
"error": "insufficient credit",
"code": "INSUFFICIENT_CREDIT",
"request_id": "req_01HQ8Z…"
}
| HTTP | Code | What it means |
|---|---|---|
| 400 | BAD_REQUEST | Validation failed — check the error field |
| 401 | UNAUTHORIZED / INVALID_API_KEY | Key missing, revoked, or malformed |
| 403 | FORBIDDEN | Key exists but lacks permission for this workspace |
| 404 | NOT_FOUND | Resource not in this workspace |
| 402 | INSUFFICIENT_CREDIT | Top up at /dashboard/credits |
| 409 | AGENT_NOT_RUNNING | You called /cdp on a destroyed agent |
| 429 | TOO_MANY_REQUESTS | Per-key rate limit — respect Retry-After |
| 502 | UPSTREAM_UNAVAILABLE | Cloud runner unreachable — safe to retry |
| 504 | UPSTREAM_TIMEOUT | Provisioning took too long — retry with longer client timeout |
| 500 | INTERNAL_ERROR | Our bug — quote request_id to support |
Job failure codes
When a job terminates with status="failed", the error_code field tells you why. These are different from the HTTP codes above — they describe what went wrong during agent execution, not what happened to your HTTP request. error_message carries human-readable detail.
| Code | What it means | How to fix |
|---|---|---|
STALLED | The LLM agent ran step_count steps but never called done() with a final result. error_message distinguishes "ran out of steps" (raise max_steps) from "agent decided to give up" (LLM got lost or hit anti-bot). | Raise max_steps, narrow the task wording, or use a more capable LLM (sonnet/opus instead of haiku). |
LLM_ERROR | The LLM provider returned an error on every step. Usually wrong key, wrong model name, or upstream provider rate limit. error_message includes the upstream provider's verbatim error. | Verify your BYOK key, model name, and that the provider isn't throttling you. |
TIMEOUT | The wallclock budget (default 900s) was exceeded before the agent terminated. | Pass a higher wallclock_seconds on the create request, or split the task into smaller steps. |
CANCELLED | You fired POST /v1/jobs/{id}/cancel mid-run. No charge for the partial work. | Re-submit if cancellation was unintentional. |
INSUFFICIENT_CREDIT | Workspace balance hit 0 before provisioning could complete. | Top up at /dashboard/credits. |
INVALID_API_KEY | The BYOK LLM key was rejected by the provider mid-run. | Re-check the key, base URL, and model name. |
RATE_LIMIT | The BYOK LLM provider rate-limited and exhausted our retry budget. | Lower task concurrency or upgrade the LLM-side plan. |
INTERNAL_ERROR | Unexpected platform failure during the run. | Quote the job's request_id to support@browsa.io. |
Rate limits
Today there is no enforced per-key request-rate limit — if your workspace has credits and is under the concurrent-burner cap, request volume is not metered. The platform applies two real caps:
- Concurrent burner cap — provisioning more burners than your workspace allows returns
429 TOO_MANY_REQUESTSwithRetry-After: 30. The cap rises automatically as existing burners expire (default 5min TTL). Email support to lift it. - Daily credit budget — once your balance hits 0 you get
402 INSUFFICIENT_CREDITon the next provisioning attempt.
Per-key request-rate limits are on the near-term roadmap. Until then, well-behaved clients should respect Retry-After on any 4xx/5xx response.