> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nativeport.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nativeport.ai/_mcp/server.

# Steel

Fronts three single-shot actions on [Steel](https://docs.steel.dev)'s Browser Tools REST API (`https://api.steel.dev`) — each one page load, one managed browser, one response:

```
POST /steel/scrape       → https://api.steel.dev/v1/scrape
POST /steel/screenshot   → https://api.steel.dev/v1/screenshot
POST /steel/pdf          → https://api.steel.dev/v1/pdf
```

Steel is a full browser-automation platform, not just these three actions — Sessions, a CDP relay for driving a live browser, and account-level Files, Credentials, Profiles, and Extensions all sit behind the same account key every NativePort tenant shares. Only the three Browser Tools actions above are stateless and single-call, so only those are mounted.

**Sessions, the CDP relay, profiles, credentials, files, extensions, and CAPTCHA/proxy configuration are not exposed.** Any other Steel path draws the gateway's own `404`. Those surfaces are either stateful, cross-tenant resources on the one shared Steel account, or cost-expanding channels this gateway has no ownership-tracking layer for.

Like [Browserless](/browserless), `/steel` runs a closed **field policy** ahead of the upstream call — a fixed allow-set of JSON body fields, everything else rejected before Steel is ever touched. Unlike Browserless, Steel's Browser Tools accept **no query parameters at all**: auth and every control travel in the `steel-api-key` header or the JSON body, so the query-string policy here is simpler — any query parameter present is rejected outright.

## Credential handling

* **`steel-api-key` is injected server-side** on every call; `Authorization` is dropped before the request goes upstream.
* There's no forced timeout field to strip — Steel's only timing knob, `delay`, is a body field the policy already bounds directly (see below).

## Field policy

Every JSON body field is explicitly admitted or rejected — there's no passthrough for anything the list below doesn't name.

**Admitted on all three routes:** `url` (required, `http://` or `https://`), `delay` (integer milliseconds, `0`–`10000`, to wait after navigation before capturing).

**Extra fields per route:**

* `/scrape` also admits `format` (an array — `html`, `cleaned_html`, `markdown`, `readability`), plus `screenshot` and `pdf` (booleans) to bundle a hosted screenshot or PDF alongside the scraped content in the same call.
* `/screenshot` also admits `fullPage` (boolean) — capture the entire scrollable page instead of just the viewport.
* `/pdf` admits nothing beyond the common set.

**Denied outright — a `400` before the request reaches Steel:** `useProxy` (Steel's residential-proxy toggle), and any other body field or query parameter the policy doesn't name above. Default-deny, not default-allow.

A rejected call answers `400` with `{"error": "Rejected: <reason>."}` and is never forwarded — Steel never sees it, and it's never billed.

## Scrape

`POST /steel/scrape` — loads `url` and returns page content. The response always includes `content`, `metadata`, and `links`; `format` controls which content variants come back (`html` if omitted).

```bash
curl -X POST "$GATEWAY/steel/scrape" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "format": ["markdown", "readability"]}'
```

Response: `application/json` — Steel's own scrape result, relayed as-is.

```json
{
  "content": {
    "markdown": "# Example Domain\n\n..."
  },
  "metadata": {
    "title": "Example Domain",
    "statusCode": 200
  },
  "links": []
}
```

Add `screenshot: true` or `pdf: true` to bundle a hosted file alongside the scraped content:

```bash
curl -X POST "$GATEWAY/steel/scrape" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "format": ["markdown"], "screenshot": true}'
```

## Screenshot

`POST /steel/screenshot` — loads `url` and returns a hosted PNG. Use `fullPage: true` to capture the entire scrollable page instead of just the viewport.

```bash
curl -X POST "$GATEWAY/steel/screenshot" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "fullPage": true}'
```

Response: `application/json`, a hosted, public, durable URL:

```json
{ "url": "https://files.steel.dev/v1/static/example.png" }
```

## PDF

`POST /steel/pdf` — loads `url` and returns a hosted PDF. Same shape as screenshot: a JSON body with one hosted URL.

```bash
curl -X POST "$GATEWAY/steel/pdf" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'
```

Response: `application/json`:

```json
{ "url": "https://files.steel.dev/v1/static/example.pdf" }
```

## Billing

Billed a flat **5,000 microcredits (\$0.005) per accepted, forwarded call**, charged synchronously *before* the request reaches Steel — Steel bills each Browser Tools call flat regardless of outcome, so the charge lands the moment the gateway commits to forwarding and applies the same whether the call succeeds, Steel answers a non-2xx, or the upstream request fails outright. A call the field policy rejects (see above) is never charged.

There's no idempotency key on this route: a retry after a failed or timed-out call opens its own new Steel Browser Tools call and is billed as its own, independent attempt.

Field-level request and response detail for the three admitted actions lives in [Steel's Browser Tools docs](https://docs.steel.dev/overview/browser-tools/overview); the gateway modifies neither beyond the field policy described above.