openapi: 3.1.0
info:
  title: Better Fetch API
  version: "0.3.30"
  summary: Fetch URLs through a real, stealth Chromium browser.
  description: |
    Better Fetch is a private HTTP API for fetching URLs through a real
    Chromium browser. Use it when a plain HTTP client is not enough because
    the target needs JavaScript execution, browser-like navigation, regional
    routing, sticky sessions, screenshots, or rendered HTML.

    ## Versioning

    Endpoints are versioned under `/v1`. The unversioned paths (`/fetch`,
    `/health`) remain as **deprecated aliases** for backward compatibility
    and may be removed in a future major version — use `/v1`.

    ## Two layers of status

    The **HTTP status of the Better Fetch response** tells you whether the
    API call itself completed. The **`status` field inside the JSON body**
    is the target page's navigation status. Better Fetch can return HTTP
    `200` while the target `status` is `403`.

    ## Errors

    Every non-2xx response uses one JSON envelope:
    `{ "ok": false, "error": "<code>", "message": "<text>", "status": <http> }`.
    `error` is a stable machine code; `message` is human-readable.

    ## Sessions, pooling, and retries

    A request that carries a `session` reuses a warm, persistent-profile
    browser context keyed by a stable fingerprint derived from that session.
    If a fetch comes back `blocked` and a residential exit is in play
    (`country` or `session` set), the service automatically retries on a
    fresh session — new exit IP and fingerprint — and escalates those
    retries to a headed browser. `attempts` reports how many tries it took.
  contact:
    name: Better Fetch
  license:
    name: Proprietary

servers:
  - url: https://api.betterfetch.co
    description: Production

tags:
  - name: fetch
    description: Browser fetch operations
  - name: health
    description: Liveness

