> Full docs index: https://docs.steel.dev/llms.txt

# Automate logins with the Credentials API
URL: https://docs.steel.dev/cookbook/credentials


**TypeScript**

Steel's credentials vault stores usernames and passwords against an origin. When a session opts in, Steel watches for login forms on that origin and fills them for you. No login code in your automation, no plaintext passwords in env vars, no custom storage for cookies. It's one of Steel's [authentication](/cookbook/topics/authentication) primitives.

Two API calls wire it up. First, save the credential once:

```typescript
await client.credentials.create({
  origin: "https://demo.testfire.net",
  value: { username: "admin", password: "admin" },
});
```

Then opt the session in:

```typescript
session = await client.sessions.create({
  credentials: {},
});
```

That empty object is the opt-in. Without it, the vault exists but the session ignores it. With it, Steel matches the page's origin against stored credentials and types them in when a login form appears.

After that, drive the browser with Playwright as usual. The demo navigates to the Altoro Mutual test site, clicks `#AccountLink` to open the login form, and checks the heading to confirm the fill worked:

```typescript
await page.goto("https://demo.testfire.net", { waitUntil: "networkidle" });
await page.click("#AccountLink");
await setTimeout(2000);

const headingText = await page.textContent("h1");
if (headingText?.trim() === "Hello Admin User") {
  console.log("Success, you are logged in");
}
```

The `setTimeout(2000)` gives Steel room to fill and submit the form. In a real script you would swap that for `page.waitForURL` or a selector wait tied to a post-login element.

Credentials are per-origin. Create one per site you automate. Re-calling `credentials.create` for an origin that already has a credential throws `Credential already exists`, which the demo swallows so the script is idempotent.

## Run it

```bash
cd examples/credentials-ts
cp .env.example .env          # set STEEL_API_KEY
npm install
npm start
```

Get a key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys). The script prints a session viewer URL as it starts. Open it in another tab to watch the auto-fill happen.

Your output varies. Structure looks like this:

```text
Creating credential...
Creating Steel session...
Steel Session created!
View session at https://app.steel.dev/sessions/ab12cd34...

Connected to browser via Playwright
Success, you are logged in
Releasing session...
Session released
Done!
```

On a second run the credential already exists, so you see `Credential already exists, moving on.` before the session starts. The behavior is otherwise identical.

## Make it yours

- **Swap the target site.** Change the `origin` and `value` in `credentials.create`, then update `page.goto` and the login-trigger click in `index.ts`. Steel handles the form detection as long as the page exposes a standard username/password input pair.
- **Manage credentials out of band.** `client.credentials.list()`, `client.credentials.retrieve(id)`, and `client.credentials.delete(id)` let you rotate or audit stored creds without touching automation code. Create credentials from a setup script and keep `index.ts` focused on the workflow.
- **Combine with stealth.** Pass `useProxy`, `solveCaptcha`, or `sessionTimeout` alongside `credentials: {}` in `sessions.create()`. The vault works with every other session option.

## When to use this vs. auth-context

Both recipes persist login across runs. They solve it differently:

- **Credentials (this recipe)** stores username and password. Steel re-authenticates each session by filling the login form. Works for any site with a standard form; the login UI runs every time.
- **[auth-context](/cookbook/auth-context)** captures cookies and localStorage from an already-authenticated session and replays them into the next one. Skips the login form entirely, but the context expires when the site's session does and needs to be recaptured.

Reach for credentials when you want a stable, long-lived setup tied to an account. Reach for auth-context when the site uses flows the vault cannot drive (SSO, MFA prompts, magic links) and you only need the resulting cookies.

## Related

[auth-context](/cookbook/auth-context) (cookie and localStorage replay) · [Playwright docs](https://playwright.dev)

**Python**

Look at `main.py` and notice what is missing: there is no `page.fill("#username", ...)`, no password typed into a selector, no submit click. The automation navigates to the login page and reads the result. Steel handles the form in between. The credential lives in Steel's vault, the session opts into it, and when a matching login form renders, Steel types the username and password for you. Your script never sees the password after it is stored.

Setup is two calls. Store the credential against an origin once:

```python
client.credentials.create(
    origin="https://demo.testfire.net",
    value={"username": "admin", "password": "admin"},
)
```

Then opt the session in by passing an empty `credentials` dict:

```python
session = client.sessions.create(
    credentials={},
)
```

The empty dict is the switch. Leave it off and the vault still holds the credential, but the session ignores it. Pass it and Steel matches each page's origin against what is stored and fills the form when one appears.

## The two-second wait

After clicking `#AccountLink` the script calls `time.sleep(2)`. That is deliberate slack: the click opens the login form, Steel detects it, fills the fields, and submits. The sleep gives that round trip room before the script reads the `h1` to confirm `"Hello Admin User"`. It is the blunt version. In a real workflow swap it for `page.wait_for_url(...)` or `page.wait_for_selector(...)` keyed to something that only exists once you are logged in, so you wait exactly as long as you need to.

Credentials are scoped per origin, so create one per site. Calling `credentials.create` again for an origin that already has one raises a `steel.APIError` whose message contains `Credential already exists`. The script catches that case and keeps going, which is why a second run behaves the same as the first.

## Run it

```bash
cd examples/credentials-py
cp .env.example .env          # set STEEL_API_KEY
uv run main.py
```

Grab a key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys). `uv sync` runs automatically on first `uv run`, so there is no separate install step. The script prints a session viewer URL on startup. Open it in another tab to watch the auto-fill happen live. If you prefer pip, `pip install -e .` then `python main.py` works too.

