openapi: 3.1.0 info: title: Browsa API version: "0.1.0" description: | The identity layer for AI agents. Provision a CDP-attached cloud browser identity in 2 seconds; drive it with Playwright / Puppeteer / raw CDP. Two modes share one endpoint: - **Burner** — ephemeral, 5min–1h TTL, self-destructs, pay-per-call - **Persistent** — durable, vault-synced, monthly-billed, sessions opened on demand contact: { email: support@browsa.io } license: { name: Proprietary } servers: - url: https://browsa.io description: Production security: - ApiKeyAuth: [] - BearerAuth: [] paths: /v1/agents: post: summary: Provision an agent (burner or persistent) description: | Mode is auto-detected: if `persona` or `warmup_recipe` is set AND no `ttl`/`ttl_seconds`, persistent. Otherwise burner. Set `mode` explicitly to override. operationId: createAgent requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/CreateAgentRequest" responses: "201": description: Agent provisioned content: application/json: schema: { $ref: "#/components/schemas/Agent" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": description: Insufficient credit. Top up at /v1/credits/checkout. content: application/json: schema: { $ref: "#/components/schemas/Error" } "429": { $ref: "#/components/responses/RateLimited" } "502": { $ref: "#/components/responses/UpstreamUnavailable" } get: summary: List the workspace's agents description: | Returns the workspace's agents, newest first. Used by the dashboard's Identities view and by SDK consumers. operationId: listAgents parameters: - name: mode in: query required: false schema: { type: string, enum: [burner, persistent] } - name: limit in: query required: false schema: { type: integer, default: 50, minimum: 1, maximum: 200 } responses: "200": description: Agents list content: application/json: schema: type: object properties: agents: type: array items: { $ref: "#/components/schemas/Agent" } "401": { $ref: "#/components/responses/Unauthorized" } /v1/agents/{id}: get: summary: Get an agent by id operationId: getAgent parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "200": description: Agent payload content: application/json: schema: { $ref: "#/components/schemas/Agent" } "401": { $ref: "#/components/responses/Unauthorized" } "404": description: No agent with that id (or belongs to a different workspace) content: application/json: schema: { $ref: "#/components/schemas/Error" } delete: summary: Destroy an agent immediately operationId: destroyAgent parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Destroyed } "401": { $ref: "#/components/responses/Unauthorized" } "404": description: Not found content: application/json: schema: { $ref: "#/components/schemas/Error" } /v1/agents/{id}/cdp: get: summary: Get the CDP URLs for an agent operationId: getAgentCDP parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "200": description: CDP URLs content: application/json: schema: type: object properties: cdp_http: { type: string, format: uri } cdp_url: { type: string, description: "wss://... WebSocket endpoint" } profile_id: { type: string } expires_at: { type: string, format: date-time } "409": description: Agent is not running content: application/json: schema: { $ref: "#/components/schemas/Error" } /v1/agents/{id}/sessions: post: summary: Open a new session on a persistent identity description: | Only valid on persistent agents. Charges PersistentSessionCredits. The identity's profile (cookies, localStorage) is reused; only the cloud session is new. operationId: openSession parameters: - { name: id, in: path, required: true, schema: { type: string } } requestBody: required: false content: application/json: schema: type: object properties: initial_url: { type: string, format: uri } country: { type: string } responses: "201": description: Session opened content: application/json: schema: { $ref: "#/components/schemas/Agent" } "400": description: Tried to open a session on a non-persistent agent "402": { description: Insufficient credit } "404": { description: Agent not found } "502": { $ref: "#/components/responses/UpstreamUnavailable" } /v1/agents/{id}/fingerprint: get: summary: Preview the agent's wire-level fingerprint description: | Returns the concrete navigator / screen / WebGL / timezone / canvas / audio / TLS fingerprint this identity presents to detection sites — derived deterministically from the agent's pinned options (os_type, country, browser, persona, overrides). Lets customers validate "anti-detect" against their own test suite BEFORE running a task. Override knobs that disagree with the OS shape surface in `warnings`. operationId: getAgentFingerprint parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "200": description: Fingerprint preview content: application/json: schema: { $ref: "#/components/schemas/Fingerprint" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { description: No agent with that id (or different workspace) } /v1/agents/{id}/trust: get: summary: Per-factor trust-score breakdown description: | Returns the line-item contribution of every input that produced the agent's `trust_score` (baseline, mode, os_type, warmup, country, age, sessions, harness signal) plus the score band (undetected / suspicious / flagged) and remediation hints. operationId: getAgentTrust parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "200": description: Trust breakdown content: application/json: schema: { $ref: "#/components/schemas/TrustBreakdown" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { description: No agent with that id (or different workspace) } /v1/agents/{id}/detection-signal: get: summary: Latest detection-harness signal for this fingerprint shape description: | Returns the most-recent detection-harness pass-rate for THIS identity's fingerprint shape (os_type + browser + core major), 14-day lookback. Empirical proof behind the trust score. Returns 200 with `signal: null` when no harness run exists for the shape; 503 when the signals repository isn't wired on this deployment. operationId: getAgentDetectionSignal parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "200": description: Detection signal (or null when none on file) content: application/json: schema: { $ref: "#/components/schemas/DetectionSignalResponse" } "401": { $ref: "#/components/responses/Unauthorized" } "404": { description: No agent with that id (or different workspace) } "503": { description: Detection-signal repository not wired on this deployment } /v1/quick/scrape: post: summary: Scrape a single URL (sync or async) description: | Fetches one URL on a fresh anti-detect burner and returns the page in the requested `formats` (html, text, markdown, links). Synchronous by default — blocks and returns the data. Pass `async: true` (or `?async=true`) to get a 202 + job envelope you poll at GET /v1/jobs/{id}. Send `Idempotency-Key` to dedupe retries (Stripe-compatible; 24h window). operationId: quickScrape parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ScrapeRequest" } responses: "200": description: Synchronous scrape result content: application/json: schema: { $ref: "#/components/schemas/ScrapeResult" } "202": description: "Async job accepted (async=true)" content: application/json: schema: { $ref: "#/components/schemas/JobAccepted" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": description: Insufficient credit content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } /v1/quick/extract: post: summary: Extract structured data from a URL (sync or async) description: | Fetches a URL and returns structured data shaped by either a JSON-schema `schema` or a freeform `prompt` (at least one is required; `url` is mandatory). Synchronous by default; `async: true` returns a 202 job envelope. operationId: quickExtract requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ExtractRequest" } responses: "200": description: Synchronous extract result content: application/json: schema: { type: object, additionalProperties: true } "202": description: "Async job accepted (async=true)" content: application/json: schema: { $ref: "#/components/schemas/JobAccepted" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": description: Insufficient credit content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } /v1/quick/screenshot: post: summary: Screenshot a URL (sync or async) description: | Captures a PNG of the page. Synchronous by default; `async: true` returns a 202 job envelope. Use `full_page: true` for the full scrollable height. operationId: quickScreenshot requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ScreenshotRequest" } responses: "200": description: Synchronous screenshot result content: application/json: schema: { type: object, additionalProperties: true } "202": description: "Async job accepted (async=true)" content: application/json: schema: { $ref: "#/components/schemas/JobAccepted" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": description: Insufficient credit content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } /v1/quick/batch: post: summary: Scrape many URLs in one job (always async) description: | Scrapes a list of URLs concurrently. Always async — returns a 202 job envelope immediately because the runtime exceeds any reasonable HTTP timeout. Poll GET /v1/jobs/{id}; the result carries one row per URL. operationId: quickBatch requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/BatchRequest" } responses: "202": description: Batch job accepted content: application/json: schema: { $ref: "#/components/schemas/JobAccepted" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": description: Insufficient credit content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } /v1/quick/crawl: post: summary: Crawl a site from a start URL (always async) description: | Breadth-first crawl from `start_url`, honoring max_depth / max_pages / same_domain and optional include/exclude regexes. Always async — returns a 202 job envelope. Poll GET /v1/jobs/{id}. operationId: quickCrawl requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CrawlRequest" } responses: "202": description: Crawl job accepted content: application/json: schema: { $ref: "#/components/schemas/JobAccepted" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": description: Insufficient credit content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } /v1/auto: post: summary: Smart-routed task (fast scrape path or full agent) description: | Same body as /v1/tasks. The platform inspects the task wording: single-URL, fetch-only prompts short-circuit to a synchronous scrape (~6× faster, no LLM/burner) and return 200 with `routed_via: "scrape"`. Interactive tasks (clicks, form submits, logins) fall through to the full LLM agent and return 202 with `routed_via: "task"` plus the job envelope. `llm_api_key` is only required when the task routes to the interactive path. Send `Idempotency-Key` to dedupe retries. operationId: autoRoute parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/TaskRequest" } responses: "200": description: "Fetch-only fast path (routed_via=scrape)" content: application/json: schema: { $ref: "#/components/schemas/AutoScrapeResult" } "202": description: "Interactive task queued (routed_via=task)" content: application/json: schema: { $ref: "#/components/schemas/AutoTaskAccepted" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": description: Insufficient credit content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } /v1/jobs: get: summary: List recent jobs for the workspace description: | Returns the workspace's most-recent jobs (newest first). Result blobs are intentionally omitted — GET /v1/jobs/{id} for the full payload. Cursor pagination: pass the previous page's `next_cursor` (RFC3339 timestamp) as `cursor`. operationId: listJobs parameters: - name: limit in: query required: false schema: { type: integer, default: 50, minimum: 1, maximum: 200 } - name: cursor in: query required: false description: RFC3339 timestamp; pass the previous page's next_cursor. schema: { type: string, format: date-time } responses: "200": description: Jobs page content: application/json: schema: type: object properties: jobs: type: array items: { $ref: "#/components/schemas/JobListItem" } count: { type: integer } has_more: { type: boolean } next_cursor: { type: string, format: date-time } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } /v1/webhooks/signing-secret: get: summary: Get the workspace's webhook signing secret description: | Returns the hex HMAC-SHA256 secret used to sign every outbound delivery for this workspace, plus the signature header name and format. The secret is derived deterministically and never rotates, so receivers can hard-code it. 503 when webhook delivery isn't configured on this deployment. operationId: getWebhookSigningSecret responses: "200": description: Signing secret + signature scheme content: application/json: schema: type: object properties: secret: { type: string, description: "hex-encoded HMAC-SHA256 key" } algorithm: { type: string, example: "HMAC-SHA256" } header: { type: string, example: "X-Agents-Signature" } format: { type: string, example: "t=, v1=" } body: { type: string, example: "." } "401": { $ref: "#/components/responses/Unauthorized" } "503": { description: Webhook delivery not configured on this deployment } /v1/webhooks/{id}/test: post: summary: Fire a synthetic test delivery description: | Delivers a `webhook.test` event to the registered URL so you can verify your receiver (and signature checking) before depending on production traffic. The attempt is logged and appears in /v1/webhooks/{id}/deliveries. `/v1/webhooks/{id}/ping` is an alias for this endpoint. operationId: testWebhook parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "200": description: Delivered successfully content: application/json: schema: type: object properties: ok: { type: boolean } delivered_at: { type: string, format: date-time } event_id: { type: string } url: { type: string, format: uri } "401": { $ref: "#/components/responses/Unauthorized" } "404": { description: No webhook with that id } "409": { description: Webhook subscription is disabled } "502": { description: Receiver returned an error / unreachable } "503": { description: Webhook delivery not configured on this deployment } /v1/webhooks/{id}/ping: post: summary: Alias for the test-delivery endpoint description: | Identical to POST /v1/webhooks/{id}/test — fires a `webhook.test` event to the registered URL. Provided because `/ping` is the name customers reach for first. operationId: pingWebhook parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "200": description: Delivered successfully content: application/json: schema: type: object properties: ok: { type: boolean } delivered_at: { type: string, format: date-time } event_id: { type: string } url: { type: string, format: uri } "401": { $ref: "#/components/responses/Unauthorized" } "404": { description: No webhook with that id } "409": { description: Webhook subscription is disabled } "502": { description: Receiver returned an error / unreachable } "503": { description: Webhook delivery not configured on this deployment } /v1/webhooks/{id}/deliveries: get: summary: List recent delivery attempts for a webhook description: | Returns the most-recent delivery attempts for the webhook (newest first), so you can debug missed events. Empty list (200) for a freshly registered webhook with no deliveries yet; 404 when the webhook id doesn't exist in this workspace. operationId: listWebhookDeliveries parameters: - { name: id, in: path, required: true, schema: { type: string } } - name: limit in: query required: false schema: { type: integer, default: 50, minimum: 1, maximum: 200 } responses: "200": description: Delivery attempts content: application/json: schema: type: object properties: deliveries: type: array items: { $ref: "#/components/schemas/WebhookDelivery" } count: { type: integer } "401": { $ref: "#/components/responses/Unauthorized" } "404": { description: No webhook with that id } /v1/team/members: get: summary: List workspace members + pending invites description: | Returns the workspace owner (the caller) plus any team members and pending invitations. Roles are 'owner' or 'member'. operationId: listTeamMembers responses: "200": description: Members and pending invites content: application/json: schema: type: object properties: members: type: array items: { $ref: "#/components/schemas/TeamMember" } pending_invites: type: array items: { $ref: "#/components/schemas/TeamInvite" } "401": { $ref: "#/components/responses/Unauthorized" } /v1/team/invite: post: summary: Invite an email to the workspace description: | Records an invitation (upsert on re-invite). The invitee sees the pending invite when they sign up with this email. Role defaults to 'member'. operationId: inviteTeamMember requestBody: required: true content: application/json: schema: type: object required: [email] properties: email: { type: string, format: email } role: { type: string, default: member } responses: "201": description: Invitation recorded content: application/json: schema: type: object properties: id: { type: string } email: { type: string, format: email } role: { type: string } workspace: { type: string } invited_at: { type: string, format: date-time } status: { type: string, example: pending } next_action: { type: string } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } /v1/team/invitations/{id}: delete: summary: Cancel a pending invitation operationId: cancelTeamInvite parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Cancelled } "401": { $ref: "#/components/responses/Unauthorized" } "404": { description: No such pending invitation } /v1/team/members/{userId}: delete: summary: Remove a member from the workspace description: | Removes a team member. You cannot remove yourself (the owner) — use account deletion instead. operationId: removeTeamMember parameters: - { name: userId, in: path, required: true, schema: { type: string } } responses: "204": { description: Removed } "400": { description: Cannot remove yourself } "401": { $ref: "#/components/responses/Unauthorized" } "404": { description: User is not a member of this workspace } /v1/credits: get: summary: Get current credit balance + pack menu operationId: getCredits responses: "200": description: Balance content: application/json: schema: type: object properties: workspace_id: { type: string } balance: { type: integer } packs: type: array items: type: object properties: sku: { type: string } label: { type: string } amount_usd_cents: { type: integer } credits: { type: integer } /v1/credits/checkout: post: summary: Create a Stripe Checkout session for a credit pack operationId: createCheckout requestBody: required: true content: application/json: schema: type: object required: [sku] properties: sku: type: string enum: [starter_20, growth_100, pro_500] customer_email: { type: string, format: email } responses: "201": description: Checkout session created — redirect the customer to checkout_url content: application/json: schema: type: object properties: checkout_url: { type: string, format: uri } stripe_session_id: { type: string } pack_id: { type: string } sku: { type: string } "400": { description: Unknown SKU / missing fields } "503": { description: Stripe Checkout not enabled on this deployment } /v1/credits/webhook: post: summary: Stripe webhook receiver (signed by Stripe, NOT API-key auth'd) description: | Customer-facing endpoint pointed at by Stripe. Verifies the `Stripe-Signature` header before granting credits. Operators configure the webhook in the Stripe dashboard pointing here. operationId: stripeWebhook security: [] responses: "200": { description: Event received and processed } "401": { description: Signature did not verify } /v1/keys: get: summary: List API keys for the workspace operationId: listKeys responses: "200": description: keys content: application/json: schema: type: object properties: keys: type: array items: { $ref: "#/components/schemas/APIKey" } post: summary: Mint a new API key description: | The `secret` field is shown EXACTLY ONCE in the response. Save it client-side or it's lost. operationId: mintKey requestBody: required: false content: application/json: schema: type: object properties: name: { type: string } responses: "201": description: Key minted — secret shown once content: application/json: schema: { $ref: "#/components/schemas/MintedKey" } /v1/keys/{id}: delete: summary: Revoke an API key operationId: revokeKey parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Revoked } "404": { description: Key not found } /v1/webhooks: get: summary: List webhook subscriptions operationId: listWebhooks responses: "200": description: subscriptions content: application/json: schema: type: object properties: webhooks: type: array items: { $ref: "#/components/schemas/WebhookSub" } post: summary: Register a webhook callback URL operationId: createWebhook requestBody: required: true content: application/json: schema: type: object required: [url] properties: url: { type: string, format: uri } events: type: array description: Empty array = all events items: { $ref: "#/components/schemas/EventType" } responses: "201": description: Webhook subscription created content: application/json: schema: { $ref: "#/components/schemas/WebhookSub" } /v1/webhooks/{id}: delete: summary: Disable a webhook subscription operationId: disableWebhook parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Disabled } "404": { description: Not found } /v1/jobs/{id}: get: summary: Fetch an async job description: | Returns the full job envelope. Two optional query params: - `wait=N` (1-120) long-polls the API up to N seconds for a terminal state. Lets linear client code feel synchronous without hammering the endpoint. - `format=csv|ndjson` returns a downloadable export of the job's result instead of JSON. Heavy fields (html, screenshot_png_b64) collapse to `_bytes` counts so the export stays consumable in Excel / pandas / DuckDB. Batch + crawl jobs render one row per URL. Content-Disposition: attachment; filename="-." is set so browsers / curl -O drop the file directly to disk. operationId: getJob parameters: - { name: id, in: path, required: true, schema: { type: string } } - name: wait in: query required: false schema: { type: integer, minimum: 0, maximum: 120 } - name: format in: query required: false schema: { type: string, enum: [csv, ndjson] } responses: "200": description: Job payload (JSON) or downloadable export (CSV/NDJSON) content: application/json: schema: { $ref: "#/components/schemas/Job" } text/csv: { schema: { type: string, format: binary } } application/x-ndjson: { schema: { type: string, format: binary } } "404": { description: No job with that id (or different workspace) } "409": description: | Returned when `format=csv|ndjson` is requested for a job that isn't completed yet — there's nothing to export. Poll the status endpoint or use `?wait=N` until status='completed'. content: application/json: schema: { $ref: "#/components/schemas/Error" } /v1/jobs/{id}/status: get: summary: Lightweight status-only poll description: | Returns {id, status, kind, progress_done, progress_total} only. Cheap shape for polling loops that don't want to pull the full result blob every tick. operationId: getJobStatus parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "200": description: Status payload content: application/json: schema: type: object properties: id: { type: string } status: type: string enum: [queued, running, completed, failed, cancelled, input_required] kind: { type: string, enum: [scrape, extract, screenshot, batch, crawl, task] } progress_done: { type: integer } progress_total: { type: integer } "404": { description: No job with that id } /v1/jobs/{id}/cancel: post: summary: Cancel a running job description: | Cooperative cancel — the agent finishes its current step then stops. You're billed only for steps actually taken. Idempotent; returns 409 if the job is already terminal. operationId: cancelJob parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Cancelled (or already cancelling) } "400": { $ref: "#/components/responses/BadRequest" } "404": { description: No job with that id } "409": { description: Job already terminal } /v1/jobs/{id}/respond: post: summary: Answer an input_required prompt description: | When the agent calls the `ask_user` tool (CAPTCHA solution, decision point, etc.) the job pauses in `input_required` state. POST your answer here and the job flips back to `running`. operationId: respondToJob parameters: - { name: id, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object required: [response] properties: response: { type: string } responses: "200": description: Updated job envelope content: application/json: schema: { $ref: "#/components/schemas/Job" } "400": { $ref: "#/components/responses/BadRequest" } "404": { description: No job with that id } "409": { description: Job not in input_required state } /v1/tasks: post: summary: Submit an AI-driven browser task description: | Submit a plain-English instruction; the platform spawns a fresh anti-detect Chromium burner, drives it with the chosen LLM via browser-use, and returns when the agent finishes. Use GET /v1/jobs/{id}?wait=30 to long-poll or watch the live_url noVNC stream while it runs. operationId: submitTask requestBody: required: true content: application/json: schema: type: object required: [task, llm, llm_api_key] properties: task: { type: string, description: "Plain-English instructions for the agent" } framework: { type: string, default: "browser-use" } llm: { type: string, example: "claude-opus-4-8" } llm_api_key: { type: string, description: "Customer's LLM provider key (sk-ant-… / sk-… / jot_…)" } llm_base_url: { type: string, description: "Override provider host for OpenAI-compatible proxies" } country: { type: string, example: "US" } os_type: { type: string, enum: [OS_TYPE_MACOS, OS_TYPE_WINDOWS, OS_TYPE_LINUX] } max_steps: { type: integer, minimum: 1, maximum: 100 } ja3_profile: { type: string, example: "macos_chrome_137" } initial_url: { type: string } agent_id: { type: string, description: "Persistent identity UUID; omit for a fresh burner" } responses: "202": description: Task accepted content: application/json: schema: { $ref: "#/components/schemas/Job" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": description: Insufficient credit content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } /v1/credits/ledger: get: summary: Per-job credit ledger description: | Returns the workspace's credit ledger entries — every signup bonus, paid top-up, agent-provision debit, and per-job debit. Each row carries the linked `agent_id` (job id for job-debit rows) so an agency can bill their own client from this data. operationId: listLedger parameters: - name: limit in: query required: false schema: { type: integer, default: 50, minimum: 1, maximum: 500 } responses: "200": description: Ledger snapshot content: application/json: schema: type: object properties: balance: { type: integer } count: { type: integer } entries: type: array items: type: object properties: id: { type: string } delta: { type: integer, description: "Signed; debits are negative" } balance: { type: integer, description: "Running balance after this entry" } reason: { type: string, example: "job:252e8eac-…" } agent_id: { type: string, description: "Linked job or agent UUID" } created_at: { type: string, format: date-time } /v1/health: get: summary: Public health + build SHA description: | Public endpoint; no auth required. Returns build SHA, version, service name, current time, and docs URL. Use for status pages and deployment verification. operationId: health security: [] responses: "200": description: Service is alive content: application/json: schema: type: object properties: service: { type: string, example: "browsa" } status: { type: string, example: "ok" } build: { type: string, example: "a3f4d2b" } version: { type: string, example: "v0.1.0" } now: { type: string, format: date-time } docs: { type: string, format: uri } components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key BearerAuth: type: http scheme: bearer bearerFormat: agt_live_<24-hex> parameters: IdempotencyKey: name: Idempotency-Key in: header required: false description: | Stripe-compatible idempotency key. The same (workspace, key) within 24h replays the original response byte-for-byte and sets `Idempotent-Replay: true` instead of re-executing (and re-charging). Empty = normal execution. schema: { type: string } responses: BadRequest: description: Validation failed content: application/json: schema: { $ref: "#/components/schemas/Error" } Unauthorized: description: Missing or invalid API key content: application/json: schema: { $ref: "#/components/schemas/Error" } RateLimited: description: Per-key rate limit exceeded headers: Retry-After: { schema: { type: integer } } content: application/json: schema: { $ref: "#/components/schemas/Error" } UpstreamUnavailable: description: neout gateway or cloud-runner unreachable content: application/json: schema: { $ref: "#/components/schemas/Error" } schemas: CreateAgentRequest: type: object properties: ttl: type: string description: "Duration string: '5m', '300s', '1h'. Capped at 1h for burner." ttl_seconds: type: integer description: Numeric alternative to ttl mode: type: string enum: [burner, persistent] os_type: type: string example: OS_TYPE_MACOS browser_type: type: string example: BROWSER_TYPE_PHANTOM initial_url: type: string format: uri country: type: string example: us description: >- Fingerprint-locale hint (timezone / language) only. Browsa does not provision proxies, so this does not route egress and carries no surcharge. To control egress, supply your own `proxy`. persona: type: string description: Persistent mode — freeform label like "us-shopper-30s" warmup_recipe: type: string description: "Optional warmup: 'amazon', 'google', 'social', ''" Agent: type: object properties: id: { type: string, example: "agt_aabbccddeeff00112233" } status: type: string enum: [queued, running, destroyed, failed] mode: { type: string, enum: [burner, persistent] } cdp_url: { type: string } cdp_http: { type: string } expires_at: { type: string, format: date-time } ttl_seconds: { type: integer } trust_score: { type: integer, minimum: 0, maximum: 1000 } cost_credits: { type: integer } created_at: { type: string, format: date-time } error_code: { type: string } error_message: { type: string } APIKey: type: object properties: id: { type: string } name: { type: string } prefix: { type: string } created_at: { type: string, format: date-time } last_used_at: { type: ["string", "null"], format: date-time } revoked_at: { type: ["string", "null"], format: date-time } MintedKey: allOf: - $ref: "#/components/schemas/APIKey" - type: object properties: secret: type: string description: Shown ONCE on creation; the server stores only sha256(secret). Job: type: object description: | Async job row. Created by POST /v1/quick/* (with async=true), POST /v1/quick/batch, POST /v1/quick/crawl, or POST /v1/tasks. Reaches a terminal `completed` or `failed` state via the worker, then optionally fires job.completed / job.failed webhooks to every active subscription. properties: id: { type: string, format: uuid } kind: type: string enum: [scrape, extract, screenshot, batch, crawl, task] status: type: string enum: [queued, running, completed, failed] progress_done: { type: integer } progress_total: { type: integer } created_at: { type: string, format: date-time } started_at: { type: ["string", "null"], format: date-time } completed_at: { type: ["string", "null"], format: date-time } credits_charged: { type: ["integer", "null"] } result: type: object description: | Populated only on status='completed'. Shape varies by kind — see service.ScrapeResult / BatchResult / CrawlResult / TaskResult. additionalProperties: true error_code: allOf: - { $ref: "#/components/schemas/JobErrorCode" } description: "Present on status='failed'. See JobErrorCode for full catalogue + remediation." error_message: { type: string, description: "Present on status='failed' — human-readable detail" } framework: { type: string, description: "Task-only: browser-use, claude-cu, openai-cua" } live_url: { type: string, description: "Task-only: noVNC embed for the task burner" } agent_id: { type: string, description: "Task-only: the burner the task ran on" } step_count: { type: integer, description: "Task-only: steps the LLM took" } WebhookSub: type: object properties: id: { type: string } url: { type: string, format: uri } events: type: array items: { $ref: "#/components/schemas/EventType" } created_at: { type: string, format: date-time } disabled_at: { type: ["string", "null"], format: date-time } last_sent_at: { type: ["string", "null"], format: date-time } last_error: { type: string } EventType: type: string enum: - agent.provisioned - agent.destroyed - agent.failed - agent.session.started - agent.session.ended - agent.renewed - agent.renewal_failed - job.completed - job.failed JobEvent: type: object description: | Payload delivered when a /v1/quick/* or /v1/tasks job reaches a terminal state. Signed exactly like every other webhook (see Webhook delivery section in API.md). At-least-once delivery — dedupe on `id` (the event_id). required: [id, type, created_at, workspace_id, job_id, job_kind] properties: id: { type: string, description: "event_id; stable across retries" } type: type: string enum: [job.completed, job.failed] created_at: { type: string, format: date-time } workspace_id: { type: string } job_id: { type: string, description: "GET /v1/jobs/:id to retrieve" } job_kind: type: string enum: [scrape, extract, screenshot, batch, crawl, task] data: type: object description: | Snapshot of the terminal job. Populated fields: - progress_done, progress_total - credits_charged - result (the full job result object — see GET /v1/jobs/:id) On job.failed: data is small and error_code/error_reason carry the failure envelope. additionalProperties: true error_code: { type: string, description: "Present on job.failed" } error_reason: { type: string, description: "Present on job.failed" } Error: type: object required: [error, code] properties: error: { type: string } code: type: string description: | Top-level API error codes returned in {error,code,request_id} envelopes. Job-level failure codes (set on completed jobs with status=failed) live in JobErrorCode below — they describe why a task didn't reach a final result, not why the HTTP call failed. enum: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - IDEMPOTENCY_CONFLICT - INSUFFICIENT_CREDIT - TOO_MANY_REQUESTS - UPSTREAM_UNAVAILABLE - UPSTREAM_TIMEOUT - AGENT_NOT_RUNNING - BILLING_DISABLED - INVALID_SIGNATURE - INVALID_API_KEY - MISSING_API_KEY - INTERNAL_ERROR # IDEMPOTENCY_CONFLICT (HTTP 409): a create request with this # Idempotency-Key is already in progress. Retry in a moment to # replay its result (responses are cached 24h once complete). request_id: { type: string } # JobErrorCode is the catalogue customers see on /v1/jobs/:id when # status=failed. Documented at request of DevOps-persona audit # 2026-06-09 P1 — "STALLED error code appears undocumented" was # blocking on-call triage because there was no public reference # for what each terminal failure code means or how to fix it. JobErrorCode: type: string enum: - STALLED - LLM_ERROR - MODEL_NO_TOOL_USE - MODEL_RATE_LIMITED - PROXY_AUTH_FAILED - PROXY_FAILED - TIMEOUT - CANCELLED - INSUFFICIENT_CREDIT - INVALID_API_KEY - RATE_LIMIT - INTERNAL_ERROR description: | - `STALLED` — the LLM agent ran `step_count` steps but never called `done()` to emit a final result. Causes ranked by frequency: (a) max_steps was too low for the task; (b) the LLM got lost in a wait/scroll loop; (c) the target site presented an anti-bot challenge / CAPTCHA the agent couldn't pass. The error_message will distinguish (a) — "ran out of steps, try raising max_steps" — from (b)/(c) — "agent decided to give up before calling done()". Fix: 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 means the BYOK key was rejected mid-run, the model name was wrong, or the provider rate-limited. error_message includes the upstream provider's verbatim error. - `MODEL_NO_TOOL_USE` — a fast pre-flight probe found the chosen model/endpoint returns plain text and never a tool call, so the browser agent (which needs a tool call every step) cannot run. Caught in ~2s before any burner is provisioned. Fix: use a model that supports tool/function calling (e.g. `claude-haiku-4-5`). - `MODEL_RATE_LIMITED` — the model is rate-limited at your LLM provider (HTTP 429 on 3 consecutive pre-flight probes); an agent makes many calls/min and would stall. Caught in ~2-6s. Fix: retry later, raise the provider quota, or use a different model. - `PROXY_AUTH_FAILED` — your bring-your-own proxy refused authentication (HTTP 407 → `net::ERR_PROXY_AUTH_UNSUPPORTED`). Double-check the username/password — for IPRoyal/Bright Data/ Smartproxy-style proxies, modifiers like `_country-us` and `_session-xxxx` go in the PASSWORD, not the username. Refunded. - `PROXY_FAILED` — your proxy host/port was unreachable or refused the tunnel. Verify host, port, scheme, and that it's online. Refunded. - `TIMEOUT` — the wallclock budget (default 900s) was exceeded before the agent terminated. Customer can pass a higher `wallclock_seconds` on the create request, but typically this signals a pathologically slow page or LLM. - `CANCELLED` — customer fired DELETE /v1/jobs/{id}/cancel mid-run. No charge for the partial work. - `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. Re-check the key, base URL, and model name. - `RATE_LIMIT` — the BYOK LLM provider rate-limited and exhausted our retry budget. Either upgrade the LLM-side plan or lower task concurrency. - `INTERNAL_ERROR` — unexpected platform failure. Includes a request_id; please send it to support@neout.com. # --- /v1/quick/* request + result shapes ------------------------------- ScrapeRequest: type: object required: [url] properties: url: { type: string, format: uri } formats: type: array description: "Subset of: html, text, markdown, links" items: { type: string, enum: [html, text, markdown, links] } wait_ms: { type: integer, description: "Extra settle time after load" } max_chars: { type: integer, description: "Truncate text/markdown output" } country: { type: string, example: "US" } block: type: array description: "Resource types to block (e.g. image, font, media)" items: { type: string } async: { type: boolean, description: "Return a 202 job envelope instead of blocking" } ScrapeResult: type: object description: | Synchronous scrape payload. Populated fields vary with the requested `formats`. Mirrors service.ScrapeResult. properties: url: { type: string, format: uri, description: "Final landed URL after redirects" } http_status: { type: integer } html: { type: string } text: { type: string } markdown: { type: string } links: type: array items: { type: string } agent_id: { type: string } trust_score: { type: integer } cost_credits: { type: integer } duration_ms: { type: integer } additionalProperties: true ExtractRequest: type: object required: [url] description: "Either `schema` or `prompt` is required." properties: url: { type: string, format: uri } schema: type: object description: "JSON-schema object describing the desired output shape" additionalProperties: true prompt: { type: string, description: "Freeform NL instruction (alternative to schema)" } wait_ms: { type: integer } country: { type: string } block: type: array items: { type: string } async: { type: boolean } ScreenshotRequest: type: object required: [url] properties: url: { type: string, format: uri } wait_ms: { type: integer } full_page: { type: boolean, description: "Capture the full scrollable height" } country: { type: string } async: { type: boolean } BatchRequest: type: object required: [urls] properties: urls: type: array items: { type: string, format: uri } formats: type: array items: { type: string, enum: [html, text, markdown, links] } wait_ms: { type: integer } max_chars: { type: integer } country: { type: string } block: type: array items: { type: string } concurrency: { type: integer, description: "Max in-flight fetches" } CrawlRequest: type: object required: [start_url] properties: start_url: { type: string, format: uri } url: { type: string, format: uri, description: "Alias for start_url (accepted for consistency with the other quick endpoints). start_url wins if both are sent." } max_depth: { type: integer } max_pages: { type: integer } same_domain: { type: boolean, default: true } formats: type: array items: { type: string, enum: [html, text, markdown, links] } wait_ms: { type: integer } max_chars: { type: integer } country: { type: string } block: type: array items: { type: string } include_regex: { type: string, description: "Only crawl URLs matching this regex" } exclude_regex: { type: string, description: "Skip URLs matching this regex" } TaskRequest: type: object required: [task] description: | Body shared by POST /v1/tasks and POST /v1/auto. `llm_api_key` is required for /v1/tasks and for the interactive path of /v1/auto. properties: task: { type: string, description: "Plain-English instructions for the agent" } framework: { type: string, default: "browser-use" } llm: { type: string, example: "claude-opus-4-8" } llm_api_key: { type: string, description: "Customer's LLM provider key (sk-ant-… / sk-… / jot_…)" } llm_base_url: { type: string, description: "Override provider host for OpenAI/Anthropic-compatible proxies" } max_steps: { type: integer, minimum: 1, maximum: 100 } max_cost_usd: { type: number, description: "Cap total cost; 0 = no cap" } country: { type: string, example: "US", description: "Fingerprint-locale hint (timezone/language) only — does not route egress or add a surcharge. Use `proxy` to control egress." } os_type: { type: string, enum: [OS_TYPE_MACOS, OS_TYPE_WINDOWS, OS_TYPE_LINUX] } initial_url: { type: string, format: uri } proxy: { $ref: "#/components/schemas/ProxyConfig", description: "Bring your own egress proxy. The only way to control egress IP; with none set the run egresses direct from the datacenter." } agent_id: { type: string, description: "Reuse a persistent identity; omit for a fresh burner" } ja3_profile: { type: string, example: "macos_chrome_137" } screen_resolution: { type: string, example: "1920x1080" } webrtc_mode: { type: string } geolocation_mode: { type: string } geolocation_data: { type: string } media_devices_mode: { type: string } port_scan_protection: { type: string } ProxyConfig: type: object description: "Bring-your-own residential / mobile proxy." properties: server: { type: string, description: "host:port" } username: { type: string } password: { type: string } additionalProperties: true JobAccepted: type: object description: "Immediate 202 envelope after queuing an async job." properties: job_id: { type: string, format: uuid } status: { type: string, enum: [queued, running, completed, failed, cancelled, input_required] } kind: { type: string, enum: [scrape, extract, screenshot, batch, crawl, task] } progress_done: { type: integer } progress_total: { type: integer } created_at: { type: string, format: date-time } status_url: { type: string, format: uri } result_url: { type: string, format: uri } JobListItem: type: object description: "Trimmed job row returned by GET /v1/jobs (no result blob)." properties: id: { type: string, format: uuid } kind: { type: string, enum: [scrape, extract, screenshot, batch, crawl, task] } status: { type: string, enum: [queued, running, completed, failed, cancelled, input_required] } progress_done: { type: integer } progress_total: { type: integer } created_at: { type: string, format: date-time } started_at: { type: ["string", "null"], format: date-time } completed_at: { type: ["string", "null"], format: date-time } credits_charged: { type: ["integer", "null"] } error_code: { type: string, description: "Present on status='failed'" } live_url: { type: string, description: "Task-only: noVNC embed" } AutoScrapeResult: type: object description: "POST /v1/auto fast path — fetch-only prompts (routed_via=scrape)." properties: routed_via: { type: string, enum: [scrape] } routing_reason: { type: string } url: { type: string, format: uri } http_status: { type: integer } html: { type: string } text: { type: string } agent_id: { type: string } trust_score: { type: integer } cost_credits: { type: integer } duration_ms: { type: integer } AutoTaskAccepted: allOf: - type: object properties: routed_via: { type: string, enum: [task] } routing_reason: { type: string } - $ref: "#/components/schemas/JobAccepted" # --- webhook deliveries ------------------------------------------------ WebhookDelivery: type: object properties: event_id: { type: string } event_type: { type: string } status_code: { type: ["integer", "null"], description: "null = transport-layer failure" } delivered_at: { type: string, format: date-time } duration_ms: { type: integer } error: { type: string } attempt: { type: integer } # --- team -------------------------------------------------------------- TeamMember: type: object properties: user_id: { type: string } email: { type: string, format: email } role: { type: string, enum: [owner, member] } status: { type: string, enum: [active, invited] } TeamInvite: type: object properties: id: { type: string } email: { type: string, format: email } role: { type: string } invited_at: { type: string, format: date-time } # --- identity-quality shapes ------------------------------------------- Fingerprint: type: object description: | Wire-level fingerprint preview derived from the agent's pinned options. Mirrors the values the Phantom build emits at runtime. properties: agent_id: { type: string } navigator: type: object properties: user_agent: { type: string } platform: { type: string } languages: { type: array, items: { type: string } } language: { type: string } hardware_concurrency: { type: integer } device_memory: { type: integer } vendor: { type: string } webdriver: { type: boolean } screen: type: object properties: width: { type: integer } height: { type: integer } color_depth: { type: integer } pixel_depth: { type: integer } avail_width: { type: integer } avail_height: { type: integer } webgl: type: object properties: vendor: { type: string } renderer: { type: string } unmasked_vendor: { type: string } unmasked_renderer: { type: string } webgl2_supported: { type: boolean } timezone: type: object properties: name: { type: string } offset: { type: string } canvas: type: object additionalProperties: true audio: type: object additionalProperties: true tls: type: object additionalProperties: true overrides: type: object additionalProperties: true warnings: type: array items: { type: string } raw_options: type: object additionalProperties: true TrustBreakdown: type: object properties: agent_id: { type: string } trust_score: { type: integer, minimum: 0, maximum: 1000 } band: { type: string, enum: [undetected, suspicious, flagged] } max_score: { type: integer, example: 1000 } factors: type: array items: type: object properties: label: { type: string } delta: { type: integer, description: "Signed contribution to the score" } explain: { type: string } DetectionSignalResponse: type: object description: | Latest detection-harness signal for the agent's fingerprint shape. When no run is on file, `signal` is null and the static formula is used for the trust score. properties: agent_id: { type: string } signal: { type: ["object", "null"], description: "null when no harness run on file" } shape: type: object properties: os_type: { type: string } browser_type: { type: string } recorded_at: { type: string, format: date-time } provider: { type: string } sites_tested: { type: integer } sites_passed: { type: integer } pass_rate: { type: number } site_verdicts: type: object additionalProperties: true