> ## Documentation Index
> Fetch the complete documentation index at: https://docs.notte.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# Passwords, 2FA Codes and API Keys

> Swap passwords, one-time codes, API keys and tokens through a secret proxy, not only card numbers

> [!TIP]
>
> Read [the Notte skill](https://notte.cc/skill.md) and follow its setup instructions when setting up Notte or building browser automation. This is the shared source for CLI setup, authentication, and the browser-to-code workflow.

A [secret proxy](https://docs.notte.cc/features/sessions/secret-proxies/overview.md) is not limited to payment cards. It can swap any stand-in that the page sends to a listed host in an HTTP or HTTPS request: a username or password on a login form, a 2FA code, an API key or a bearer token. The agent, the page, screenshots, recordings and network logs only contain the stand-ins, provided your proxy also [masks real values in responses](#real-values-in-responses).

Notte only routes the traffic. Which values are swapped, and where in the request, depends on your secret proxy.

## What you can swap

| Secret | Where it usually appears in the request | What to put in `hosts` |
| - | - | - |
| Card number, CVV | Form or JSON body, often sent from a payment iframe | The payment provider's API or iframe host, for example `api.stripe.com` |
| Username, email, password | Form or JSON body of the login request | The host the login form posts to. This is often a separate auth domain (`auth.example.com`, `accounts.example.com`) or your identity provider's domain, not the host in the address bar. |
| 2FA one-time code | Form or JSON body of the verification request | The host that receives the code, often the same auth host. See [2FA codes](#2fa-codes). |
| Recovery or backup code | Form or JSON body | The host that receives the code |
| API key | A header (`Authorization`, `X-API-Key`), the query string, or a form or JSON body when it is typed into a portal | The API or portal host the page sends it to |
| Bearer or session token | The `Authorization` or `Cookie` header, or a JSON body | The host that receives it. The browser must hold the stand-in, for example in a [cookie](https://docs.notte.cc/features/sessions/cookies.md) you set. |

To cover every row, your secret proxy must replace stand-ins in request headers, the query string, form bodies and JSON bodies, including JSON strings written with `\u` escapes. See the [Evervault](https://docs.notte.cc/features/sessions/secret-proxies/evervault.md), [VGS](https://docs.notte.cc/features/sessions/secret-proxies/vgs.md) and [custom proxy](https://docs.notte.cc/features/sessions/secret-proxies/custom-proxy.md) pages for what each option supports.

Each stand-in must also pass the page's own checks before the form is sent: length, allowed characters, or a Luhn check for card numbers.

## Log in with stand-ins

1. Get stand-ins for the username, password and 2FA code from your provider or your own proxy.
2. Start a session whose `hosts` lists the host that receives the login request.
3. Fill the stand-ins and submit the form.
4. Your secret proxy swaps them on the way to that host. The site receives the real values.

```python login_session.py theme={null}
from pathlib import Path

from notte_sdk import NotteClient
from notte_sdk.types import NotteProxy, ProxySettings, SecretProxy

client = NotteClient()

# Stand-ins from your provider or your own proxy. They are not secrets:
# the page, the agent, screenshots and network logs only contain these values.
USERNAME = "tok_user_7Hq2Xc"
PASSWORD = "tok_pwd_9Kd3Wm"
OTP = "tok_otp_4Lm8Rv"

proxies: list[ProxySettings] = [
    NotteProxy.from_country("us"),
    SecretProxy(
        server="https://secret-proxy.example.com:443",
        username="YOUR_PROXY_USERNAME",
        password="YOUR_PROXY_PASSWORD",
        ca_certificate=Path("your-ca.pem").read_text(),
        # The host that receives the login and 2FA requests, not the app's host
        hosts=["auth.example.com"],
    ),
]

with client.Session(proxies=proxies) as session:
    session.execute(type="goto", url="https://app.example.com/login")

    # The form posts to auth.example.com, where your proxy swaps the stand-ins
    session.execute(type="fill", selector="input[name='username']", value=USERNAME)
    session.execute(type="fill", selector="input[name='password']", value=PASSWORD)
    session.execute(type="click", selector="button[type='submit']")

    # 2FA step: the same stand-in every time; your proxy sends the current code
    session.execute(type="fill", selector="input[name='otp']", value=OTP)
    session.execute(type="click", selector="button[type='submit']")
```

```typescript login_session.ts theme={null}
import { readFileSync } from 'node:fs';
import { NotteClient, type NotteProxy, type SecretProxy } from 'notte-sdk';

const client = new NotteClient();

// Stand-ins from your provider or your own proxy. They are not secrets:
// the page, the agent, screenshots and network logs only contain these values.
const USERNAME = 'tok_user_7Hq2Xc';
const PASSWORD = 'tok_pwd_9Kd3Wm';
const OTP = 'tok_otp_4Lm8Rv';

const exit: NotteProxy = { type: 'notte', country: 'us' };

const secret: SecretProxy = {
  type: 'secret',
  server: 'https://secret-proxy.example.com:443',
  username: 'YOUR_PROXY_USERNAME',
  password: 'YOUR_PROXY_PASSWORD',
  ca_certificate: readFileSync('your-ca.pem', 'utf8'),
  // The host that receives the login and 2FA requests, not the app's host
  hosts: ['auth.example.com'],
};

await client.Session({ proxies: [exit, secret] }).use(async (session) => {
  await session.execute({ type: 'goto', url: 'https://app.example.com/login' });

  // The form posts to auth.example.com, where your proxy swaps the stand-ins
  await session.execute({ type: 'fill', selector: "input[name='username']", value: USERNAME });
  await session.execute({ type: 'fill', selector: "input[name='password']", value: PASSWORD });
  await session.execute({ type: 'click', selector: "button[type='submit']" });

  // 2FA step: the same stand-in every time; your proxy sends the current code
  await session.execute({ type: 'fill', selector: "input[name='otp']", value: OTP });
  await session.execute({ type: 'click', selector: "button[type='submit']" });
});
```

An agent can fill the stand-ins too. They are not secrets, so they can go in the agent's task.

## 2FA codes

A TOTP code changes every interval, usually 30 seconds, so a stand-in cannot map to one fixed real value. Have your secret proxy compute the code when the request passes through:

1. Store the TOTP seed in your proxy's vault, next to a stable stand-in such as `tok_otp_4Lm8Rv`.
2. The agent types that stand-in into the code field, at every login.
3. On the request to the listed host, your proxy looks up the seed, computes the current code as defined in [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238), with the interval, algorithm and number of digits the site set up for that account, and sends it in place of the stand-in.

The seed never leaves your systems, and the same stand-in works for every login.

* **Two logins in one interval.** Both get the same code, and many sites accept a code only once. After a login, wait for the next interval before the next one.

* **Fields that only accept digits.** If the code field only accepts six digits, the stand-in must be six digits too. A short number can also appear elsewhere in the request, so swap it only in the code field, selected by its form field name or JSON path.

* **Codes sent by SMS or email.** The real code arrives on a phone number or inbox you control, so your proxy cannot compute it. It would have to fetch the code at request time, which depends on your setup.

* **Recovery and backup codes** are fixed values. Treat them like a password.

* **Provider support.** Evervault's [Browser Tokens docs](https://docs.evervault.com/browser-tokens) list passwords, recovery codes and one-time backup codes, which are stored values, and do not describe computing a value at request time. VGS's public docs do not describe it either. For TOTP codes, use this pattern with a [custom proxy](https://docs.notte.cc/features/sessions/secret-proxies/custom-proxy.md), or use a [Notte vault](#secret-proxy-or-vault).

## Limits

### List the host that receives the request

Swaps only happen on requests to the hosts you list. A login page often sends the password to another host than the one in the address bar: an auth domain, a regional API host or an identity provider. A request to an unlisted host carries the stand-in unchanged, and the login fails.

To find the host, open the login page in a browser with developer tools, sign in with a test account and read the host of the request that carries the password. Do the same for the 2FA step. Remember that `*.example.com` does not match `example.com`.

Your secret proxy decrypts all traffic to the listed hosts, so list only the hosts that receive secrets or send them back. A host you do not list returns its responses to the browser unchanged, so if a site shows a real value on another host, for example the account email on `app.example.com`, list that host too and mask its responses.

### Plain HTTP

On a plain HTTP request, your secret proxy sends the real value to the site without encryption. `hosts` matches host names, not schemes, so a listed host the page also reaches over plain HTTP sends those requests through your proxy too. Configure your proxy to refuse plain HTTP requests or to leave their stand-ins unchanged. Evervault's browser proxy only accepts HTTPS.

### Values the page encrypts before sending

Some sites encrypt or hash the password or card number with JavaScript in the page before sending it. The request then carries a value computed from the stand-in, not the stand-in itself, so no proxy can swap it, and the site rejects the login or payment. Test each site with a stand-in before you rely on it. For these sites, a [Notte vault](#secret-proxy-or-vault) fills the real value in the page and can work where a secret proxy cannot.

### Channels other than HTTP requests

This page covers values sent in HTTP requests: headers, query strings, form bodies and JSON bodies. Whether values sent in WebSocket messages are swapped depends on your proxy. Evervault's and VGS's public docs do not cover it, so do not rely on it without testing.

### Real values in responses

Many sites send account details back: the username or email on the next page, an API key shown once in a portal, a token in a JSON response. Notte does not change responses, so configure your secret proxy to replace real values with stand-ins in responses from the listed hosts. See [Mask real values in responses](https://docs.notte.cc/features/sessions/secret-proxies/overview.md#mask-real-values-in-responses).

After a login, the site also issues its own session cookie or token. That value is real and lives in the browser, as in any session, unless your proxy replaces it with a stand-in in responses and swaps it back on requests.

## Secret proxy or vault

[Notte vaults](https://docs.notte.cc/concepts/vaults.md) also fill credentials without the agent seeing them. They work differently:

| | Secret proxy | Notte vault |
| - | - | - |
| Where the real value lives | Only in your secret proxy or tokenization provider | Encrypted in Notte |
| What reaches the browser | Stand-ins. The real value is added on the way to the listed hosts. | The real value, filled in the page during `fill` and `form_fill` actions, outside the model context |
| Setup | A secret proxy, a CA certificate and the list of hosts | A vault attached to the session, and placeholders such as `PASSWORD` and `MFA` in fill actions |
| 2FA codes | Your proxy computes them, as above | Supported through the vault's two-factor credentials |
| Pages that encrypt the password | Cannot swap the value | Works, because the page encrypts the real value |

Use a secret proxy when real values must never reach Notte, for example to keep Notte out of your compliance scope, or when the values already live with your tokenization provider. Use a vault when Notte holding encrypted credentials is acceptable.

You can also fill logins from [1Password](https://docs.notte.cc/integrations/onepassword.md) or [Bitwarden](https://docs.notte.cc/integrations/bitwarden.md). In both, the real value reaches the browser, as with a Notte vault.

## Next Steps

> [card] [Secret Proxies](https://docs.notte.cc/features/sessions/secret-proxies/overview.md)
>
> Request fields, rules and response masking

> [card] [Custom proxy](https://docs.notte.cc/features/sessions/secret-proxies/custom-proxy.md)
>
> Run your own secret proxy

> [card] [Secret Vaults](https://docs.notte.cc/concepts/vaults.md)
>
> Let Notte store and fill credentials

> [card] [Cookies](https://docs.notte.cc/features/sessions/cookies.md)
>
> Set cookies on a session


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.