Your output varies. Structure looks like this:

```text
Steel + Credentials Starter
============================================================

Creating credential...
Creating Steel session...
Steel Session created!
View session at https://app.steel.dev/sessions/ab12cd34...
Connected to browser via Playwright
Success, you are logged in
Releasing session...
Session released
Done!
```

On a second run the credential already exists, so you see `Credential already exists, moving on.` after the `Creating credential...` line. Everything else is identical.

## Make it yours

- **Swap the target site.** Change `origin` and `value` in `credentials.create`, then update the `page.goto` URL and the login-trigger click in `main.py`. Steel detects the form as long as the page uses a standard username and password input pair.
- **Manage credentials separately.** `client.credentials.list()`, `client.credentials.update(...)`, and `client.credentials.delete(...)` let you rotate or audit stored logins without touching the automation. Seed credentials from a one-off setup script and keep `main.py` about the workflow.
- **Stack it with other session options.** `use_proxy`, `solve_captcha`, and `session_timeout` slot in next to `credentials={}` in `sessions.create()`. The vault coexists with every other knob.

## When to use this vs. auth-context

Both persist a login across runs, by different means. Credentials stores a username and password, and Steel re-authenticates by filling the login form on every session. It works for any site with a standard form, but the login UI runs each time. [auth-context-py](/cookbook/auth-context) instead captures cookies and localStorage from an already-authenticated session and replays them, skipping the form entirely, though that context expires when the site's session does. Reach for credentials when you want a stable, long-lived setup tied to an account; reach for auth-context when the site uses flows the vault cannot drive (SSO, MFA, magic links) and you only need the resulting cookies.

## Related

