# Scrapfly Documentation

## Table of Contents

### Dashboard

- [Intro](https://scrapfly.io/docs)
- [Project](https://scrapfly.io/docs/project)
- [Account](https://scrapfly.io/docs/account)
- [Workspace & Team](https://scrapfly.io/docs/workspace-and-team)
- [Billing](https://scrapfly.io/docs/billing)

### Products

#### MCP Server

- [Getting Started](https://scrapfly.io/docs/mcp/getting-started)
- [Tools & API Spec](https://scrapfly.io/docs/mcp/tools)
- [Authentication](https://scrapfly.io/docs/mcp/authentication)
- [Examples & Use Cases](https://scrapfly.io/docs/mcp/examples)
- [FAQ](https://scrapfly.io/docs/mcp/faq)
##### Integrations

- [Overview](https://scrapfly.io/docs/mcp/integrations)
- [Claude Desktop](https://scrapfly.io/docs/mcp/integrations/claude-desktop)
- [Claude Code](https://scrapfly.io/docs/mcp/integrations/claude-code)
- [ChatGPT](https://scrapfly.io/docs/mcp/integrations/chatgpt)
- [Cursor](https://scrapfly.io/docs/mcp/integrations/cursor)
- [Cline](https://scrapfly.io/docs/mcp/integrations/cline)
- [Windsurf](https://scrapfly.io/docs/mcp/integrations/windsurf)
- [Zed](https://scrapfly.io/docs/mcp/integrations/zed)
- [Roo Code](https://scrapfly.io/docs/mcp/integrations/roo-code)
- [VS Code](https://scrapfly.io/docs/mcp/integrations/vscode)
- [LangChain](https://scrapfly.io/docs/mcp/integrations/langchain)
- [LlamaIndex](https://scrapfly.io/docs/mcp/integrations/llamaindex)
- [CrewAI](https://scrapfly.io/docs/mcp/integrations/crewai)
- [OpenAI](https://scrapfly.io/docs/mcp/integrations/openai)
- [n8n](https://scrapfly.io/docs/mcp/integrations/n8n)
- [Make](https://scrapfly.io/docs/mcp/integrations/make)
- [Zapier](https://scrapfly.io/docs/mcp/integrations/zapier)
- [Vapi AI](https://scrapfly.io/docs/mcp/integrations/vapi)
- [Agent Builder](https://scrapfly.io/docs/mcp/integrations/agent-builder)
- [Custom Client](https://scrapfly.io/docs/mcp/integrations/custom-client)


#### Web Scraping API

- [Getting Started](https://scrapfly.io/docs/scrape-api/getting-started)
- [API Specification]()
- [Monitoring](https://scrapfly.io/docs/monitoring)
- [Customize Request](https://scrapfly.io/docs/scrape-api/custom)
- [Debug](https://scrapfly.io/docs/scrape-api/debug)
- [Unblocker (formerly ASP)](https://scrapfly.io/docs/scrape-api/unblocker)
- [Proxy](https://scrapfly.io/docs/scrape-api/proxy)
- [Proxy Mode](https://scrapfly.io/docs/scrape-api/proxy-mode)
- [Proxy Mode - Screaming Frog](https://scrapfly.io/docs/scrape-api/proxy-mode/screaming-frog)
- [Proxy Mode - Apify](https://scrapfly.io/docs/scrape-api/proxy-mode/apify)
- [(Auto) Data Extraction](https://scrapfly.io/docs/scrape-api/extraction)
- [Javascript Rendering](https://scrapfly.io/docs/scrape-api/javascript-rendering)
- [Javascript Scenario](https://scrapfly.io/docs/scrape-api/javascript-scenario)
- [SSL](https://scrapfly.io/docs/scrape-api/ssl)
- [DNS](https://scrapfly.io/docs/scrape-api/dns)
- [Cache](https://scrapfly.io/docs/scrape-api/cache)
- [Batch (Multi-URL Scraping)](https://scrapfly.io/docs/scrape-api/batch)
- [Session](https://scrapfly.io/docs/scrape-api/session)
- [Webhook](https://scrapfly.io/docs/scrape-api/webhook)
- [Schedule](https://scrapfly.io/docs/scrape-api/schedule)
- [Screenshot](https://scrapfly.io/docs/scrape-api/screenshot)
- [Errors](https://scrapfly.io/docs/scrape-api/errors)
- [Timeout](https://scrapfly.io/docs/scrape-api/understand-timeout)
- [Throttling](https://scrapfly.io/docs/throttling)
- [Troubleshoot](https://scrapfly.io/docs/scrape-api/troubleshoot)
- [Billing](https://scrapfly.io/docs/scrape-api/billing)
- [FAQ](https://scrapfly.io/docs/scrape-api/faq)

#### Crawler API

- [Getting Started](https://scrapfly.io/docs/crawler-api/getting-started)
- [API Specification]()
- [Retrieving Results](https://scrapfly.io/docs/crawler-api/results)
- [WARC Format](https://scrapfly.io/docs/crawler-api/warc-format)
- [Data Extraction](https://scrapfly.io/docs/crawler-api/extraction-rules)
- [Search](https://scrapfly.io/docs/crawler-api/search)
- [Prompt & Extract](https://scrapfly.io/docs/crawler-api/prompt)
- [Auto Refresh](https://scrapfly.io/docs/crawler-api/refresh)
- [Webhook](https://scrapfly.io/docs/crawler-api/webhook)
- [Schedule](https://scrapfly.io/docs/crawler-api/schedule)
- [Billing](https://scrapfly.io/docs/crawler-api/billing)
- [Errors](https://scrapfly.io/docs/crawler-api/errors)
- [Troubleshoot](https://scrapfly.io/docs/crawler-api/troubleshoot)
- [FAQ](https://scrapfly.io/docs/crawler-api/faq)

#### Screenshot API

- [Getting Started](https://scrapfly.io/docs/screenshot-api/getting-started)
- [API Specification]()
- [Accessibility Testing](https://scrapfly.io/docs/screenshot-api/accessibility)
- [Webhook](https://scrapfly.io/docs/screenshot-api/webhook)
- [Schedule](https://scrapfly.io/docs/screenshot-api/schedule)
- [Billing](https://scrapfly.io/docs/screenshot-api/billing)
- [Errors](https://scrapfly.io/docs/screenshot-api/errors)

#### Extraction API

- [Getting Started](https://scrapfly.io/docs/extraction-api/getting-started)
- [API Specification]()
- [Rules Template](https://scrapfly.io/docs/extraction-api/rules-and-template)
- [LLM Extraction](https://scrapfly.io/docs/extraction-api/llm-prompt)
- [AI Auto Extraction](https://scrapfly.io/docs/extraction-api/automatic-ai)
- [Webhook](https://scrapfly.io/docs/extraction-api/webhook)
- [Billing](https://scrapfly.io/docs/extraction-api/billing)
- [Errors](https://scrapfly.io/docs/extraction-api/errors)
- [FAQ](https://scrapfly.io/docs/extraction-api/faq)

#### Data API


#### Proxy Saver

- [Getting Started](https://scrapfly.io/docs/proxy-saver/getting-started)
- [Fingerprints](https://scrapfly.io/docs/proxy-saver/fingerprints)
- [Optimizations](https://scrapfly.io/docs/proxy-saver/optimizations)
- [SSL Certificates](https://scrapfly.io/docs/proxy-saver/certificates)
- [Protocols](https://scrapfly.io/docs/proxy-saver/protocols)
- [Pacfile](https://scrapfly.io/docs/proxy-saver/pacfile)
- [Secure Credentials](https://scrapfly.io/docs/proxy-saver/security)
- [Billing](https://scrapfly.io/docs/proxy-saver/billing)

#### Cloud Browser API

- [Getting Started](https://scrapfly.io/docs/cloud-browser-api/getting-started)
- [Proxy & Geo-Targeting](https://scrapfly.io/docs/cloud-browser-api/proxy)
- [Unblock API](https://scrapfly.io/docs/cloud-browser-api/unblock)
- [Captcha Solver](https://scrapfly.io/docs/cloud-browser-api/captcha-solver)
- [File Downloads](https://scrapfly.io/docs/cloud-browser-api/file-downloads)
- [Session Resume](https://scrapfly.io/docs/cloud-browser-api/session-resume)
- [Human-in-the-Loop](https://scrapfly.io/docs/cloud-browser-api/human-in-the-loop)
- [Debug Mode](https://scrapfly.io/docs/cloud-browser-api/debug-mode)
- [Browser Extensions](https://scrapfly.io/docs/cloud-browser-api/extensions)
- [Credential Vault](https://scrapfly.io/docs/cloud-browser-api/credential-vault)
- [Native Browser MCP](https://scrapfly.io/docs/cloud-browser-api/mcp)
- [DevTools Protocol](https://scrapfly.io/docs/cloud-browser-api/cdp-reference)
##### Integrations

- [Puppeteer](https://scrapfly.io/docs/cloud-browser-api/puppeteer)
- [Playwright](https://scrapfly.io/docs/cloud-browser-api/playwright)
- [Selenium](https://scrapfly.io/docs/cloud-browser-api/selenium)
- [Vercel Agent Browser](https://scrapfly.io/docs/cloud-browser-api/agent-browser)
- [Browser Use](https://scrapfly.io/docs/cloud-browser-api/browser-use)
- [Stagehand](https://scrapfly.io/docs/cloud-browser-api/stagehand)
- [1Password](https://scrapfly.io/docs/cloud-browser-api/1password)

- [Billing](https://scrapfly.io/docs/cloud-browser-api/billing)
- [Errors](https://scrapfly.io/docs/cloud-browser-api/errors)


### Tools

- [Antibot Detector](https://scrapfly.io/docs/tools/antibot-detector)

### SDK

- [Golang](https://scrapfly.io/docs/sdk/golang)
- [Python](https://scrapfly.io/docs/sdk/python)
- [Rust](https://scrapfly.io/docs/sdk/rust)
- [TypeScript](https://scrapfly.io/docs/sdk/typescript)
- [Scrapy](https://scrapfly.io/docs/sdk/scrapy)

### Integrations

- [Getting Started](https://scrapfly.io/docs/integration/getting-started)
- [LangChain](https://scrapfly.io/docs/integration/langchain)
- [LlamaIndex](https://scrapfly.io/docs/integration/llamaindex)
- [CrewAI](https://scrapfly.io/docs/integration/crewai)
- [Zapier](https://scrapfly.io/docs/integration/zapier)
- [Make](https://scrapfly.io/docs/integration/make)
- [n8n](https://scrapfly.io/docs/integration/n8n)

### Academy

- [Overview](https://scrapfly.io/academy)
- [Web Scraping Overview](https://scrapfly.io/academy/scraping-overview)
- [Tools](https://scrapfly.io/academy/tools-overview)
- [Reverse Engineering](https://scrapfly.io/academy/reverse-engineering)
- [Static Scraping](https://scrapfly.io/academy/static-scraping)
- [HTML Parsing](https://scrapfly.io/academy/html-parsing)
- [Dynamic Scraping](https://scrapfly.io/academy/dynamic-scraping)
- [Hidden API Scraping](https://scrapfly.io/academy/hidden-api-scraping)
- [Headless Browsers](https://scrapfly.io/academy/headless-browsers)
- [Hidden Web Data](https://scrapfly.io/academy/hidden-web-data)
- [JSON Parsing](https://scrapfly.io/academy/json-parsing)
- [Data Processing](https://scrapfly.io/academy/data-processing)
- [Scaling](https://scrapfly.io/academy/scaling)
- [Walkthrough Summary](https://scrapfly.io/academy/walkthrough-summary)
- [Scraper Blocking](https://scrapfly.io/academy/scraper-blocking)
- [Proxies](https://scrapfly.io/academy/proxies)

---

# Credential Vault Beta

 A vault holds passwords, passkeys, TOTP seeds, cookies, and opaque secrets that Scrapium hands to the **real Chromium password manager** the moment your Cloud Browser session opens. Same autofill, same origin matching, same WebAuthn flow your daily browser does, with the storage backend swapped for an end-to-end encrypted vault and the whole thing pilotable over CDP.

 You never have to carry a sensitive secret in your scripts again. Create a vault, fill it with the credentials your automation needs (by hand, or by [linking a 1Password vault](#link-1password) so items mirror in automatically), then give your teammates the vault id so they can run sessions against it. The plaintext stays sealed; everyone on the team uses the credential through the vault, never by copy-pasting it into code or a config file.

- **Native autofill.** Vaults seed Scrapium's built-in password manager. Forms fill the way they do in your daily browser, including WebAuthn / passkey flows.
- **Direct CDP.** A custom `PasswordManager` CDP domain is available on the same session if you want to drive credentials yourself. See [PasswordManager CDP domain](#cdp-domain).

 Every password and cookie in the vault is loaded into the browser at once, on your **first page**. Loading does not depend on which page is open; see [Injection timing and coverage](#injection-timing) below. What each credential's **origin** controls is what Chromium is willing to offer afterwards. A password registered for `https://web-scraping.dev` can autofill on that origin. Chromium will not suggest it anywhere else. Set the origin precisely. Too broad an origin (a bare apex when only one subdomain should receive the credential) can expose the credential to a page you didn't intend.

## Create a vault and fill it

 Easiest path: open the [Vault page](https://scrapfly.io/dashboard/cloud-browser/vault) in the dashboard, click **New Vault**, and save the key the modal reveals (shown only once). The same flows are available over the API.

 **The vault key cannot be recovered.** Scrapfly's servers never store it. If you lose it, there is no reset link and no support ticket that gets it back. Every item in that vault becomes permanently unreadable. Save it somewhere durable, a password manager of your own or a secrets store, the moment it is shown. If you still have a working session or the dashboard open with the key cached, rotate the key now and save the new one. See [Error codes](#error-codes) for what a lost or wrong key looks like on the wire.

Create a vault. The response carries the one-time `key`. The `name` must be **letters and digits only**, with no spaces, hyphens or dots, because it is what you pass as the `vault=` parameter on the Cloud Browser WebSocket URL. Anything else is rejected with a `400`. The item `label` below is free text.

 ```
curl -sS "https://browser.scrapfly.io/vault?key=$SCRAPFLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"productionlogins"}'
```

Add a password (`X-Vault-Key` seals the secret server-side then drops the key):

 ```
curl -sS "https://browser.scrapfly.io/vault/$VAULT_ID/item?key=$SCRAPFLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Vault-Key: $VAULT_KEY" \
  -d '{
    "type":"password",
    "label":"GitHub admin",
    "origin":"https://github.com/login",
    "username":"alice",
    "secret":{"password":"hunter2"}
  }'
```

 Every item has a `type` and a `secret` object shaped for that type. Five types exist:

 | Type | `secret` shape | What it's for |
|---|---|---|
| `password` | `{"password": "hunter2"}` | Autofilled on the item's `origin`, matched the way Chromium matches any saved password. |
| `passkey` | `{"credentialId", "privateKey", "userHandle", "signCount"}` (raw FIDO2 fields) | A WebAuthn credential you enrolled yourself with a virtual authenticator. |
| `cookie` | `{"name", "value", "domain", ...}` (mirrors CDP `Network.CookieParam`) | Resuming a session you captured by hand. |
| `totp` | `{"seed": "JBSWY3DPEHPK3PXP", "issuer": "...", "digits": 6, "period": 30}` | An RFC 6238 seed. Scrapium computes the live code itself; see [`getTotpCode`](#pm-getTotpCode). |
| `blob` | `{"data": "...", "content_type": "application/json"}` | An opaque payload: a service-account JSON key, an access-key pair, an API token. Not injected into the browser. Your automation reads it back as-is. This is the type [RPA BYOS storage destinations](https://scrapfly.io/docs/rpa/storage) use to hold their credentials. |

 `secret` also accepts a typed sub-object, for example `{"secret": {"totp": {"secret": "...", ...}}}`, as an alternative to the flat shape above. Both are equivalent. `password`, `passkey`, and `cookie` are injected into the browser's real Chrome password store, WebAuthn store, and cookie jar respectively. `totp` is held in a session-scoped seed store Scrapium computes codes from. `blob` is never injected anywhere. See [Injection timing and coverage](#injection-timing).

## Link a vault to 1Password

 Instead of entering items by hand, a vault can mirror them from a 1Password vault. Scrapfly reads items with a 1Password service-account token you provide, converts what it can into vault items, and keeps them in sync. Only 1Password is supported today. See the [1Password Integration](https://scrapfly.io/docs/cloud-browser-api/1password) page for the service-account prerequisites, the end-to-end security model, and per-language session code samples.

### Connecting from the dashboard

 Open a vault's [detail page](https://scrapfly.io/dashboard/cloud-browser/vault) and click **Connect 1Password**.

1. **Paste a service-account token.** Create one in 1Password with read access to the vault you want to mirror. The wizard tests it and lists the 1Password vaults it can see.
2. **Pick the upstream vault.** Choose which 1Password vault to mirror from.
3. **Set optional filters and sync mode.** A title filter (glob) and a tag filter narrow which items come across. Choose `on_session` or `manual`; see [Sync modes and refresh timing](#link-sync-modes) below.
4. **Connect.** The wizard runs the first sync immediately, whichever sync mode you picked.

 The vault's card then shows the linked provider, the item count, and buttons for **Sync now**, **Test connection**, **Rotate token**, and **Unlink**.

### What gets mirrored

 Only four 1Password categories map to vault items. Everything else is skipped and counted, never dropped silently: the sync report names each skip and its reason.

 | 1Password category | Becomes |
|---|---|
| Login, Password | One `password` item per website on the entry (up to 10), plus one `totp` item per one-time-password field on it (up to 5). |
| API Credentials | One `blob` item holding the credential field. |
| Secure Note | One `blob` item holding the note body. |
| Everything else (SSH keys, documents, credit cards, and so on) | Skipped, reported as `category_unsupported`. |

 **Passkeys never mirror.** 1Password does not expose passkey material to a service account, whatever access it's granted, so a passkey field on a Login item is always skipped.

### Labels

 A mirrored item's label is generated, not chosen: `<slug>-<tag>-<fingerprint>`, where the slug comes from the 1Password item's title and the tag is `pw`, `otp`, `note`, or `api`. If that label collides with one already on a manual item in the same vault, the mirrored candidate is skipped, not renamed, and the manual item wins. If you plan to reference an item by label from an agent or a workflow, give your manually entered items names that won't collide with a mirrored one.

### Sync modes and refresh timing

 | Mode | When it syncs |
|---|---|
| `on_session` (default) | Refreshes right before a Cloud Browser session opens against this vault, if the sync TTL has elapsed since the last sync. The TTL defaults to 15 minutes and can be set between 1 minute and 24 hours. |
| `manual` | Never syncs on its own. Only **Sync now** in the dashboard, or the sync REST endpoint, refreshes it. |

 A sync that fails with an auth, rate-limit, or scope error backs off for an hour before Scrapfly tries again on its own, so a dead token doesn't retry on every session. **Sync now** bypasses that backoff, since clicking it is the retry.

 A mirrored item cannot be edited or deleted directly. A `PATCH` or `DELETE` against one is refused with `409 Conflict`; edit the source in 1Password and let it re-sync, or unlink first.

### Limits

 | Limit | Value |
|---|---|
| Items considered per sync pass | 1000 |
| Mirrored items per vault | 200 |
| Websites per Login/Password item | 10 |
| TOTP fields per Login/Password item | 5 |
| Size per blob | 64 KiB |
| Total blob bytes per sync pass | 4 MiB |
| Label length | 64 characters |
| Origin length | 255 characters |

 An item that would exceed the mirrored-item cap or the blob-byte cap is skipped for that pass and reported, the same as an unsupported category. It is picked up on a later sync once room frees up.

### Rotating, testing, and unlinking

- **Test connection** checks the stored token against 1Password without changing anything.
- **Rotate token** replaces the stored service-account token, for example after 1Password expires or revokes the old one. It requires the vault key, since the new token is sealed under it.
- **Unlink** removes the link. The **keep mirrored items as manual credentials** checkbox is on by default: leave it checked and the mirrored rows stay in the vault as ordinary manual items you can edit again. Uncheck it and every mirrored row is deleted along with the link. Either way, the sealed service-account token is deleted; it never lingers after an unlink.

## Using a vault in a session

 Pass `vault` and `vault_key` on the Cloud Browser WebSocket URL alongside your usual session parameters. `vault` is the vault's **name**, not its id. Scrapfly resolves it, pushes every credential into the browser, then drops the key. Any CDP client works: Playwright, Puppeteer, Selenium CDP, or a raw WebSocket.

Raw WebSocket URL the session opens against:

 ```
wss://browser.scrapfly.io/?key=$SCRAPFLY_API_KEY&vault=$VAULT_NAME&vault_key=$VAULT_KEY&proxy_pool=public_residential_pool
```

 Scrapfly's own request logs redact `vault_key` before they're written. That covers Scrapfly's side. It does not cover yours. The key still sits in plain sight in the connect URL itself: your shell history, your own HTTP client's debug logging, a proxy in front of your script, or your browser's network tab if you build the URL client-side can all see it. The [dashboard](https://scrapfly.io/dashboard/cloud-browser/vault) also caches the key you type into it in that browser's `localStorage`, scoped to the vault, so you aren't asked for it again on every action in the same browser. Handle the URL and that browser profile with the same care as the key itself. Don't paste either into a shared log or ticket.

### Injection timing and coverage

 Credentials go in when your script attaches to its **first page** target, not when the session opens. If your automation drives the browser without ever attaching to a page, talking only to the `Target` domain for example, nothing is ever injected and forms stay empty. Once that first attach happens, every password and cookie the vault holds is pushed in that one pass. There is no per-navigation re-injection and no re-push if you open additional pages afterward.

- **Passwords and cookies** land in Chromium's real password store and cookie jar, so they behave exactly like a credential you saved by hand: origin matching, autofill, the works.
- **Passkeys** are wired through the standard CDP `WebAuthn` domain (`addVirtualAuthenticator` + `addCredential`), not through [`PasswordManager.registerPasskey`](#pm-registerPasskey). That command exists for your own runtime seeding, but the vault's own passkey items never go through it.
- **TOTP** seeds load into a session-scoped store; call [`getTotpCode`](#pm-getTotpCode) to read the current code.
- **Blobs are never injected.** There is nothing in the browser to fill; read a blob item back over the [REST API](#api-reference) and use the plaintext in your own code.

### From the CLI, an agent, or a workflow

 A vault isn't limited to a hand-rolled WebSocket URL. The [Scrapfly CLI](https://scrapfly.io/docs/cli/commands)'s `scrapfly browser` command takes the same two parameters as `--vault NAME` and `--vault-key KEY`. RPA [agents](https://scrapfly.io/docs/rpa/agents) and [workflows](https://scrapfly.io/docs/rpa/workflows) reference vault items by label instead of pulling the plaintext into a prompt or a script variable; see [Vaults in RPA](https://scrapfly.io/docs/rpa/vault) for that binding syntax.

## Leak prevention

 A vault credential reaches the login form. And stops there. Everything that crosses out of the browser is bulleted before it leaves: screenshots, video recordings, the live operator view, AX trees that agent frameworks read, DOM dumps, and every CDP response your script can ask for. You write automation the way you always would, and the plaintext stays in the form.

 The protection is built into Scrapium itself, not bolted on as a filter you can forget to enable. It is the default the moment a session opens against a vault.

### Screenshots, video, and the live view

 Once a credential is filled, the field renders as bullets in every visual surface: `Page.captureScreenshot`, video recordings, and the live VNC stream your operators or end users watch. The yellow autofill highlight that normally hints "this field was just filled" is suppressed at the same time, so a screenshot of a vault-filled form looks identical to one the user typed into.

 The page cannot tell that the redaction is happening. Nothing on the element is restyled and no CSS property changes, so probes like `getComputedStyle` see the field exactly as the site authored it.

### Accessibility tree (agent and RPA reads)

 Agent frameworks navigate Cloud Browser sessions by calling `Accessibility.getFullAXTree`. The `value` of any vault-filled control comes back as the literal string `<redacted>`, so an LLM driving the page never sees the plaintext, never quotes it back in tool output, and never stores it in conversation history.

### DOM dumps and derived nodes

 `DOM.getOuterHTML` and the attribute serializer emit a bullet run in place of a vault-filled field's `value`. The same applies if page JavaScript copies the credential into another node. A hidden `<input>`, a status banner, even `document.title`. The serializer tracks the plaintext itself, not just the original element, so derived copies are scrubbed too.

### `Runtime.evaluate` and the CDP catch-all

 The straightforward way to fish a value out of a page is `Runtime.evaluate` with `document.querySelector('input').value`. The result comes back as bullets. Every CDP frame the session sends is scanned for vault plaintext on its way out of the browser, so the same protection covers `DOMSnapshot.captureSnapshot` strings tables, `Network.getResponseBody` when a request echoed the credential back, debugger pause-frame variables, and any other CDP method that would otherwise surface the plaintext.

 You do not have to remember which CDP commands are safe and which are not. The wire is sealed by default.

### What a vault does not do

 A vault narrows the blast radius of a credential; it does not replace trust in the destination. The form itself receives the real plaintext (login would otherwise fail), and the page that owns that form can read its own input field in its own world. The same property every password manager, including your daily browser, has by design.

 The mitigation is simple: set each credential's `origin` narrowly so Chromium only ever offers it on hosts you actually intend to log in to. Treat it the way you would treat a "save password" prompt. The vault is exactly as safe as the URL you bind it to.

### Turning the protection off

 Calling [`PasswordManager.disable`](#pm-disable) does not just quiet the event stream. It tears down every mechanism this section describes for the rest of the session: it removes the passkey authenticator (dropping any passkeys the vault registered), zeroes and drops the TOTP seed store, clears every redaction mask already applied on every frame, and drops the CDP redact list that scrubs plaintext out of screenshots, DOM dumps, AX trees, and `Runtime.evaluate` results. Passwords already in Chromium's password store are not deleted and can still autofill, but nothing filled from that point on is protected from leaking into a CDP response anymore, and nothing filled before the call is either. Don't call `disable` on a session you still intend to use a vault credential on. See [`PasswordManager.disable`](#pm-disable) for the full symbol reference.

## Worked example: password autofill on `web-scraping.dev`

 This example creates a vault with the Python SDK, writes one login into it, opens a Cloud Browser session bound to that vault, and lets Chromium fill and submit the sign-in form. The password crosses your process once, on its way into the vault, and the script proves the login worked from what the server sends back rather than from what the page displays.

 The target, [web-scraping.dev/password-manager-test](https://web-scraping.dev/password-manager-test), is a Scrapfly-owned page with a real login form. It accepts the username `user123` and the password `password`. On a valid login its backend sets the cookie `pm_test_session` and the page marks the document with `data-pm-login="ok"`. A wrong or empty form gets a `403` and the marker `err`.

Dependencies:

 ```
pip install scrapfly-sdk==0.12.0 playwright
playwright install chromium
```

Save this as `vault_login_example.py`:

 ```
"""Worked example: password autofill on web-scraping.dev.

Stores one login in a Cloud Browser credential vault, opens a browser session
bound to that vault, and lets Chromium fill the sign-in form. The password
reaches the browser through the vault only: after step 2 this script never
holds it, never types it and never reads it back.

Run:
    pip install scrapfly-sdk==0.12.0 playwright && playwright install chromium
    SCRAPFLY_API_KEY=... python3 vault_login_example.py
"""

import os
import secrets
import time
from urllib.parse import urlsplit

from playwright.sync_api import sync_playwright
from scrapfly import BrowserConfig, ScrapflyClient

API_KEY = os.environ["SCRAPFLY_API_KEY"]
LOGIN_URL = "https://web-scraping.dev/password-manager-test"
ORIGIN = "{0.scheme}://{0.netloc}".format(urlsplit(LOGIN_URL))

USERNAME = "user123"
PASSWORD = "password"  # web-scraping.dev publishes this pair; it is written to the vault once, below

client = ScrapflyClient(key=API_KEY)

# Vault names are letters and digits only and must be unique per project.
vault_name = "demo" + secrets.token_hex(4)
created = client.cloud_browser_vault_create(
    name=vault_name,
    description="web-scraping.dev worked example",
)
vault_id = created["vault"]["id"]
vault_key = created["key"]  # shown once, never recoverable: store it, never log it
print("1. vault %s created (id %s)" % (vault_name, vault_id))

try:
    item = client.cloud_browser_vault_item_create(
        vault_id=vault_id,
        vault_key=vault_key,
        type="password",
        label="web-scraping.dev demo login",
        origin=LOGIN_URL,
        username=USERNAME,
        secret={"password": PASSWORD},
    )
    print("2. password item %s stored for %s" % (item["item"]["id"], item["item"]["origin"]))

    session_url = client.cloud_browser(
        BrowserConfig(vault=vault_name, vault_key=vault_key, target_url=LOGIN_URL)
    )
    print("3. session URL built for vault %s" % vault_name)

    with sync_playwright() as playwright:
        browser = playwright.chromium.connect_over_cdp(session_url)
        print("4. connected to %s" % browser.version)
        context = browser.contexts[0]
        page = context.pages[0] if context.pages else context.new_page()
        cdp = context.new_cdp_session(page)

        # A vault-backed session starts in human-pick mode so a person watching
        # the live preview chooses the account. A script has nobody to click the
        # dropdown, so switch the session to programmatic filling. enable
        # replaces the session's mode, it does not add to it.
        prompts = []
        cdp.on("PasswordManager.credentialsRequested", lambda event: prompts.append(event))
        cdp.send("PasswordManager.enable", {"nativeUi": False})

        page.goto(LOGIN_URL, wait_until="load", timeout=60000)
        print("5. opened %s" % page.url)

        # The vault is injected when the script attaches to its first page, so
        # the login form can be on screen before the credential lands. Wait.
        deadline = time.time() + 15
        stored = []
        while time.time() < deadline:
            stored = cdp.send("PasswordManager.listCredentials", {"originFilter": ORIGIN}).get("credentials") or []
            if stored:
                break
            time.sleep(0.25)
        if not stored:
            raise SystemExit("the vault credential never reached the browser")
        print("6. browser holds %s for %s" % (stored[0]["username"], stored[0]["origin"]))

        # Nothing below types the credential. Some browser builds have already
        # filled both fields by the time the page is loaded; others fill only
        # when the script picks an account. Clicking the password box covers
        # both: it is what raises credentialsRequested, and it leaves that box
        # focused, which is where prefillChoice types.
        page.click("#pm-password-input")
        deadline = time.time() + 15
        filled = False
        answered = False
        last_error = ""
        while time.time() < deadline:
            filled = page.evaluate(
                "() => document.querySelector('#pm-password-input').value.length > 0"
                " && document.querySelector('#pm-username-input').value.length > 0"
            )
            if filled:
                break
            if prompts and not answered:
                # Answer the newest request once. Every keystroke of the fill
                # raises another one; replying twice would fill twice.
                reply = cdp.send(
                    "PasswordManager.prefillChoice",
                    {"requestId": prompts.pop()["requestId"], "username": USERNAME},
                )
                answered = bool(reply.get("success"))
                last_error = reply.get("errorMessage") or ""
            time.sleep(0.25)
        if not filled:
            raise SystemExit("the form was not filled: " + (last_error or "no fill and no request"))
        print("7. form filled by the browser, username on the page is %r"
              % page.input_value("#pm-username-input"))

        page.click("#pm-login-submit")
        page.wait_for_selector('body[data-pm-login="ok"]', timeout=20000)
        status = page.text_content("#pm-login-status")
        cookies = {c["name"]: c["value"] for c in context.cookies()}
        assert status.strip().endswith("username=" + USERNAME), status
        assert cookies.get("pm_test_session") == "pm-test-session-token", cookies
        print("8. server accepted the login: %r, session cookie %r"
              % (status.strip(), cookies["pm_test_session"]))

        browser.close()
finally:
    client.cloud_browser_vault_delete(vault_id=vault_id)
    remaining = [v["name"] for v in client.cloud_browser_vault_list().get("vaults") or []]
    print("9. vault deleted, %s still listed: %s" % (vault_name, vault_name in remaining))
```

Run it with your own API key:

 ```
SCRAPFLY_API_KEY= python3 vault_login_example.py
```

A successful run prints:

 ```
1. vault demo2625b414 created (id 01M32C8AZX5ZZRG6JBFS3E30HJ)
2. password item 01M32C8B0B77M1DC3W5WX0JMWP stored for https://web-scraping.dev/password-manager-test
3. session URL built for vault demo2625b414
4. connected to 153.0.8010.47
5. opened https://web-scraping.dev/password-manager-test
6. browser holds user123 for https://web-scraping.dev/
7. form filled by the browser, username on the page is 'user123'
8. server accepted the login: 'login ok — username=user123', session cookie 'pm-test-session-token'
9. vault deleted, demo2625b414 still listed: False
```

### What each step does

1. **Create the vault.** The `name` is letters and digits only and has to be free in your project, so the script appends random hex to `demo`. The one-time key comes back at the top level of the response, beside `vault` and not inside it. Scrapfly keeps no copy of it, so store it the moment it is returned. The script holds it in a variable and never prints it.
2. **Write the password.** The key travels in the `X-Vault-Key` header, which the SDK method sets for you. What comes back is the item's id, type, label, origin and username, and no part of the secret.
3. **Build the session URL.** `client.cloud_browser()` returns the WebSocket URL with `vault` and `vault_key` already percent-encoded. That encoding matters: a base64 key contains `+`, and a hand-built URL that leaves it raw hands the server a space instead. `vault` carries the vault **name**, never its id. The example also passes `target_url` so the session knows where it is going before the first navigation.
4. **Open the session.** Playwright connects over CDP. The script takes the page the session already has, opens one if there is none, and attaches a CDP session to it. That attach is what triggers the vault injection, so it has to happen whatever the session started with.
5. **Switch the session to programmatic filling.** A vault-backed session starts with Chrome's native account picker, which is the right mode when a person is watching the live view and can pick an account on it. A script cannot, so it sends [`PasswordManager.enable`](#pm-enable) with `nativeUi: false`. That call *replaces* the session's mode rather than adding to it, so it decides how the fill happens for the rest of the session. Leave it out and the picker stands: nothing fills, and [`listCredentials`](#pm-listCredentials) fails with `PasswordManager not enabled`, because the domain is enabled per CDP session, and yours has not.
6. **Wait for the credential to land.** The vault is pushed into the browser when your script attaches to its first page, so the form can be on screen before the credential is there. `listCredentials` with an `originFilter` is the check that it arrived. The script gives it 15 seconds and then stops with an error instead of falling back to typing the password itself. The origin it reports back is the origin root, `https://web-scraping.dev/`, because Chromium matches saved passwords per origin: an item stored against a path is offered on every page of that host.
7. **Let the browser fill.** The script clicks the password box and from there only reads the length of the two fields. It never calls `fill`, `type` or the keyboard. Chromium fills the form out of its own password store, the same way it would for a password you saved by hand. *When* it fills depends on the browser build, which is why the loop covers two cases. In the run above both fields were already filled by the time the page had loaded, and no event arrived. A build that does not fill on load waits to be asked: focusing the password box raises [`credentialsRequested`](#pm-credentialsRequested) with the stored account in `choices`, and the fill happens when the script answers with [`prefillChoice`](#pm-prefillChoice). Answer once. The fill types the credential in keystroke by keystroke, each keystroke raises a fresh request, and a script that replies to every one of them fills the form twice.
8. **Assert on the server, not on the page.** Three facts, all produced by the backend rather than by the DOM: the `data-pm-login="ok"` marker, set only once the login endpoint has returned success; the status text, which ends with the username the server echoed in its JSON; and the cookie `pm_test_session`, which has to equal the token the server issued. An empty or wrong form gets a `403`, the `err` marker and no cookie, so this assertion cannot pass on a form that never filled.
9. **Delete the vault.** The delete sits in a `finally`, so a failed run leaves nothing behind either. The delete response carries only a message, so the script confirms against a fresh vault list instead of trusting the reply.

### What the run shows

- **The password reaches Chromium without passing through the automation.** Everything after step 2 names the vault, never the secret. In real use you write the item once, from the [Vault page](https://scrapfly.io/dashboard/cloud-browser/vault) or a one-off script, and the job that runs every day carries only the vault name and key. The write lives in the same file here because the target publishes its own test credentials.
- **The script cannot read the secret back.** No call in it returns the plaintext: the create response is metadata, and the two form fields are inspected by length rather than by value. What a session does to a script that reads the field value instead is covered under [`Runtime.evaluate` and the CDP catch-all](#leak-cdp).
- **The fill is the browser's.** The script produces two clicks, one on the password box and one on the submit button, and not a single character. Everything in the two fields at submit time came from the password store the vault seeded.
- **One login on one origin.** The run covers a `password` item against a site with a real password input. Passkeys, TOTP and cookies ride the same injection pass but are exercised through other commands; see [Injection timing and coverage](#injection-timing). A page whose password box is not a real `input type=password` cannot autofill at all, because Chromium never builds a password form for it.

 The dashboard's [Visual API Player](https://scrapfly.io/dashboard/playground/cloud-browser) exposes the same parameters as form fields so you can try a vault end to end before wiring it into your code.

## PasswordManager CDP Domain

 The vault is exposed to your script through a custom Scrapium CDP domain called `PasswordManager`. It is not part of stock Chrome DevTools Protocol; pointing a vanilla Chromium remote-debugging session at these commands returns `Method not found`. You do not need to call it to use a vault Scrapfly seeds the browser for you when you pass `vault` and `vault_key` on the WebSocket URL. Reach for it when you want finer control than write-side seeding.

 | Symbol | Type | Description |
|---|---|---|
| `<a href="#pm-enable">PasswordManager.enable</a>` | Command | Subscribe the session to credential events. Required before any `credentialsRequested`, `passkeyRequested`, or `passkeyRegistrationRequested` event fires. Calling it yourself changes fill behaviour, see the symbol reference. |
| `<a href="#pm-disable">PasswordManager.disable</a>` | Command | Tears down passkeys, TOTP seeds, and every leak-prevention mechanism for the rest of the session, not just the event stream. See [Turning the protection off](#leak-disable). |
| `<a href="#pm-credentialsRequested">PasswordManager.credentialsRequested</a>` | Event | A credential form was detected, or `navigator.credentials.get()` was called. Reply with `prefillChoice`, `provideCredentials`, or `declineRequest`. |
| `<a href="#pm-passkeyRequested">PasswordManager.passkeyRequested</a>` | Event | A WebAuthn assertion is in flight. Reply with `providePasskey` to satisfy the call. |
| `<a href="#pm-passkeyRegistrationRequested">PasswordManager.passkeyRegistrationRequested</a>` | Event | `navigator.credentials.create()` is running. Mint and persist the credential, then reply with `providePasskey`. |
| `<a href="#pm-prefillChoice">PasswordManager.prefillChoice</a>` | Command | Pre-select which stored credential to offer when the form is detected. |
| `<a href="#pm-provideCredentials">PasswordManager.provideCredentials</a>` | Command | Satisfy an in-flight `credentialsRequested` event with username + password. |
| `<a href="#pm-providePasskey">PasswordManager.providePasskey</a>` | Command | Satisfy a passkey assertion or registration with a FIDO2 credential. |
| `<a href="#pm-declineRequest">PasswordManager.declineRequest</a>` | Command | Refuse the in-flight request and let the page fall through to its native behaviour. |
| `<a href="#pm-fillCredentials">PasswordManager.fillCredentials</a>` | Command | Ad-hoc placement against CSS selectors when origin-based autofill doesn't apply. |
| `<a href="#pm-registerCredential">PasswordManager.registerCredential</a>` | Command | Runtime seeding. Add a password to the live session. Your script handles plaintext, which is the property the vault was built to remove. |
| `<a href="#pm-listCredentials">PasswordManager.listCredentials</a>` | Command | List all passwords currently held in the session's password store. |
| `<a href="#pm-deleteCredential">PasswordManager.deleteCredential</a>` | Command | Remove a password from the session's password store. |
| `<a href="#pm-registerPasskey">PasswordManager.registerPasskey</a>` | Command | Runtime seeding. Add a FIDO2 passkey to the live session. |
| `<a href="#pm-listPasskeys">PasswordManager.listPasskeys</a>` | Command | List all passkeys currently held in the session's WebAuthn store. |
| `<a href="#pm-deletePasskey">PasswordManager.deletePasskey</a>` | Command | Remove a passkey from the session's WebAuthn store. |
| `<a href="#pm-registerCookie">PasswordManager.registerCookie</a>` | Command | Runtime seeding. Add a cookie to the live session. |
| `<a href="#pm-listCookies">PasswordManager.listCookies</a>` | Command | List all vault-managed cookies currently injected on the session. |
| `<a href="#pm-deleteCookie">PasswordManager.deleteCookie</a>` | Command | Remove a vault-managed cookie from the live session. |
| `<a href="#pm-registerTotp">PasswordManager.registerTotp</a>` | Command | Runtime seeding. Add a TOTP (RFC 6238) secret to the live session. |
| `<a href="#pm-listTotps">PasswordManager.listTotps</a>` | Command | List all TOTP seeds currently held in the session. |
| `<a href="#pm-deleteTotp">PasswordManager.deleteTotp</a>` | Command | Remove a TOTP seed from the live session. |
| `<a href="#pm-getTotpCode">PasswordManager.getTotpCode</a>` | Command | Compute the current code (6-10 digits, per the seed's `digits`) for a stored TOTP seed. |

### Symbol reference

#### `PasswordManager.enable` Command

 Subscribe the session to credential events. After `enable` Scrapium emits [`credentialsRequested`](#pm-credentialsRequested), [`passkeyRequested`](#pm-passkeyRequested), and [`passkeyRegistrationRequested`](#pm-passkeyRegistrationRequested) as the page interacts with credentials. Takes one optional parameter, `nativeUi` (boolean, default `false`).

 Scrapfly's own vault seeding calls `enable` with `nativeUi: true` for you, so a session you leave alone behaves like your daily browser: Chrome's native dropdown renders and fills on account select. That mode expects a person on the live view to pick the account, which is why the [worked example](#worked-password) sends `enable` with `nativeUi: false` before it drives the form.

 **Calling `enable` yourself is not additive.** It replaces that state for the session. With `nativeUi` left at its default of `false`, Scrapium suppresses the native dropdown and emits `credentialsRequested` for your script to answer with [`provideCredentials`](#pm-provideCredentials) or [`prefillChoice`](#pm-prefillChoice).

 `nativeUi: true` is not a click-to-fill mode. Chrome renders its own dropdown and fills only once an account is picked on it, which takes a person on the live view. A script that clicks the password box and waits gets two empty inputs and a login the site rejects. So an automated session sends `enable` with `nativeUi: false`, and `nativeUi: true` stays for sessions a human drives.

#### `PasswordManager.disable` Command

 Ends the `PasswordManager` domain for this session. This is not a quiet unsubscribe. See [Turning the protection off](#leak-disable) for the full list of what it tears down: passkeys, TOTP seeds, every redaction mask already applied, and the CDP redact list itself. No parameters.

#### `PasswordManager.credentialsRequested` Event

 Fires when a credential form is detected on the active page, or when the page calls `navigator.credentials.get()`. Carries the matched origin and the candidate items Scrapium has for it. Reply with [`prefillChoice`](#pm-prefillChoice), [`provideCredentials`](#pm-provideCredentials), or [`declineRequest`](#pm-declineRequest) to drive the flow.

#### `PasswordManager.passkeyRequested` Event

 Fires for an in-flight WebAuthn assertion. Carries the relying-party id, the challenge, and the user verification mode. Reply with [`providePasskey`](#pm-providePasskey) to satisfy the call.

#### `PasswordManager.passkeyRegistrationRequested` Event

 Fires when `navigator.credentials.create()` runs. Mint and persist the credential out-of-band (or via [`registerPasskey`](#pm-registerPasskey)), then reply with [`providePasskey`](#pm-providePasskey).

#### `PasswordManager.prefillChoice` Command

 Pre-select which stored credential to offer when the form is detected. Useful when several items are bound to the same origin and you want deterministic selection instead of leaving it to Scrapium's default order.

#### `PasswordManager.provideCredentials` Command

 Satisfy an in-flight [`credentialsRequested`](#pm-credentialsRequested) event with username + password. Use this to inject a credential that did not pre-exist in the vault. For example one resolved at runtime from your own secret store.

#### `PasswordManager.providePasskey` Command

 Satisfy a passkey assertion or registration with a FIDO2 credential. Used to respond to [`passkeyRequested`](#pm-passkeyRequested) and [`passkeyRegistrationRequested`](#pm-passkeyRegistrationRequested).

#### `PasswordManager.declineRequest` Command

 Refuse the in-flight request. The page falls through to its native behaviour as if no password manager had intercepted the call (typically: the browser's own "no credentials available" path).

#### `PasswordManager.fillCredentials` Command

 Ad-hoc placement against CSS selectors. Use when the form does not look like what Chromium's heuristic autofill recognises and origin-based matching cannot pick the right fields on its own. `passwordSelector` is required. `usernameSelector` and `totpSelector` are optional. The command also takes a `formId` field reserved for a future driver-resolved form target. Passing one today returns an error, so stick to selectors.

#### `PasswordManager.registerCredential` Command

 Runtime seeding. Add a password to the live session. The plaintext is handled by your script, which is the property the vault was built to remove; prefer vault items where possible.

#### `PasswordManager.listCredentials` Command

 List all passwords currently held in the session's password store, including items seeded from the vault and items added at runtime. Returns metadata only; plaintext does not cross the CDP wire.

#### `PasswordManager.deleteCredential` Command

 Remove a password from the session's password store. The vault item is not touched.

#### `PasswordManager.registerPasskey` Command

 Runtime seeding. Add a FIDO2 passkey to the live session, typically as part of an enrollment flow you drive yourself.

#### `PasswordManager.listPasskeys` Command

 List all passkeys currently held in the session's WebAuthn store.

#### `PasswordManager.deletePasskey` Command

 Remove a passkey from the session's WebAuthn store. The vault item is not touched.

#### `PasswordManager.registerCookie` Command

 Runtime seeding. Add a cookie to the live session. Equivalent to placing a `cookie` vault item out-of-band; same origin-binding rules apply.

#### `PasswordManager.listCookies` Command

 List all vault-managed cookies currently injected on the session.

#### `PasswordManager.deleteCookie` Command

 Remove a vault-managed cookie from the live session.

#### `PasswordManager.registerTotp` Command

 Runtime seeding. Add a TOTP (RFC 6238) secret to the live session. Pair with [`getTotpCode`](#pm-getTotpCode) when the destination site asks for a one-time code.

#### `PasswordManager.listTotps` Command

 List all TOTP seeds currently held in the session. Returns metadata only.

#### `PasswordManager.deleteTotp` Command

 Remove a TOTP seed from the live session.

#### `PasswordManager.getTotpCode` Command

 Compute the current code (6-10 digits, per the seed's `digits` field; defaults to 6) for a stored TOTP seed. Use this to fill the second factor on sites that gate login behind a TOTP challenge.

## SDK support

 Two surfaces are involved when you wire a vault into your code:

1. **Use a vault on a session**. Pass `vault` and `vault_key` on the WebSocket URL. Any CDP client (Playwright, Puppeteer, Selenium CDP, raw WS) does this without help from a Scrapfly SDK; the URL builder in our SDKs forwards arbitrary query parameters.
2. **Manage vaults and items** (create, list, rotate key, add or remove credentials). These are REST endpoints on the Browser API (`/vault` and `/vault/{id}/item`) wrapped by typed methods in every Scrapfly SDK. Use the wrappers from your code; reach for curl or a raw HTTP client only for languages outside the matrix below.

### Use a vault on a session

 This is just URL-parameter plumbing, so every SDK and every raw CDP client supports it today. The browser-automation libraries (Playwright, Puppeteer, Selenium) accept the WebSocket URL Scrapfly hands them and pass the query string through unmodified.

 | Client | Pass `vault` + `vault_key` on session URL |
|---|---|
| [Python SDK](https://scrapfly.io/docs/sdk/python) |  |
| [TypeScript SDK](https://scrapfly.io/docs/sdk/typescript) |  |
| [Go SDK](https://scrapfly.io/docs/sdk/golang) |  |
| [Rust SDK](https://scrapfly.io/docs/sdk/rust) |  |
| [Scrapy integration](https://scrapfly.io/docs/sdk/scrapy) |  |
| [Playwright](https://scrapfly.io/docs/cloud-browser-api/playwright) (Python &amp; Node) |  |
| [Puppeteer](https://scrapfly.io/docs/cloud-browser-api/puppeteer) |  |
| [Selenium CDP](https://scrapfly.io/docs/cloud-browser-api/selenium) |  |
| Raw WebSocket / any CDP client |  |

### Manage vaults and items

 Each row maps a typed SDK method to its [Browser API REST endpoint](#api-reference). Every cell ships today and is exercised against the live Browser API by our integration harness on every release. The raw endpoint also remains reachable from any HTTP client (curl, Postman, your own fetch wrapper) for languages or platforms not covered below.

 | Operation | REST endpoint | Python | TypeScript | Go | Rust |
|---|---|---|---|---|---|
| Create vault | `POST /vault` |  |  |  |  |
| List vaults | `GET /vault` |  |  |  |  |
| Get vault | `GET /vault/{id}` |  |  |  |  |
| Update vault | `PATCH /vault/{id}` |  |  |  |  |
| Delete vault | `DELETE /vault/{id}` |  |  |  |  |
| Rotate vault key | `POST /vault/{id}/rotate` |  |  |  |  |
| List items | `GET /vault/{id}/item` |  |  |  |  |
| Create item (any of the five types) | `POST /vault/{id}/item` |  |  |  |  |
| Update item (rename or rotate value) | `PATCH /vault/{id}/item/{item_id}` |  |  |  |  |
| Delete item | `DELETE /vault/{id}/item/{item_id}` |  |  |  |  |

 ships in the SDK today, validated end-to-end against the live Browser API in our SDK integration matrix. Scrapy reuses the Python SDK methods.

 A `PATCH` or `DELETE` against an item mirrored from [a linked 1Password vault](#link-1password) is refused with `409 Conflict`: the row belongs to the sync and your edit would be overwritten on the next pass anyway. Edit the source item in 1Password and let it re-sync, or [unlink the provider](#link-unlink) first.

 Method names follow the existing `cloud_browser_extension_*` convention: `cloud_browser_vault_create`, `cloud_browser_vault_list`, `cloud_browser_vault_item_create`, and so on (PascalCase in Go, camelCase in TypeScript, snake\_case in Rust). No SDK wraps the linked-service endpoints (`/vault/{id}/service*`). Managing a 1Password link is a dashboard or raw-REST operation only. See [Link a vault to 1Password](#link-1password).

### REST reference

 The full request and response shapes for every row in the matrix above are documented inline in [Create a vault and fill it](#create-and-fill) and the worked example. The endpoints all live under the Cloud Browser host (`https://browser.scrapfly.io`) and are authenticated with your `?key=$SCRAPFLY_API_KEY` on the query string. Body-bearing routes that touch encrypted material additionally require `X-Vault-Key`:

- `POST /vault/{id}/item` (sealing a fresh secret)
- `PATCH /vault/{id}/item/{item_id}` (only when the body carries a new `secret`. Metadata-only patches do not need the key)
- `POST /vault/{id}/rotate` (rewraps every item under a new key)
- `POST /vault/{id}/service` (linking a 1Password vault: the service-account token is sealed under this key)
- `PATCH /vault/{id}/service` (only when the body rotates the token. Changing the sync mode or filters alone does not need the key)
- `POST /vault/{id}/service/sync` (an operator-triggered sync opens the token to call 1Password)
- `POST /vault/{id}/service/test` (opens the token, or the one in the request body, to probe the connection)

 `DELETE /vault/{id}/service` (unlink) does not need the key: nothing is sealed or opened to remove a link. See [Link a vault to 1Password](#link-1password).

## Error codes

 The vault has six error codes. A session can only ever produce two of them. The other four come from the linked-service REST endpoints, never from a session.

### Session errors

 These two are checked at session allocation, before any browser slot is consumed, so a bad vault or key never costs you a session.

- [`ERR::BROWSER::VAULT_NOT_FOUND`](https://scrapfly.io/docs/cloud-browser-api/error/ERR::BROWSER::VAULT_NOT_FOUND): the vault id does not exist for this user, project, or environment. Most common cause: a `LIVE` vault used in a `TEST` session, or vice versa.
- [`ERR::BROWSER::VAULT_KEY_INVALID`](https://scrapfly.io/docs/cloud-browser-api/error/ERR::BROWSER::VAULT_KEY_INVALID): the `vault_key` is missing, malformed, or does not match the key the vault was sealed with. Double-check the value you saved at vault creation. If the key itself is gone, rotating won't help: the old key is required to rotate, and a vault with no working key stays permanently unreadable. There is no recovery path around that; see [Create a vault and fill it](#create-and-fill).

### Linked 1Password errors

 These four describe a linked 1Password vault falling out of sync. They come only from the `/vault/:id/service*` REST endpoints (link, update, sync, test), as an HTTP status on that request. A session never sees them: a provider outage or a dead token degrades the mirror to its last successful sync, it does not fail your browser allocation. See [Link a vault to 1Password](#link-1password).

- [`ERR::BROWSER::VAULT_PROVIDER_AUTH_FAILED`](https://scrapfly.io/docs/cloud-browser-api/error/ERR::BROWSER::VAULT_PROVIDER_AUTH_FAILED) (401): 1Password rejected the stored service-account token. Rotate the token on the vault.
- [`ERR::BROWSER::VAULT_PROVIDER_RATE_LIMITED`](https://scrapfly.io/docs/cloud-browser-api/error/ERR::BROWSER::VAULT_PROVIDER_RATE_LIMITED) (429): 1Password rate-limited the service account. Its daily request budget is shared by every integration using the same token.
- [`ERR::BROWSER::VAULT_PROVIDER_UNAVAILABLE`](https://scrapfly.io/docs/cloud-browser-api/error/ERR::BROWSER::VAULT_PROVIDER_UNAVAILABLE) (503): 1Password could not be reached. The vault keeps serving its last successful mirror.
- [`ERR::BROWSER::VAULT_PROVIDER_SCOPE`](https://scrapfly.io/docs/cloud-browser-api/error/ERR::BROWSER::VAULT_PROVIDER_SCOPE) (403): the service-account token cannot see the requested 1Password vault. Personal, Private, and Employee vaults are never reachable by a service account, whatever permissions it has.
