Extract Forms from Web Pages — Detect Login Forms, Hidden Fields & Checkout Flows

Detect login forms, find hidden form fields, and extract checkout flows from any URL. Structured JSON with field types, action URLs, and pixel positions. Try free.

Vitalii Holben Updated

The Page Analysis API extracts every <form> element from a rendered web page and returns it as structured JSON. For each form you get the action URL, HTTP method, field count, and a bounding rectangle in pixels. Individual fields (inputs, selects, textareas, buttons) come back in the interactive category with their own coordinates, types, and labels. One POST to /v1/analyze with include: ["forms", "interactive"], and you can extract forms from any URL without writing a single DOM query.

curl -X POST https://api.screenshotrun.com/v1/analyze \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/login",
    "include": ["forms", "interactive"]
  }'

Form data splits across two categories on purpose. The forms array gives you the container: where does this form live, where does it submit, how many fields does it have? The interactive array gives you every field inside it: email inputs, password fields, submit buttons, checkboxes, hidden inputs. Together they answer both "what forms exist on this page?" and "what does each form ask for?"

What you get back for each form

Every form element returns four properties.

{
  "forms": [
    {
      "action": "/api/auth/login",
      "method": "POST",
      "fields": 3,
      "rect": [400, 200, 480, 320]
    }
  ],
  "interactive": [
    { "tag": "input", "type": "email", "name": "email", "label": "Email address", "rect": [420, 260, 440, 40] },
    { "tag": "input", "type": "password", "name": "password", "label": "Password", "rect": [420, 320, 440, 40] },
    { "tag": "button", "type": "submit", "text": "Sign In", "rect": [420, 390, 440, 44] }
  ]
}

The action value is pulled straight from the HTML attribute: the URL where the form submits. Empty string means it posts to the current page. method is GET or POST. fields counts every input, select, and textarea inside the form container. And rect gives you the form's pixel position on the rendered page: x=400, y=200, 480px wide, 320px tall.

Interactive elements within each form carry their type (email, password, text, checkbox, hidden), name attribute (handy for constructing programmatic submissions), and label pulled from aria-label. Every field has its own bounding rectangle, so you know exactly where it sits on the rendered page.

How to detect login forms on any website

A login form has a recognizable pattern: one or two text/email inputs, a password input, and a submit button. You can identify login forms from the structured JSON without parsing any HTML.

import requests

response = requests.post(
    "https://api.screenshotrun.com/v1/analyze",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "url": target_url,
        "include": ["forms", "interactive"],
        "stealth": True,
    },
    timeout=60,
)

data = response.json()

# Find forms that look like login pages
for form in data["result"]["forms"]:
    fields = [
        el for el in data["result"]["interactive"]
        if el["tag"] == "input"
        and form["rect"][0] <= el["rect"][0] <= form["rect"][0] + form["rect"][2]
        and form["rect"][1] <= el["rect"][1] <= form["rect"][1] + form["rect"][3]
    ]

    has_password = any(f.get("type") == "password" for f in fields)
    has_email_or_text = any(f.get("type") in ("email", "text") for f in fields)

    if has_password and has_email_or_text:
        print(f"Login form found: action={form['action']}, position={form['rect']}")
        for f in fields:
            print(f"  {f.get('type', 'unknown')}: name={f.get('name', '')}")

This script checks if each form contains a password field alongside an email or text input. That two-field pattern identifies login forms with high accuracy across most sites. Bounding rectangle overlap maps interactive elements back to their parent form — fields physically inside the form's area belong to it.

Finding hidden forms

Set visible_only: false to include forms that aren't visible in the viewport. Some sites load forms inside collapsed accordions, behind modals, or off-screen. Others hide forms with display: none or opacity: 0. A page might show a "Login" button that opens a modal containing the actual form — with visible_only: true (the default), you'd miss it.

{
  "url": "https://example.com",
  "include": ["forms", "interactive"],
  "visible_only": false
}

Hidden forms matter for security audits. A phishing page might have an invisible form that captures keystrokes. A compromised site might include a hidden form posting credentials to a third-party domain. When you set visible_only: false, the API returns every form the browser's DOM contains, whether the user can see it or not.

Compare the action URLs of hidden forms against the page's own domain. If a hidden form posts to a different host, that's a red flag worth investigating.

Why browser rendering matters for finding forms

Fetching raw HTML with a GET request and running a regex for <form> tags works on static sites. It fails everywhere else. React renders forms after JavaScript executes. Angular templates compile into DOM elements at runtime. Vue components mount their form markup dynamically. A page might load a login form only after a user clicks "Sign In," assembling the <form> from a JavaScript template that doesn't exist in the initial HTML response.

The analyze endpoint loads the page in a real Chromium browser. It waits for JavaScript to finish, for lazy components to mount, for AJAX-loaded forms to appear. The forms you get back are the forms a real user would see (or, with visible_only: false, the forms the browser has in its DOM tree).

Sites that block headless browsers (banking portals, e-commerce checkouts, enterprise SaaS login pages) often use bot defenses to prevent exactly this kind of form scanning. Stealth mode bypasses common headless browser checks: it patches the navigator object, fixes WebGL fingerprints, and emulates human-like browser properties. Enable it with stealth: true when the target site is likely to detect automated requests.

Extracting forms for AI agents

An AI agent built with LangChain or Claude MCP needs to fill out forms on websites. Before filling anything, the agent has to answer three questions: Is there a form? What fields does it have? Where is each field on screen?