[credentials-ts](/cookbook/credentials) (TypeScript port of this recipe) · [credentials-go](/cookbook/credentials) · [credentials-rs](/cookbook/credentials) · [auth-context-py](/cookbook/auth-context) (cookie and localStorage replay) · [Playwright docs](https://playwright.dev/python/)

**Rust**

Steel's credentials vault stores a username and password against an origin. Opt a session in, and Steel watches for the login form on that origin and types the stored values for you. The automation never sees the password, holds no cookies, and contains no login code, just navigation and a check that the fill landed.

This recipe wires it up with two SDK calls, then connects [chromiumoxide](https://github.com/mattsse/chromiumoxide) over CDP to drive the resulting page.

## How it fits together

`main` stores the credential once with `client.credentials().create(...)`. Credentials are per-origin, so a re-run hits "Credential already exists"; the recipe matches that text on the returned `steel::Error` and continues, which keeps the script idempotent:

```rust
match create {
    Ok(_) => println!("Credential stored"),
    Err(err) if err.to_string().contains("already exists") => {
        println!("Credential already exists, moving on");
    }
    Err(err) => return Err(err.into()),
}
```

The opt-in is a default `SessionCreateParamsCredentials` on session create. Present, it tells Steel to match the page origin against the vault and fill the form when one appears; absent, the vault is ignored:

```rust
client.sessions().create(SessionCreateParams {
    credentials: Some(Box::new(SessionCreateParamsCredentials::default())),
    ..Default::default()
}).await?
```

From there it is ordinary chromiumoxide: open the Altoro Mutual login page and poll the username field (`#uid`) until Steel injects the vaulted value, which is the proof the fill landed. With the default `auto_submit` Steel also submits the form, so the filled field is only briefly visible — polling catches it as soon as it appears. The test site currently serves an expired certificate, so the page first sends `SetIgnoreCertificateErrorsParams`; drop that for a site with a valid certificate.

## Run it

```bash
cd examples/credentials-rs
cp .env.example .env          # set STEEL_API_KEY
cargo run
```

Get a key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys). The run prints a session viewer URL up front. Open it in another tab to watch Steel auto-fill the form live.

Output looks like this:

```text
Storing credential for https://demo.testfire.net...
Credential stored
Creating Steel session with credentials enabled...
Session live at https://app.steel.dev/sessions/ab12cd34...
Opening the login page; Steel auto-fills it from the vault...
Success: Steel auto-filled the login form with "admin" from the vault, no credentials in this code.
Releasing session...
Session released
```

On a second run the first lines read `Credential already exists, moving on`; the rest is identical.

## Make it yours

- **Swap the target site.** Change `ORIGIN` and the `value` map in `credentials().create`, then point the navigation and the polled field selector at your site. Steel handles detection as long as the page exposes a standard username/password input pair.
- **Tune the fill.** `SessionCreateParamsCredentials` carries `auto_submit`, `blur_fields`, and `exact_origin`. Set them on the struct instead of taking the default to control whether Steel presses submit, blurs filled fields, or matches the origin exactly.
- **Manage credentials out of band.** `credentials().list`, `update`, and `delete` let a setup script rotate or audit stored creds while `main.rs` stays focused on the workflow.

## Related

- [credentials-ts](/cookbook/credentials), [credentials-py](/cookbook/credentials), [credentials-go](/cookbook/credentials): the same recipe in TypeScript, Python, and Go.
- [auth-context-rs](/cookbook/auth-context): replay captured cookies and localStorage instead of refilling a login form.
- [chromiumoxide](https://github.com/mattsse/chromiumoxide): the async Rust CDP client used here.

**Go**

The automation in `main.go` never types a username or a password. It opens the site's login page and confirms that Steel auto-filled the form from the vault. The login itself happens server-side: Steel keeps the credential in a vault, watches the page for a matching form, and fills it. Your chromedp code stays a plain navigation script.

Wiring it up is two API calls. Store the credential against an origin:

```go
client.Credentials.Create(ctx, steel.CredentialCreateParams{
	Origin: steel.F("https://demo.testfire.net"),
	Value:  steel.F(map[string]string{"username": "admin", "password": "admin"}),
})
```

Then opt the session into the vault with an empty config struct:

```go
client.Sessions.Create(ctx, steel.SessionCreateParams{
	Credentials: steel.F(steel.SessionCreateParamsCredentials{}),
})
```

`SessionCreateParamsCredentials{}` is the opt-in. Leave it off and the vault is ignored for that session. The zero value uses the defaults; its fields (`AutoSubmit`, `BlurFields`, `ExactOrigin`) tune whether Steel presses submit for you, masks the typed values, and matches the origin exactly.

## Confirming the fill

After navigating to `/login.jsp`, the script polls the username field (`#uid`) until Steel injects the vaulted value, then reports success. With the default `AutoSubmit`, Steel also presses submit, so the filled field is only briefly visible — polling catches it as soon as it lands. The demo site currently serves an expired certificate, so the run first sends `Security.setIgnoreCertificateErrors`; drop that for a site with a valid certificate.

Re-running `Credentials.Create` for an origin that already has a stored credential returns an error whose message contains `already exists`. The script checks for that string and continues, so repeat runs are idempotent.

## Run it

```bash
cd examples/credentials-go
cp .env.example .env          # set STEEL_API_KEY
go mod tidy
go run .
```

Get a key at [app.steel.dev/settings/api-keys](https://app.steel.dev/settings/api-keys). The session viewer URL prints as the run starts. Open it in another tab to watch the auto-fill land.

Output looks like this:

```text
Storing credential...
Credential stored.
Creating Steel session with credentials enabled...
Session created. Watch it live at https://app.steel.dev/sessions/ab12cd34...
Opening the login page; Steel auto-fills it from the vault...
Success: Steel auto-filled the login form with "admin" from the vault, no credentials in this code.
Releasing session...
```

On a second run the credential is already in the vault, so the first lines read `Credential already exists, moving on.` and the rest is identical.

## Make it yours

- **Target another site.** Change `origin` and the `Value` map, then point the navigation and the polled field selector at the new login form. Steel handles detection for any standard username/password form.
- **Tune the fill.** Set `AutoSubmit`, `BlurFields`, or `ExactOrigin` on `SessionCreateParamsCredentials` to control submit behavior, value masking, and origin matching.
- **Manage creds out of band.** `Credentials.List`, `Credentials.Update`, and `Credentials.Delete` let a setup script rotate or audit stored values while `main.go` stays focused on the workflow.

## Related

[credentials-ts](/cookbook/credentials) (TypeScript) · [credentials-py](/cookbook/credentials) (Python) · [credentials-rs](/cookbook/credentials) (Rust) · [auth-context-go](/cookbook/auth-context) (cookie and localStorage replay) · [chromedp docs](https://github.com/chromedp/chromedp)

## Related recipes

- [Persist authenticated sessions with Profiles](/cookbook/profiles): Maintain authenticated sessions across Steel browser instances using profiles.
- [Reuse authenticated sessions across browsers](/cookbook/auth-context): Maintain authenticated sessions across Steel browser instances by capturing and reusing cookies and local storage.
- [Scrape a page to Markdown, screenshot, and PDF](/cookbook/scrape): Use the Steel TypeScript SDK's direct API to scrape a page to clean Markdown for LLM context, plus screenshot and PDF, with no browser library.