paths:
  /v1/health:
    get:
      tags: [health]
      operationId: getHealth
      summary: Liveness probe
      description: Health check. Does not require authentication. Also at `/health` (deprecated alias).
      security: []
      responses:
        "200":
          description: Service is up.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
              example:
                ok: true
                version: "0.3.30"
                proxy_configured: true
                pool_size: 2
                pool_max: 8

  /v1/fetch:
    post:
      tags: [fetch]
      operationId: fetchUrl
      summary: Fetch a URL through the browser
      description: |
        Launches (or reuses) a stealth Chromium context, navigates to `url`,
        waits according to `wait_until` / `wait_selector` / `wait_ms`, then
        returns browser-observed page data. Also at `/fetch` (deprecated alias).

        Unknown request fields are rejected (`400`). The request body is
        limited to 2 MiB.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FetchRequest"
            examples:
              minimal:
                summary: Minimal request
                value:
                  url: https://example.com
              recommended:
                summary: Recommended general request
                value:
                  url: https://example.com
                  wait_until: domcontentloaded
                  timeout_ms: 60000
              regional:
                summary: Sticky regional routing
                value:
                  url: https://example.com
                  country: gb
                  session: example-gb
                  wait_until: domcontentloaded
                  timeout_ms: 60000
              json_api:
                summary: JSON API endpoint
                value:
                  url: https://api.example.com/data
                  country: us
                  session: api-data-us
                  return_response_text: true
      responses:
        "200":
          description: |
            The browser request completed. Inspect `ok`, the target `status`,
            and `blocked` to interpret the result. A `200` here does not mean
            the target accepted the request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FetchSuccess"
              example:
                ok: true
                status: 200
                final_url: https://example.com/
                title: Example Domain
                html: "<!DOCTYPE html><html>...</html>"
                body_text: null
                json_parse_ok: false
                json: null
                headers:
                  content-type: text/html
                screenshot_b64: null
                blocked: false
                headed: false
                pooled: true
                attempts: 1
                timing_ms: 3200
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
        "402":
          $ref: "#/components/responses/Error"
        "405":
          $ref: "#/components/responses/Error"
        "413":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/Error"
        "500":
          $ref: "#/components/responses/Error"
        "502":
          $ref: "#/components/responses/Error"
        "504":
          $ref: "#/components/responses/Error"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer <your-api-key>`. Keep the key server-side.

  responses:
    Error:
      description: |
        Uniform error envelope. Returned for every non-2xx response:
        `400` (invalid JSON, body not an object, unknown field, missing/
        non-HTTP `url`, invalid `wait_until`, out-of-range `timeout_ms`,
        `extra_headers` not an object, invalid `country`/`session`, or
        `country`/`session` without proxy credentials), `401` (bad or
        revoked token), `402` (no active subscription), `404`, `405`,
        `413` (>2 MiB body), `429` (monthly quota exhausted; resets at
        the next billing cycle), `500` (server misconfigured),
        `502` (browser/proxy/target failure), `504` (API-layer timeout).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            unauthorized:
              value:
                ok: false
                error: unauthorized
                message: invalid or missing bearer token
                status: 401
            payment_required:
              value:
                ok: false
                error: payment_required
                message: no active subscription — subscribe at https://betterfetch.co
                status: 402
            quota_exceeded:
              value:
                ok: false
                error: quota_exceeded
                message: monthly quota exhausted; resets at your next billing cycle
                status: 429
            unknown_field:
              value:
                ok: false
                error: bad_request
                message: "unknown request field(s): wait_unil"
                status: 400
            fetch_failed:
              value:
                ok: false
                error: fetch_failed
                message: "TimeoutError: Page.goto: Timeout 30000ms exceeded."
                status: 502
            timeout:
              value:
                ok: false
                error: timeout
                message: request timed out at the API layer
                status: 504

  schemas:
    FetchRequest:
      type: object
      additionalProperties: false
      required: [url]
      properties:
        url:
          type: string
          format: uri
          pattern: "^https?://"
          description: HTTP or HTTPS URL to fetch.
          examples: ["https://example.com"]
        wait_until:
          type: string
          enum: [load, domcontentloaded, networkidle, commit]
          default: load
          description: Playwright navigation wait state.
        wait_selector:
          type: [string, "null"]
          default: null
          description: CSS selector to wait for after navigation.
        wait_ms:
          type: [integer, "null"]
          default: null
          minimum: 0
          description: Extra fixed wait after navigation, in ms. Capped by `timeout_ms`.
        timeout_ms:
          type: integer
          default: 30000
          minimum: 1
          maximum: 120000
          description: Navigation and selector timeout in milliseconds.
        return_response_text:
          type: boolean
          default: false
          description: >
            Include the raw navigation response body in `body_text`. JSON
            responses include it automatically regardless of this flag.
        return_cf_clearance:
          type: boolean
          default: false
          description: >
            Return Cloudflare `cf_clearance` token data when the browser
            receives that cookie. When false, cookies are not read or returned.
        capture_network:
          type: boolean
          default: false
          description: >
            Capture matching browser network calls and return them in
            `network`. Defaults to XHR/fetch resource types only.
        network_resource_types:
          type: array
          default: [xhr, fetch]
          items:
            type: string
            enum:
              - document
              - stylesheet
              - image
              - media
              - font
              - script
              - texttrack
              - xhr
              - fetch
              - eventsource
              - websocket
              - manifest
              - other
          description: >
            Playwright resource types to capture when `capture_network` is true.
        network_include_bodies:
          type: boolean
          default: true
          description: Include capped response bodies for captured network responses.
        network_include_headers:
          type: boolean
          default: false
          description: >
            Include `request_headers` and `response_headers` for captured
            network entries. Off by default because headers can contain secrets.
        network_max_entries:
          type: integer
          minimum: 1
          maximum: 500
          default: 100
          description: Maximum number of matching network entries to return.
        network_max_body_bytes:
          type: integer
          minimum: 0
          maximum: 1048576
          default: 262144
          description: Maximum bytes to keep from each captured response body.
        screenshot:
          type: boolean
          default: false
          description: Include a base64 PNG screenshot in `screenshot_b64`.
        full_page:
          type: boolean
          default: false
          description: Capture the full scrollable page (only when `screenshot` is true).
        country:
          type: [string, "null"]
          default: null
          pattern: "^[A-Za-z]{2}$"
          description: >
            Two-letter ISO country code for managed regional routing
            (e.g. `us`, `gb`, `de`, `au`, `ca`). Enables the residential proxy.
          examples: ["gb"]
        session:
          type: [string, "null"]
          default: null
          description: >
            Sticky routing + warm-pool key. Stripped to `[A-Za-z0-9]`;
            effective max 64 characters (the residential-proxy stickiness id
            uses the first 32). Reusing it keeps the same residential exit IP
            for ~10 minutes and reuses a warm browser context with a stable
            fingerprint. Enables the residential proxy and automatic block-retry.
          examples: ["example-gb"]
        geoip:
          type: boolean
          default: true
          description: >
            Align browser timezone, locale, and WebRTC IP with the proxy exit
            IP. Only effective when regional routing is in play
            (`country`/`session`).
        locale:
          type: [string, "null"]
          default: null
          maxLength: 64
          description: >
            Browser locale (e.g. `en-GB`), applied at the browser. Overrides
            the GeoIP-derived locale and works with or without regional routing.
        timezone:
          type: [string, "null"]
          default: null
          maxLength: 64
          description: >
            IANA timezone (e.g. `Europe/London`), applied at the browser.
            Overrides the GeoIP-derived timezone and works with or without
            regional routing.
        user_agent:
          type: [string, "null"]
          default: null
          maxLength: 512
          description: >
            Custom user agent applied to the browser context. Usually leave
            unset. Part of a session's warm-context identity (see `session`).
        extra_headers:
          type: object
          default: {}
          additionalProperties:
            type: string
          description: Additional HTTP headers applied inside the browser context.
        humanize:
          type: boolean
          description: >
            Human-like mouse, keyboard, and scroll behavior. When omitted it
            defaults to `true` for HTML page fetches and `false` when
            `return_response_text` is set or `extra_headers.Accept` is
            `application/json` (i.e. JSON/API calls).

    FetchSuccess:
      type: object
      required:
        - ok
        - status
        - final_url
        - title
        - html
        - body_text
        - json_parse_ok
        - json
        - headers
        - screenshot_b64
        - blocked
        - headed
        - pooled
        - attempts
        - timing_ms
      properties:
        ok:
          type: boolean
          const: true
          description: True when the browser request completed (not whether the target accepted it).
        status:
          type: [integer, "null"]
          description: HTTP status from the target navigation response.
        final_url:
          type: string
          format: uri
          description: Final browser URL after redirects.
        title:
          type: string
          description: Page title after rendering (empty string if unavailable mid-navigation).
        html:
          type: string
          description: Rendered DOM HTML from the browser.
        body_text:
          type: [string, "null"]
          description: Raw navigation response body when requested or when the response is JSON.
        json_parse_ok:
          type: boolean
          description: Whether `body_text` parsed as JSON.
        json:
          description: Parsed JSON payload when `json_parse_ok` is true; otherwise null.
          oneOf:
            - type: "null"
            - type: object
            - type: array
            - type: string
            - type: number
            - type: boolean
        headers:
          type: object
          additionalProperties:
            type: string
          description: Response headers from the target navigation response.
        screenshot_b64:
          type: [string, "null"]
          contentEncoding: base64
          contentMediaType: image/png
          description: Base64-encoded PNG when `screenshot` is true; otherwise null.
        cf_clearance:
          type: [string, "null"]
          description: >
            Cloudflare `cf_clearance` token value when `return_cf_clearance`
            is true and the target issued it. Otherwise null or omitted.
        cf_clearance_cookie:
          description: >
            Storage-ready `cf_clearance` cookie metadata when requested and
            present. Otherwise null or omitted.
          oneOf:
            - type: "null"
            - $ref: "#/components/schemas/CfClearanceCookie"
        cf_clearance_session:
          type: [string, "null"]
          description: >
            Better Fetch session that produced the `cf_clearance` result. If
            a blocked request was retried, this may be a rotated session.
        network:
          type: array
          items:
            $ref: "#/components/schemas/NetworkEntry"
          description: >
            Captured browser network entries when `capture_network` is true.
            Otherwise omitted.
        blocked:
          type: boolean
          description: >
            True when the response looks like a bot wall or unsolved
            challenge (block status with a small interstitial body, an
            interstitial page title, or a 200 JS interstitial). A solved
            page that merely carries Turnstile DOM is not blocked.
        headed:
          type: boolean
          description: Whether the attempt that produced this result ran a headed browser.
        pooled:
          type: boolean
          description: >
            True when served from a warm pooled context (session requests);
            false for sessionless one-off (ephemeral) contexts.
        attempts:
          type: integer
          minimum: 1
          description: >
            Total attempts including the initial one. Greater than 1 means
            the result was retried after a block on a fresh session.
        timing_ms:
          type: integer
          minimum: 0
          description: Time spent inside the fetch request (final attempt), in ms.

    NetworkEntry:
      type: object
      additionalProperties: false
      required:
        - url
        - method
        - resource_type
        - status
        - ok
        - from_service_worker
        - request_post_data
        - body_text
        - body_base64
        - body_bytes
        - body_truncated
        - json_parse_ok
        - json
        - error
      properties:
        url:
          type: string
        method:
          type: string
        resource_type:
          type: string
        status:
          type: [integer, "null"]
        ok:
          type: boolean
        from_service_worker:
          type: boolean
        request_post_data:
          type: [string, "null"]
        body_text:
          type: [string, "null"]
        body_base64:
          type: [string, "null"]
          contentEncoding: base64
        body_bytes:
          type: [integer, "null"]
          minimum: 0
        body_truncated:
          type: boolean
        json_parse_ok:
          type: boolean
        json:
          oneOf:
            - type: "null"
            - type: object
            - type: array
            - type: string
            - type: number
            - type: boolean
        error:
          type: [string, "null"]
        timing_ms:
          type: integer
          minimum: 0
        request_headers:
          type: object
          additionalProperties:
            type: string
        response_headers:
          type: object
          additionalProperties:
            type: string

    CfClearanceCookie:
      type: object
      additionalProperties: false
      required: [name, value, domain, path, expires, httpOnly, secure, sameSite]
      properties:
        name:
          type: string
          const: cf_clearance
        value:
          type: string
          description: Token value.
        domain:
          type: string
        path:
          type: string
        expires:
          type: [number, "null"]
          description: Unix timestamp expiry as reported by the browser, or null when absent.
        httpOnly:
          type: boolean
        secure:
          type: boolean
        sameSite:
          type: [string, "null"]

    Error:
      type: object
      additionalProperties: false
      required: [ok, error, message, status]
      description: >
        Uniform error envelope for every non-2xx response.
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: string
          description: Stable machine-readable error code.
          enum:
            - bad_request
            - unauthorized
            - payment_required
            - not_found
            - method_not_allowed
            - payload_too_large
            - quota_exceeded
            - internal_error
            - fetch_failed
            - timeout
        message:
          type: string
          description: Human-readable explanation.
        status:
          type: integer
          description: HTTP status code (mirrors the response status line).

    HealthResponse:
      type: object
      required: [ok, version, proxy_configured, pool_size, pool_max]
      properties:
        ok:
          type: boolean
          const: true
        version:
          type: string
          description: Service version.
        proxy_configured:
          type: boolean
          description: Whether residential proxy credentials are configured.
        pool_size:
          type: integer
          minimum: 0
          description: Number of warm pooled browser contexts.
        pool_max:
          type: integer
          minimum: 1
          description: Maximum pooled contexts before LRU eviction.
