{
  "openapi": "3.1.0",
  "info": {
    "title": "Web Unblocker API",
    "version": "1.0.0",
    "description": "Fetch or screenshot any public web page, including the ones that turn an ordinary request away. Send a URL, get the page back as HTML, visible text, Markdown or an image — and every response that returns a page reports what it cost and how long it took."
  },
  "servers": [
    {
      "url": "https://api.fetchlayer.dev/web-unblocker"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API Key",
        "description": "Send your FetchLayer key as Authorization: Bearer ss-your-key."
      }
    }
  },
  "paths": {
    "/fetch": {
      "post": {
        "operationId": "fetch",
        "tags": [
          "Pages"
        ],
        "summary": "Fetch a web page",
        "description": "Fetch any public web page and get it back as HTML, visible text or Markdown — including the pages an ordinary request comes back empty from, and the pages that turn an ordinary request away. A page is always served the cheapest way that works, and costs more only when an easier attempt has genuinely failed — so an easy page never subsidises a hard one. `pagesFetched` and `usage.billableUnits` report what this particular request actually cost, in units, at three units to the credit: **1 for a simple page, 3 for a harder one, 6 for a hard one, 9 for the hardest**. The whole request runs inside a 120-second budget. Work that cannot finish in what is left of that budget is **skipped and reported as skipped**, never started and cut off halfway — which would bill for work that produced nothing. A 404 returns immediately: the page is not there, and working harder cannot change that. A 503 means the page could not be read at all, and it is not billed. The hardest pages are also the slowest. In a large burst some will exhaust the 120-second budget and come back unread rather than wrong, so expect a slow tail and retry it rather than sizing every timeout for the worst case. Use `output: \"markdown\"` when the page is going to a model: it keeps the structure, drops the markup, and is far smaller than the HTML. `expectText` and `waitForSelector` are how you tell a real page from a block page — if what you asked for never appears, the request is reported unsuccessful rather than answered with the wrong content. Billing: a simple page costs 1 unit, a harder one 3, a hard one 6, and the hardest 9 — at three units to the credit, so a simple page is a third of a credit. The cost is on every response as `pagesFetched`, and explained in `usage.billing`. A 503 is not billed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "The page to fetch. Must be an http or https address."
                  },
                  "render": {
                    "type": "string",
                    "description": "Whether to run the page in a browser: auto, always, never. auto only runs one when a direct read is not enough.",
                    "example": "auto"
                  },
                  "maxEffort": {
                    "type": "string",
                    "description": "Your ceiling on how hard this request may try, cheapest first: basic, standard, advanced, maximum. It sets a ceiling on cost rather than choosing a tier — the cheapest that works is always used regardless. Set it to plain when you would rather a page failed than cost more than the cheapest tier.",
                    "example": "solve"
                  },
                  "country": {
                    "type": "string",
                    "description": "Two-letter country code to read the page from, when the page differs by country."
                  },
                  "session": {
                    "type": "string",
                    "description": "An arbitrary label. Requests sharing a label are read over the same connection where possible, so a multi-step flow looks like one visitor."
                  },
                  "assets": {
                    "type": "string",
                    "description": "How much of the page to load: minimal, standard, full. minimal is the document only; standard loads what is needed to render, without images or video; full loads everything. Lower settings are faster and cheaper.",
                    "example": "standard"
                  },
                  "waitUntil": {
                    "type": "string",
                    "description": "When to consider the page ready: domcontentloaded, load, networkidle. Only applies when the page runs in a browser.",
                    "example": "domcontentloaded"
                  },
                  "waitForSelector": {
                    "type": "string",
                    "description": "A CSS selector to wait for before reading the page. If it never appears the request is reported unsuccessful rather than returning a partial page."
                  },
                  "waitMs": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Extra time to let the page settle, in milliseconds, up to 15000."
                  },
                  "expectText": {
                    "type": "string",
                    "description": "Text that must appear on the page. If it does not, the request is reported unsuccessful rather than handing back the wrong page — a block page instead of the article you asked for."
                  },
                  "device": {
                    "type": "string",
                    "description": "Read the page as a desktop or mobile visitor.",
                    "example": "desktop"
                  },
                  "headers": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Request headers to send to the page, such as Accept-Language or Referer. Headers that could impersonate another caller are not forwarded, and any that are dropped are listed in notes."
                  },
                  "maxRedirects": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Redirects to follow before giving up, up to 20. Every hop is re-validated against the same address rules as the first."
                  },
                  "format": {
                    "type": "string",
                    "description": "Envelope format: json or markdown.",
                    "example": "json"
                  },
                  "output": {
                    "type": "string",
                    "description": "The form the page comes back in: html, text, markdown. html is the page as rendered; text is visible text only; markdown keeps the structure and drops the markup — much smaller, and the best choice for feeding to a model.",
                    "example": "html"
                  },
                  "maxBytes": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Largest page to read, in bytes (1024 to 16777216). A larger page comes back cut short, with truncated true and a note saying so."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "url"
                ]
              },
              "examples": {
                "default": {
                  "value": {
                    "url": "https://news.ycombinator.com/",
                    "output": "markdown"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transport": {
                      "type": "string",
                      "description": "The source. Always \"web\"."
                    },
                    "notes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Caveats you can act on: a page that carried no readable text, a body cut short at maxBytes, request headers that were not forwarded, or an escalation ceiling that was applied."
                    },
                    "pagesFetched": {
                      "type": "number",
                      "description": "What this request cost, in units — the same number as usage.billableUnits, under the field name billing reads. It is a cost, not a page count. A unit is a third of a credit, and the figure reflects how hard the page was to get: a simple page is 1 unit, a harder one 3 (one credit), a hard one 6, and the hardest 9 (three credits). A 503 is not billed."
                    },
                    "url": {
                      "type": "string",
                      "description": "The address that was requested."
                    },
                    "finalUrl": {
                      "type": "string",
                      "description": "The address the request ended at, after redirects."
                    },
                    "status": {
                      "type": "number",
                      "description": "The HTTP status the page answered with."
                    },
                    "redirects": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      },
                      "description": "Redirects that were followed, in order."
                    },
                    "title": {
                      "type": "string",
                      "description": "The page title, when it has one."
                    },
                    "contentType": {
                      "type": "string",
                      "description": "The content type the page declared."
                    },
                    "content": {
                      "type": "string",
                      "description": "The page, in the form asked for by output."
                    },
                    "contentFormat": {
                      "type": "string",
                      "description": "Which form content is in: html, text or markdown."
                    },
                    "textLength": {
                      "type": "number",
                      "description": "Characters of visible text the page carried. 0 with a note is how a page that renders nothing announces itself."
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when the page exceeded maxBytes and was cut short."
                    },
                    "usage": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "What the request cost, how long it took, and how much came back.",
                      "properties": {
                        "bytesTransferred": {
                          "type": "number",
                          "description": "Bytes transferred across every attempt. A lower bound when a browser was used, because some responses do not declare their length."
                        },
                        "totalMs": {
                          "type": "number",
                          "description": "Wall-clock time for the whole request, every attempt included."
                        },
                        "step": {
                          "type": "string",
                          "description": "The effort level that served the page — basic, standard, advanced or maximum, the same vocabulary as the maxEffort ceiling. This is what the charge is derived from, so a request that cost more than expected says which tier it landed in."
                        },
                        "billableUnits": {
                          "type": "number",
                          "description": "What this request cost, in units, at three units to the credit. The same number as pagesFetched. Charged requests run on a per-account counter rather than a fraction taken off each response, so one response shows the units and the credit falls on every third one — it averages to a third of a credit per unit over any real volume."
                        }
                      }
                    }
                  },
                  "additionalProperties": false
                },
                "examples": {
                  "default": {
                    "value": {
                      "transport": "web",
                      "notes": [],
                      "pagesFetched": 1,
                      "url": "https://news.ycombinator.com/",
                      "finalUrl": "https://news.ycombinator.com/",
                      "status": 200,
                      "redirects": [],
                      "contentType": "text/html; charset=utf-8",
                      "title": "Hacker News",
                      "content": "# Hacker News\n\n1. …",
                      "contentFormat": "markdown",
                      "textLength": 3923,
                      "truncated": false,
                      "usage": {
                        "step": "basic",
                        "bytesTransferred": 34097,
                        "totalMs": 598,
                        "billableUnits": 1
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing Bearer token"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    },
    "/screenshot": {
      "post": {
        "operationId": "screenshot",
        "tags": [
          "Pages"
        ],
        "summary": "Screenshot a web page",
        "description": "Capture any public web page as a PNG or JPEG, either the visible area or the whole scrollable page. An image is never a simple page, so a screenshot starts at the second tier rather than the first — everything above that is costed the same as a fetch. A page is always served the cheapest way that works, and costs more only when an easier attempt has genuinely failed — so an easy page never subsidises a hard one. `pagesFetched` and `usage.billableUnits` report what this particular request actually cost, in units, at three units to the credit: **1 for a simple page, 3 for a harder one, 6 for a hard one, 9 for the hardest**. The whole request runs inside a 120-second budget. Work that cannot finish in what is left of that budget is **skipped and reported as skipped**, never started and cut off halfway — which would bill for work that produced nothing. A 404 returns immediately: the page is not there, and working harder cannot change that. A 503 means the page could not be read at all, and it is not billed. The hardest pages are also the slowest. In a large burst some will exhaust the 120-second budget and come back unread rather than wrong, so expect a slow tail and retry it rather than sizing every timeout for the worst case. The image comes back base64-encoded in `imageBase64`, with its format, byte size and captured dimensions alongside. jpeg is much smaller than png on a photographic page. Billing: a screenshot starts at 3 units — one credit — rising to 6 on a hard page and 9 on the hardest. The cost is on every response as `pagesFetched`, and explained in `usage.billing`. A 503 is not billed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "The page to fetch. Must be an http or https address."
                  },
                  "render": {
                    "type": "string",
                    "description": "Whether to run the page in a browser: auto, always, never. auto only runs one when a direct read is not enough.",
                    "example": "auto"
                  },
                  "maxEffort": {
                    "type": "string",
                    "description": "Your ceiling on how hard this request may try, cheapest first: basic, standard, advanced, maximum. It sets a ceiling on cost rather than choosing a tier — the cheapest that works is always used regardless. Set it to plain when you would rather a page failed than cost more than the cheapest tier.",
                    "example": "solve"
                  },
                  "country": {
                    "type": "string",
                    "description": "Two-letter country code to read the page from, when the page differs by country."
                  },
                  "session": {
                    "type": "string",
                    "description": "An arbitrary label. Requests sharing a label are read over the same connection where possible, so a multi-step flow looks like one visitor."
                  },
                  "assets": {
                    "type": "string",
                    "description": "How much of the page to load: minimal, standard, full. minimal is the document only; standard loads what is needed to render, without images or video; full loads everything. Lower settings are faster and cheaper.",
                    "example": "standard"
                  },
                  "waitUntil": {
                    "type": "string",
                    "description": "When to consider the page ready: domcontentloaded, load, networkidle. Only applies when the page runs in a browser.",
                    "example": "domcontentloaded"
                  },
                  "waitForSelector": {
                    "type": "string",
                    "description": "A CSS selector to wait for before reading the page. If it never appears the request is reported unsuccessful rather than returning a partial page."
                  },
                  "waitMs": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Extra time to let the page settle, in milliseconds, up to 15000."
                  },
                  "expectText": {
                    "type": "string",
                    "description": "Text that must appear on the page. If it does not, the request is reported unsuccessful rather than handing back the wrong page — a block page instead of the article you asked for."
                  },
                  "device": {
                    "type": "string",
                    "description": "Read the page as a desktop or mobile visitor.",
                    "example": "desktop"
                  },
                  "headers": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Request headers to send to the page, such as Accept-Language or Referer. Headers that could impersonate another caller are not forwarded, and any that are dropped are listed in notes."
                  },
                  "maxRedirects": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "Redirects to follow before giving up, up to 20. Every hop is re-validated against the same address rules as the first."
                  },
                  "format": {
                    "type": "string",
                    "description": "Envelope format: json or markdown.",
                    "example": "json"
                  },
                  "fullPage": {
                    "type": "boolean",
                    "description": "Capture the whole scrollable page rather than just the visible area.",
                    "example": "false"
                  },
                  "imageFormat": {
                    "type": "string",
                    "description": "png or jpeg. jpeg is much smaller for a photographic page.",
                    "example": "png"
                  }
                },
                "additionalProperties": false,
                "required": [
                  "url"
                ]
              },
              "examples": {
                "default": {
                  "value": {
                    "url": "https://example.com/",
                    "imageFormat": "jpeg"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transport": {
                      "type": "string",
                      "description": "The source. Always \"web\"."
                    },
                    "notes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Caveats you can act on: a page that carried no readable text, a body cut short at maxBytes, request headers that were not forwarded, or an escalation ceiling that was applied."
                    },
                    "pagesFetched": {
                      "type": "number",
                      "description": "What this request cost, in units — the same number as usage.billableUnits, under the field name billing reads. It is a cost, not a page count. A unit is a third of a credit, and the figure reflects how hard the page was to get: a simple page is 1 unit, a harder one 3 (one credit), a hard one 6, and the hardest 9 (three credits). A 503 is not billed."
                    },
                    "url": {
                      "type": "string",
                      "description": "The address that was requested."
                    },
                    "finalUrl": {
                      "type": "string",
                      "description": "The address the request ended at, after redirects."
                    },
                    "status": {
                      "type": "number",
                      "description": "The HTTP status the page answered with."
                    },
                    "redirects": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      },
                      "description": "Redirects that were followed, in order."
                    },
                    "title": {
                      "type": "string",
                      "description": "The page title, when it has one."
                    },
                    "imageBase64": {
                      "type": "string",
                      "description": "The image, base64-encoded."
                    },
                    "imageFormat": {
                      "type": "string",
                      "description": "png or jpeg."
                    },
                    "imageBytes": {
                      "type": "number",
                      "description": "Size of the image in bytes."
                    },
                    "width": {
                      "type": "number",
                      "description": "Captured width in pixels."
                    },
                    "height": {
                      "type": "number",
                      "description": "Captured height in pixels."
                    },
                    "fullPage": {
                      "type": "boolean",
                      "description": "Whether the whole scrollable page was captured."
                    },
                    "usage": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "What the request cost, how long it took, and how much came back.",
                      "properties": {
                        "bytesTransferred": {
                          "type": "number",
                          "description": "Bytes transferred across every attempt. A lower bound when a browser was used, because some responses do not declare their length."
                        },
                        "totalMs": {
                          "type": "number",
                          "description": "Wall-clock time for the whole request, every attempt included."
                        },
                        "step": {
                          "type": "string",
                          "description": "The effort level that served the page — basic, standard, advanced or maximum, the same vocabulary as the maxEffort ceiling. This is what the charge is derived from, so a request that cost more than expected says which tier it landed in."
                        },
                        "billableUnits": {
                          "type": "number",
                          "description": "What this request cost, in units, at three units to the credit. The same number as pagesFetched. Charged requests run on a per-account counter rather than a fraction taken off each response, so one response shows the units and the credit falls on every third one — it averages to a third of a credit per unit over any real volume."
                        }
                      }
                    }
                  },
                  "additionalProperties": false
                },
                "examples": {
                  "default": {
                    "value": {
                      "transport": "web",
                      "notes": [
                        "This page was run in a browser and still returned no readable text."
                      ],
                      "pagesFetched": 2,
                      "url": "https://example.com/",
                      "finalUrl": "https://example.com/",
                      "status": 200,
                      "redirects": [],
                      "title": "Example Domain",
                      "imageBase64": "/9j/4AAQSkZJRgABAQAAAQ…",
                      "imageFormat": "jpeg",
                      "imageBytes": 31462,
                      "width": 1280,
                      "height": 720,
                      "fullPage": false,
                      "usage": {
                        "step": "standard",
                        "bytesTransferred": 671,
                        "totalMs": 7483,
                        "billableUnits": 3
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing Bearer token"
          }
        },
        "security": [
          {
            "BearerAuth": []
          }
        ]
      }
    }
  }
}