# Automate logins with the Credentials API
URL: /cookbook/credentials

---
title: Automate logins with the Credentials API
description: Use the Steel Credentials API with Playwright to automate flows with stored credentials.
---

<RecipeJsonLd slug="credentials" title={"Automate logins with the Credentials API"} description={"Use the Steel Credentials API with Playwright to automate flows with stored credentials."} authors={[{"handle":"danew","name":"Dane Wilson"}, {"handle":"junhsss","name":"Jun Ryu"}]} datePublished="2024-11-19" dateModified="2026-04-24" sourceUrl="https://github.com/steel-dev/steel-cookbook/tree/92f29742253e2b6c6801d109e18232768e5291a0/examples/credentials" />

<RecipeMeta href="https://github.com/steel-dev/steel-cookbook/tree/92f29742253e2b6c6801d109e18232768e5291a0/examples/credentials" path="examples/credentials" authors={[{"handle":"danew","name":"Dane Wilson","avatar":"https://github.com/danew.png?size=40"}, {"handle":"junhsss","name":"Jun Ryu","avatar":"https://github.com/junhsss.png?size=40"}]} updated="2026-04-24" />

<RecipeQuickstart slug="credentials" />

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:

```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
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)

## Related recipes

<RecipeGrid>
<RecipeCard slug="profiles" title={"Persist authenticated sessions with Profiles"} description={"Maintain authenticated sessions across Steel browser instances using profiles."} topics={['Steel APIs', 'Authentication']} date="2025-10-13" />
<RecipeCard slug="auth-context" title={"Reuse authenticated sessions across browsers"} description={"Maintain authenticated sessions across Steel browser instances by capturing and reusing cookies and local storage."} topics={['Steel APIs', 'Authentication']} date="2025-03-11" />
<RecipeCard slug="extensions" title={"Upload and run browser extensions"} description={"Use the Steel Extensions API with Playwright to upload and run browser extensions."} topics={['Steel APIs', 'Playwright']} date="2024-11-19" />
</RecipeGrid>
