# Click, type and scroll with browser actions

> Drive a rendered page with the actions array: wait_for, wait_for_navigation, click, fill, select, scroll, evaluate and screenshot steps, up to 50 per request, before the page is captured.

Source: https://docs.spicrawl.com/guides/browser-actions

Use this when the content you need appears only after you interact with the page: a "Load more" button, a cookie wall, a tab, a search box, a login form, or an infinite-scroll feed. `actions` is an ordered list of steps the browser runs after the page loads and before it is captured. The whole list is validated before anything is spent.

Actions are browser-only. Set `js_render: true` or pin a browser `engine`; on the fetch tier they are a 400.

## Minimal request

Click "Load more", wait for the new rows, and return the page as markdown:

```bash title="curl"
curl https://api.spicrawl.com/v1/scrape \
  -H "Authorization: Bearer $SPICRAWL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/products?category=lamps",
    "js_render": true,
    "response_format": "markdown",
    "actions": [
      {"click": "button.load-more"},
      {"wait_for": ".product-card:nth-child(40)"}
    ]
  }'
```

```python title="Python"
import os, requests

r = requests.post(
    "https://api.spicrawl.com/v1/scrape",
    headers={"Authorization": f"Bearer {os.environ['SPICRAWL_API_KEY']}"},
    json={
        "url": "https://example.com/products?category=lamps",
        "js_render": True,
        "response_format": "markdown",
        "actions": [
            {"click": "button.load-more"},
            {"wait_for": ".product-card:nth-child(40)"},
        ],
    },
    timeout=180,
)
r.raise_for_status()
print(r.text)
```

```typescript title="TypeScript"
const r = await fetch("https://api.spicrawl.com/v1/scrape", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SPICRAWL_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com/products?category=lamps",
    js_render: true,
    response_format: "markdown",
    actions: [{ click: "button.load-more" }, { wait_for: ".product-card:nth-child(40)" }],
  }),
});
if (!r.ok) throw new Error(JSON.stringify(await r.json()));
console.log(await r.text());
```

```bash title="CLI"
spicrawl scrape "https://example.com/products?category=lamps" --render --format markdown \
  --actions '[{"click":"button.load-more"},{"wait_for":".product-card:nth-child(40)"}]'
# or keep the steps in a file: --actions @steps.json
```

The response is the page after the last step, in the `response_format` you asked for, with two exceptions that switch it to the JSON envelope:

* **A `screenshot` step.** An image cannot travel in a markdown, text or HTML body, so a request with a `screenshot` step is answered with the JSON envelope whatever `response_format` says: the rendering under `content` (still in the format you asked for) and the images under `screenshots`, each under its step's label. The response carries a `FORMAT_COERCED` warning saying so. Send `response_format: "json"` to get the envelope without the warning.
* **An `evaluate` step with `return_value: true`.** Same rule: the value comes back under `actions` in the envelope, with the same warning.

The envelope's `actions` array echoes every step: `index`, `label`, `ok`, `duration_ms`, `url_after` (the page's URL when the step finished) and, for `evaluate`, `return_value`.

## Step shape

Each step is an object with exactly one verb key, plus optional `label`, `timeout_ms` and `on_error`. Two verb keys in one step, or an unknown verb, is a 400 before anything runs.

