Automate logins with the Credentials API

Use the Steel Credentials API with Playwright to automate flows with stored credentials.

examples/credentials-ts
Contributors: , Updated
Terminal

Scaffolds a starter project locally. Requires the Steel CLI.

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.

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

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

Then opt the session in:

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:

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

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. 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:

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 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.

auth-context (cookie and localStorage replay) · Playwright docs

examples/credentials-py
Contributors: Updated
Terminal

Scaffolds a starter project locally. Requires the Steel CLI.

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:

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

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

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

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. 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:

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 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.

credentials-ts (TypeScript port of this recipe) · credentials-go · credentials-rs · auth-context-py (cookie and localStorage replay) · Playwright docs

examples/credentials-rs
Contributors: Updated
Terminal

Scaffolds a starter project locally. Requires the Steel CLI.

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 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:

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:

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

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

Get a key at 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:

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.
examples/credentials-go
Contributors: Updated
Terminal

Scaffolds a starter project locally. Requires the Steel CLI.

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:

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:

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

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. The session viewer URL prints as the run starts. Open it in another tab to watch the auto-fill land.

Output looks like this:

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.

credentials-ts (TypeScript) · credentials-py (Python) · credentials-rs (Rust) · auth-context-go (cookie and localStorage replay) · chromedp docs