Raw HTML wastes context tokens answering these questions. A typical login page contains 10,000–50,000 tokens of markup — navigation, footer, scripts, meta tags — and the form accounts for maybe 200 tokens of that. Our analyze endpoint strips away everything except the form structure and its fields. What comes back is a JSON object small enough for the agent to reason about directly.

// AI agent form-filling workflow
const analysis = await fetch("https://api.screenshotrun.com/v1/analyze", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCREENSHOTRUN_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://app.example.com/signup",
    include: ["forms", "interactive"],
    block_cookies: true,
  }),
}).then(r => r.json());

const forms = analysis.result.forms;
const fields = analysis.result.interactive.filter(el => el.tag === "input");

// Agent now knows: 1 form at position [x,y,w,h], 4 input fields, types and names
// It can decide what to fill and calculate click coordinates

With the ScreenshotRun MCP Server, Claude can call analyze_page directly. Pass include: ["forms", "interactive"] and the model receives a compact form map it can reason about — which fields to fill, what type each expects, where to click.

Monitoring checkout and signup flows

Track changes to competitor checkout forms by running periodic analyses. If a competitor adds a new field to their signup form, removes a coupon code input from checkout, or changes the form action URL to a new payment processor — you'll see it in the diff between two JSON responses.

# Compare forms between two runs
import json

old = json.load(open("analysis-oct-15.json"))
new = json.load(open("analysis-oct-22.json"))

old_forms = {f["action"]: f for f in old["result"]["forms"]}
new_forms = {f["action"]: f for f in new["result"]["forms"]}

for action, form in new_forms.items():
    if action not in old_forms:
        print(f"NEW form: {action} ({form['fields']} fields)")
    elif form["fields"] != old_forms[action]["fields"]:
        print(f"CHANGED: {action} — {old_forms[action]['fields']} → {form['fields']} fields")

for action in old_forms:
    if action not in new_forms:
        print(f"REMOVED form: {action}")

This works for e-commerce monitoring, competitive intelligence, and verifying compliance. A financial services company might track whether their public-facing forms still submit to approved payment gateways. An e-commerce team might flag when a competitor reduces friction by removing required fields from checkout.

Security: detect suspicious forms and phishing pages

Phishing pages look like real login pages but submit credentials to attacker-controlled servers. Structurally, the pattern is easy to spot: a form with a password field whose action URL points to a different domain than the page itself. Or a hidden form with zero visible fields but an action URL pointing to an external host.

from urllib.parse import urlparse

page_domain = urlparse(target_url).netloc

for form in data["result"]["forms"]:
    action_url = form["action"]
    if action_url and not action_url.startswith("/"):
        action_domain = urlparse(action_url).netloc
        if action_domain and action_domain != page_domain:
            print(f"SUSPICIOUS: form submits to {action_domain} (page is {page_domain})")
            print(f"  Method: {form['method']}, Fields: {form['fields']}, Position: {form['rect']}")

Run this across a list of URLs flagged by your threat intel feed. ScreenshotRun handles browser rendering and bypassing bot defenses. Your code handles the logic: cross-domain action URLs, hidden forms with password fields, forms without visible submit buttons. These patterns surface phishing pages that look convincing to humans but have structural tells in the DOM.

For defacement monitoring, compare today's form count and action URLs against yesterday's. If a site that had two forms suddenly has three, and the new one posts to an unfamiliar domain, the page may have been compromised.

Scoping to a specific container

If the page has forms in the header (search), sidebar (newsletter), and main content (checkout), use the scope parameter to focus on just one section.

{
  "url": "https://shop.example.com/checkout",
  "scope": "#checkout-form",
  "include": ["forms", "interactive"]
}

Scoping to #checkout-form returns only the checkout form and its fields. The newsletter signup in the footer, the search bar in the header: all excluded. This reduces response size and eliminates noise when you know exactly which form you care about.

Pricing

Extracting forms uses the Page Analysis endpoint. Free and Starter plans include 20 trial runs monthly. Pro, Growth, and Business plans have no limit. Each call costs 1 API request (2 if you add with_screenshot: true). The API docs cover the full parameter reference.

FAQ#

Each form returns four properties: action (the submission URL), method (GET or POST), fields (count of inputs, selects, and textareas inside the form), and rect (bounding rectangle as [x, y, width, height] in pixels). Individual form fields appear in the interactive category with their type, name, label, and position.

The API returns structured data for all forms on a page. You can identify login forms programmatically by checking for the presence of a password input alongside an email or text input. The bounding rectangle overlap lets you map fields back to their parent form.

Set visible_only to false in your request. This tells the API to return all forms in the DOM, including those hidden with display:none, opacity:0, or positioned off-screen. Useful for security audits and phishing detection.

Yes. The API loads pages in a real Chromium browser and waits for JavaScript to execute before analyzing. Forms rendered by React, Angular, Vue, or loaded via AJAX are all captured. Enable stealth mode for sites that block headless browsers.

Yes, use the scope parameter with a CSS selector. For example, scope: "#checkout-form" limits analysis to that container only, excluding forms in the header, footer, or sidebar.

Written by

Vitalii Holben

Co-founder and CEO of ScreenshotRun. Full-stack developer building screenshot API tools that help developers capture, automate, and monitor web pages at scale. Based in Europe, shipping code since 201...

Screenshot API in a few clicks

Free plan includes 200 screenshots every month. No credit card required.

Sign up → copy your key → make your first call