| Verb                  | Body                                                                                                  | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| --------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wait_for`            | `".price"` or `{"selector": ".price"}` or `{"ms": 1500}`                                              | Waits for a selector, or sleeps. `ms` (alias `wait`) is 0–30000. Needs `selector` or a positive `ms`.                                                                                                                                                                                                                                                                                                                                                                                              |
| `click`               | `"button.buy"` or `{"selector": "button.buy"}`, `{"selector": "a.next", "wait_for_navigation": true}` | Scrolls the element into view, then clicks it once. A click is a click: on a checkbox or radio it **toggles** the current state, there is no "set checked". To make sure a box ends up ticked, click it only when `evaluate` says it is not, or set it with `evaluate` (`el.checked = true` plus a `change` event). With `"wait_for_navigation": true` the step finishes only once the page the click navigates to has loaded; add `"until"` (see `wait_for_navigation`) to choose the load state. |
| `wait_for_navigation` | `{}`, `"networkidle"` or `{"until": "domcontentloaded"}`                                              | Waits for a navigation that started after the previous step began (a clicked link, a submitted form, a script redirect) to commit and reach `until`: `load` (default), `domcontentloaded` or `networkidle`. No navigation within `timeout_ms` fails the step (`ERR::UPSTREAM::TIMEOUT`). Needs `obscura` or `chromium`.                                                                                                                                                                            |
| `fill`                | `{"selector": "#email", "value": "ada@example.com"}`                                                  | Clears the input, then types `value`. `value` must be a string. Add `"secret": true` for passwords.                                                                                                                                                                                                                                                                                                                                                                                                |
| `select`              | `{"selector": "#size", "value": "L"}` or `{"selector": "#tags", "values": ["a", "b"]}`                | Chooses option values in a `<select>`. `values` wins over `value`.                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `scroll`              | `{"y": 1200}`, `{"selector": "#reviews"}` or `{"to_bottom": true}`                                    | See [Scrolling](#scrolling).                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `evaluate`            | `"document.querySelector('.modal')?.remove()"` or `{"expression": "…"}`                               | Runs JavaScript in the page.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `screenshot`          | `{"full_page": true}`, `{"selector": ".chart"}`, `{"format": "jpeg", "quality": 80}`                  | Captures an image mid-workflow. Needs `chromium`. `full_page` and `selector` are exclusive; `quality` is 0–100.                                                                                                                                                                                                                                                                                                                                                                                    |

String shorthand works for `wait_for`, `wait_for_navigation`, `click` and `evaluate` only. `fill`, `select`, `scroll` and `screenshot` need an object.

Shared keys, accepted on the step or inside the verb object (both spellings mean the same thing):

| Key          | Default                          | Effect                                                                                                                                                                                 |
| ------------ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `label`      | `<verb>[index]`, e.g. `click[0]` | Name used in diagnostics, request logs and screenshot labels.                                                                                                                          |
| `timeout_ms` | engine default                   | Per-step timeout in ms. `0` or omitted uses the engine default. The step is cut off at `timeout_ms` plus at most 250 ms, and never runs into the time reserved for capturing the page. |
| `on_error`   | `fail`                           | `fail` ends the request at 0 credits, naming the step. `skip` records the failure and continues with the next step.                                                                    |

Use `on_error: "skip"` for steps that may legitimately find nothing, such as dismissing a cookie banner that only some visitors see.

## Scrolling

`scroll` accepts one target. If you set more than one, `to_bottom` wins, then `selector`, then `y`.

* `{"y": 1200}` scrolls vertically by that many pixels (negative scrolls up). `pixels` is an alias. There is no horizontal scroll.
* `{"selector": "#reviews"}` brings that element to the centre of the viewport. No match fails the step.
* `{"to_bottom": true}` scrolls to the bottom repeatedly and stops when the page height stops growing, or when the step's timeout runs out. Use it for infinite-scroll feeds.

A `scroll` with none of these (or `y: 0`) is a 400.

## `evaluate` and `secret`

`evaluate` is for side effects: removing an overlay, expanding collapsed sections, setting a value a widget ignores from `fill`. With `{"expression": "…", "return_value": true}` the expression's value, as JSON, is returned in the envelope's `actions` array under that step (`return_value`); add `"await_promise": true` for an async expression. Asking for a value switches the response to the JSON envelope, as a screenshot does.

`"secret": true` on `fill` redacts the value from logs, results, diagnostics and your request history. Set it on every password, token or one-time code.

## Worked example: log in, scroll, extract

```json
{
  "url": "https://example.com/login",
  "engine": "chromium",
  "actions": [
    {"click": "#accept-cookies", "on_error": "skip"},
    {"fill": {"selector": "input[name=email]", "value": "ada@example.com"}},
    {"fill": {"selector": "input[name=password]", "value": "correct-horse-battery", "secret": true}},
    {"click": {"selector": "button[type=submit]", "wait_for_navigation": true}, "label": "submit-login", "timeout_ms": 15000},
    {"wait_for": {"selector": ".orders-table", "timeout_ms": 15000}},
    {"scroll": {"to_bottom": true}, "timeout_ms": 20000},
    {"screenshot": {"full_page": true}, "label": "orders"}
  ],
  "extract": {
    "orders": {"selector": ".orders-table tbody tr", "kind": "list", "output": "text"},
    "next_page": "a.pagination-next@href"
  }
}
```

The response is the envelope: `data.orders` and `data.next_page` from the page as it stood after the last step, plus a screenshot labelled `orders`. To stay logged in across requests instead of logging in every time, run the login steps once with a `session_id`; see [Sessions and logins](https://docs.spicrawl.com/guides/sessions-and-logins.md).

## Failure modes

| Code                               | HTTP | What to do                                                                                                                                                                                        |
| ---------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ERR::REQUEST::INVALID_PARAMETER`  | 400  | More than 50 steps, two verbs in one step, an unknown verb, a missing `selector`/`value`, or a bad `on_error`. `detail` names the step, e.g. `actions[3]`. 0 credits.                             |
| `ERR::REQUEST::INCOMPATIBLE_FLAGS` | 400  | Actions on the fetch tier. Add `js_render: true` or pin a browser `engine`.                                                                                                                       |
| `ERR::ENGINE::RENDER_FAILED`       | 502  | A step failed (no element matched, a click was intercepted, a script threw). `detail` names the step's label. Fix the selector, add a `wait_for` before it, or set `on_error: "skip"`. 0 credits. |
| `ERR::UPSTREAM::TIMEOUT`           | 504  | A step or the whole render ran out of time. Raise that step's `timeout_ms`, or split the workflow. 0 credits.                                                                                     |

Each step holds the browser for up to its own timeout, which is why the list is capped at 50.

## Cost

Actions add no credits. You pay the engine that ran: `obscura` (`js_render: true`) 3 credits, `chromium` 8. A failed step costs 0.

Requests with `actions` are never served from or written to the [cache](https://docs.spicrawl.com/guides/caching.md), and they turn off the default image and font blocking so that the layout you click against is complete. See [Credits](https://docs.spicrawl.com/credits.md).

## Related

* [Sessions and logins](https://docs.spicrawl.com/guides/sessions-and-logins.md) to keep cookies between requests
* [Screenshots and PDF](https://docs.spicrawl.com/guides/screenshots-and-pdf.md)
* [Structured data](https://docs.spicrawl.com/guides/structured-data.md) for the `extract` syntax
* [CDP browser](https://docs.spicrawl.com/guides/cdp-browser.md) (coming soon)
