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

# Credentials, logins and 2FA

> Store passwords, credit cards and secrets, use them in agents and from code, connect your own vault, and let Skyvern complete two-factor authentication with authenticator apps, email, SMS and magic links.

Save a login once and every agent can use it. Skyvern fills the username, password and 2FA code into the page for you.

The model that drives the browser gets placeholders, not your values. Skyvern swaps in the real value only when it types into the field. A typed value can still show up in later screenshots and page text, so turn on **Mask Secrets** in an agent's settings to hide it there too. Saved passwords, card numbers and secrets are never shown again in the app or returned by the API.

## Store a credential

> [tab] In the app
>
> **1. Open Credentials**
>
> Click **Credentials** in the left sidebar. Its sub-items open the **Passwords**, **Credit Cards**, **Secrets**, **2FA** and **Magic Links** tabs.
>
> ![Credentials in the sidebar with Passwords, Credit Cards, Secrets, 2FA and Magic Links](https://mintcdn.com/skyvern/amcBKP0IBwdNswdN/images/cloud/credentials-sidebar-nav.png?fit=max\&auto=format\&n=amcBKP0IBwdNswdN\&q=85\&s=ab1e304dcd2da5c0dcaefabff569c9f9)
>
> **2. Click Add**
>
> Choose **Password**, **Credit Card** or **Secret**.
>
> ![Add menu with Password, Credit Card and Secret](https://mintcdn.com/skyvern/amcBKP0IBwdNswdN/images/cloud/credentials-add-dropdown.png?fit=max\&auto=format\&n=amcBKP0IBwdNswdN\&q=85\&s=846ece6c7332a79e2670d5ff4c18366b)
>
> **3. Fill in the form and click Save**
>
> The credential is available to agents right away.
>
> ![Credentials page with Folders and the Passwords, Credit Cards, Secrets, 2FA and Magic Links tabs](https://mintcdn.com/skyvern/amcBKP0IBwdNswdN/images/cloud/credentials-overview.png?fit=max\&auto=format\&n=amcBKP0IBwdNswdN\&q=85\&s=9b84ce657a48a8b8a81f9a5d48585a72)
>
> **Password** fields:
>
> * **Name**: a label you will recognize, such as "Vendor portal".
> * **Login Page URL**: where the sign-in form lives.
> * **Username or Email** and **Password**.
> * **Save browser session for future logins**: Skyvern signs in once, checks it worked, and saves the signed-in browser so later runs skip the login form. Needs the **Login Page URL**. You can add **Login instructions (optional)**, such as "Click the SSO button first".
> * **Two-Factor Authentication**: see [Two-factor authentication](#two-factor-authentication).
> * **Use a consistent IP address**: signs in from the same residential IP each time, which avoids extra 2FA prompts, captchas and lockouts caused by a new location.
>
> ![Add Credential dialog for a password](https://mintcdn.com/skyvern/amcBKP0IBwdNswdN/images/cloud/credentials-add-password.png?fit=max\&auto=format\&n=amcBKP0IBwdNswdN\&q=85\&s=f2efdba1eab96a2a9f2e47be4deae94b)
>
> **Credit Card** fields: **Name**, **Cardholder Name**, **Number**, **Brand**, **Expiration** (`MM/YY`) and **CVV**. **Billing Details** (address, **Billing Email**, **Billing Phone**) are optional; fill them in when checkout pages ask for a billing address. After you save, only the last four digits are shown.
>
> ![Add Credential dialog for a credit card, with card fields and Billing Details](https://mintcdn.com/skyvern/amcBKP0IBwdNswdN/images/cloud/credentials-add-credit-card.png?fit=max\&auto=format\&n=amcBKP0IBwdNswdN\&q=85\&s=0563a944a6fc19f16d412fd7026eca7a)
>
> **Secret** fields: **Name**, **Secret Label (optional)** and **Secret Value**. Use secrets for API keys and tokens.
>
> Group credentials with **+ New folder**. Click a folder card to filter the list.
>
> To edit a credential, click the pencil icon on its row. Saved values are never shown again. The name, username and 2FA method stay filled in; leave the password blank to keep it, and re-enter the other secret values, such as the authenticator key or card number. The edit dialog also has **Run workflows sequentially for this credential**, which lets only one run use the account at a time.
>
> Click the trash icon to delete a credential. Deletion is permanent, and agents that use it fail until you pick another one.

> [tab] With code
>
> ```python Python theme={null}
> import asyncio
> import os
>
> from skyvern import Skyvern
>
>
> async def main():
>     client = Skyvern(api_key=os.environ["SKYVERN_API_KEY"])
>     credential = await client.create_credential(
>         name="Vendor portal",
>         credential_type="password",
>         credential={
>             "username": "you@example.com",
>             "password": "your-password",
>         },
>     )
>     print(credential.credential_id)  # cred_...
>
>
> asyncio.run(main())
> ```
>
> ```typescript TypeScript theme={null}
> import { Skyvern } from "@skyvern/client";
>
> const client = new Skyvern({ apiKey: process.env.SKYVERN_API_KEY! });
>
> const credential = await client.createCredential({
>   name: "Vendor portal",
>   credential_type: "password",
>   credential: {
>     username: "you@example.com",
>     password: "your-password",
>   },
> });
> console.log(credential.credential_id); // cred_...
> ```
>
> ```bash cURL theme={null}
> curl -X POST "https://api.skyvern.com/v1/credentials" \
>   -H "x-api-key: $SKYVERN_API_KEY" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "name": "Vendor portal",
>     "credential_type": "password",
>     "credential": {"username": "you@example.com", "password": "your-password"}
>   }'
> ```
>
> The `credential` object depends on `credential_type`:
>
> | `credential_type` | Fields in `credential` |
> | - | - |
> | `password` | `username`, `password`, optional `totp` (authenticator key) and `totp_type` (`authenticator`, `email`, `text`) |
> | `credit_card` | `card_holder_name`, `card_number`, `card_exp_month`, `card_exp_year`, `card_cvv`, `card_brand` (free text, such as `visa`) |
> | `secret` | `secret_value`, optional `secret_label` |
>
> List credentials with `get_credentials()` / `getCredentials()` and delete one with `delete_credential(credential_id)` / `deleteCredential(credentialId)`. Reads return names and metadata only, never the stored values. Full parameters are in the [API reference](https://skyvern.com/docs/api-reference) and [SDK reference](https://skyvern.com/docs/sdk-reference/overview.md).

## Use a credential in an agent

> [tab] In the app
>
> * **Login block**: in [Studio](https://skyvern.com/docs/cloud/studio.md), add a Login block and pick a saved login in its **Credential** field. Skyvern enters the username, password and 2FA code.
> * **Credential input**: to choose the account per run, open **Inputs**, click **Add Input** and set the value type to **credential**. Pick the credential type (**Password**, **Secret** or **Credit Card**) and the source (**Skyvern**, or a [connected vault](#use-your-own-vault)). Whoever runs the agent picks the credential from a dropdown.
> * **Credit cards** need no field mapping. A Browser Task or Browser Action block on a checkout page fills the card number, name, expiration and CVV from the credential input.
> * **Secrets** go where a block takes text, such as an HTTP Request or Code block. Reference the input's key: `{{ my_secret.secret_value }}`.
>
> See the [blocks reference](https://skyvern.com/docs/reference/blocks.md) for every Login block setting.

> [tab] With code
>
> Pass a credential ID as the value of an agent's credential input:
>
> ```python Python theme={null}
> run = await client.run_workflow(
>     workflow_id="wpid_123",
>     parameters={"login": "cred_123"},
>     wait_for_completion=True,
> )
> ```
>
> ```typescript TypeScript theme={null}
> const run = await client.runWorkflow({
>   agent_id: "wpid_123",
>   parameters: { login: "cred_123" },
>   waitForCompletion: true,
> });
> ```
>
> Or sign in without an agent. `login` opens the page, signs in and handles 2FA:
>
> ```python Python theme={null}
> run = await client.login(
>     credential_type="skyvern",
>     url="https://portal.example.com/login",
>     credential_id="cred_123",
>     prompt="After signing in, open the dashboard",
>     wait_for_completion=True,
> )
> ```
>
> ```typescript TypeScript theme={null}
> const run = await client.login({
>   credential_type: "skyvern",
>   url: "https://portal.example.com/login",
>   credential_id: "cred_123",
>   prompt: "After signing in, open the dashboard",
>   waitForCompletion: true,
> });
> ```
>
> ```bash cURL theme={null}
> curl -X POST "https://api.skyvern.com/v1/run/tasks/login" \
>   -H "x-api-key: $SKYVERN_API_KEY" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "credential_type": "skyvern",
>     "url": "https://portal.example.com/login",
>     "credential_id": "cred_123"
>   }'
> ```
>
> `credential_type` says where the login lives:
>
> | Value | Pass |
> | - | - |
> | `skyvern` | `credential_id` |
> | `bitwarden` | `bitwarden_collection_id`, `bitwarden_item_id` |
> | `1password` | `onepassword_vault_id`, `onepassword_item_id` |
> | `azure_vault` | `azure_vault_name`, `azure_vault_username_key`, `azure_vault_password_key`, optional `azure_vault_totp_secret_key` |
>
> To continue in the signed-in browser, pass `browser_session_id` (see [Browsers](https://skyvern.com/docs/browsers.md)). Coding agents connected through [MCP](https://skyvern.com/docs/developers/getting-started/mcp.md) sign in with the `skyvern_login` tool, so passwords never pass through the chat.

## Use your own vault

If your team already keeps logins in a vault, connect it and Skyvern reads items at run time. On [Integrations](https://skyvern.com/docs/cloud/integrations.md), connect one of the **Credentials** tiles:

| Vault | What you connect with | Credential types |
| - | - | - |
| **1Password** | A **Service Account Token** with access to the vault | Password, Credit Card |
| **Bitwarden** | Your Bitwarden account **Email** and **Master Password** | Password, Secret, Credit Card |
| **Azure Key Vault** | **Tenant ID**, **Client ID**, **Client Secret** | Password |
| **Custom Credential Service** | **API Base URL**, **API Token** of your own HTTP service | Password, Secret, Credit Card |

Then add a credential input and choose the vault as its source. The input asks for the item to use: a Bitwarden collection ID and item ID (or a URL parameter key to match by site), a 1Password vault ID and item ID, or an Azure vault name with the names of the secrets that hold the username, password and optional authenticator key. Items from connected 1Password and Bitwarden vaults also show on the **Passwords** tab.

For Bitwarden, **Email** and **Master Password** are the default. Add a **Personal API key** if Bitwarden asks Skyvern for a verification code (a new-device check or two-step login). Skyvern cannot enter these codes. Without a key, the run stops with a message that explains this.

In the Bitwarden web app, open **Settings > Security > Keys > View API key**. Copy `client_id` (starts with `user.`) and `client_secret` into Skyvern's Bitwarden settings. Organization API keys do not work.

The key plus your master password open the vault without a code or two-step check. Store and share the key like your master password.

To rotate the key, select **Rotate API key** in Bitwarden, then save the new secret in Skyvern. Saving replaces the whole credential, so enter your master password again too. Saving Bitwarden settings with the `skyvern_bitwarden_config_set` MCP tool also replaces the whole credential and removes any saved API key.

> [!NOTE]
>
> 1Password does not expose a card's native expiration field. For credit cards stored in 1Password, add a text field named **Expire Date**, **Expiry Date** or **Expiration Date** in `MM/YYYY` or `MM/YY` format.

> [accordion] Custom Credential Service: the API your service implements
>
> With a custom service connected, the Add dialog shows **Store in Custom Credential Service**. Skyvern then calls your API with `Authorization: Bearer <API Token>`:
>
> | Request | Purpose | Your response |
> | - | - | - |
> | `POST {API Base URL}` | Create. Body has `name`, `type` (`password`, `credit_card` or `secret`) and the fields for that type, as in the table under [Store a credential](#store-a-credential). | `{"id": "..."}` |
> | `GET {API Base URL}/{id}` | Read at run time | The stored fields, including `type` |
> | `DELETE {API Base URL}/{id}` | Delete | Any 2xx status |
>
> Self-hosted Skyvern reads the same settings from `CREDENTIAL_VAULT_TYPE=custom`, `CUSTOM_CREDENTIAL_API_BASE_URL` and `CUSTOM_CREDENTIAL_API_TOKEN`. The other vaults (`CREDENTIAL_VAULT_TYPE` values `skyvern`, `bitwarden`, `azure_vault`, `gcp`), including a self-hosted [Bitwarden or Vaultwarden](https://skyvern.com/docs/reference/configuration.md#bitwarden-and-vaultwarden) setup, are in the [configuration reference](https://skyvern.com/docs/reference/configuration.md#credentials-and-encryption).

## Two-factor authentication

Set 2FA up on the password credential itself: expand **Two-Factor Authentication** in the Add or Edit dialog and pick how the site sends codes.

![Two-Factor Authentication options: Authenticator App, Email, Text Message and Passkey (Enterprise)](https://mintcdn.com/skyvern/amcBKP0IBwdNswdN/images/cloud/credentials-2fa-setup.png?fit=max\&auto=format\&n=amcBKP0IBwdNswdN\&q=85\&s=f6b59afa54ae9f0a61c6353ef1f66e65)

| Method | How Skyvern gets the code |
| - | - |
| **Authenticator App** | Generates it from the stored key. No waiting, no setup outside Skyvern. Use this whenever the site offers it. |
| **Email** | Reads it from a [connected inbox](#connect-an-inbox), or waits for a code you [forward](#forward-codes-automatically) or [push](#push-a-code). |
| **Text Message** | Receives codes from an [enabled Skyvern or Twilio number](#connect-an-sms-number), or waits for you to [push them](#push-a-code). |
| **Passkey** | Enterprise plan. |

### Authenticator app

> [tab] In the app
>
> 1. Choose **Authenticator App**.
> 2. Pick the app the site uses: **Google Authenticator**, **Microsoft Authenticator**, **Okta Verify** or **Oracle Mobile Authenticator**. Google Authenticator covers any standard authenticator setup code.
> 3. Paste the key into **Authenticator Key**, or click **Scan QR** and upload a screenshot of the site's QR code.
> 4. Click **Save**.
>
> ![Authenticator App selected with Google Authenticator, the Authenticator Key field and the Scan QR button](https://mintcdn.com/skyvern/amcBKP0IBwdNswdN/images/cloud/totp-setup-2.png?fit=max\&auto=format\&n=amcBKP0IBwdNswdN\&q=85\&s=e45189b85a2f76bba81b4f794e8d4b4b)

> [tab] With code
>
> Add `totp` and `totp_type` to a password credential. `totp` takes the base32 key or the full `otpauth://totp/...` URI.
>
> ```python theme={null}
> credential = await client.create_credential(
>     name="Vendor portal",
>     credential_type="password",
>     credential={
>         "username": "you@example.com",
>         "password": "your-password",
>         "totp": "JBSWY3DPEHPK3PXP",
>         "totp_type": "authenticator",
>     },
> )
> ```
>
> Keys stored in Bitwarden (**Authenticator Key (TOTP)**), 1Password (**One-Time Password** field) or Azure Key Vault (`azure_vault_totp_secret_key`) work the same way.

**Finding the key.** When you turn on 2FA, most sites show a QR code with a "Can't scan?" or "Enter manually" link that reveals the key, a string like `JBSWY3DPEHPK3PXP`. Password managers that store 2FA keys show it on the login item. If 2FA is already on and you never saved the key, turn 2FA off and on again on the site to get a new one.

### Email and text message codes

1. Choose **Email** or **Text Message**.
2. Fill in the identifier: the address or phone number that receives codes. For **Email**, the **TOTP Identifier (Username or Email)** field lists your connected inboxes under **Select a connected account**. Click the code icon above it (**Enter manually**) to type an address instead. For **Text Message**, select an enabled Skyvern or Twilio number in **TOTP Identifier (Phone)**. Click **Enter number manually** to type another number, including its country code (for example, `+14155550123`). Other numbers need the [manual push flow](#push-a-code).
3. Click **Save**.

![Email selected with the TOTP Identifier field showing Select a connected account](https://mintcdn.com/skyvern/amcBKP0IBwdNswdN/images/cloud/totp-setup-3.png?fit=max\&auto=format\&n=amcBKP0IBwdNswdN\&q=85\&s=faebcebafe5ab470f35111b425c639e4)

When a run reaches the code prompt, Skyvern waits for a code sent to that identifier. In code, pass the same value as `totp_identifier` to `login`, `run_task` or `run_workflow`. In Studio, blocks that sign in have a **2FA Identifier** field.

### Connect an inbox

On the **2FA** or **Magic Links** tab, click **Connect an inbox**. It opens [Integrations](https://skyvern.com/docs/cloud/integrations.md), where you connect Gmail or Outlook. Skyvern then finds verification codes and magic links in that inbox on its own, with no forwarding.

Two things have to line up:

* The account's row on Integrations shows **Read inbox**. An account connected for sending only is skipped.
* The credential's **TOTP Identifier (Username or Email)** is that inbox's address.

Skyvern reads only mail that arrives after the run starts waiting, so a code sent earlier is ignored. When you can't connect the inbox — a Workspace admin blocks third-party apps, or the connection doesn't go through — [forward the codes](#forward-codes-automatically) instead. Forwarding works for any number of inboxes and needs no connection.

### Connect an SMS number

Skyvern forwards verification messages from enabled numbers automatically. You do not need a Twilio Function.

**1. Choose a number source**

Open [Integrations](https://skyvern.com/docs/cloud/integrations.md) and choose **Skyvern number** or **Twilio**.

* **Skyvern number**: search for an available US number, optionally by area code, then click **Get this number**. Self-serve organizations need the Pro plan. Each number costs \$10/month, with the first month charged when Skyvern sets it up. An active enterprise agreement covers numbers without a separate per-number fee.
* **Twilio**: enter your Account SID and Auth Token. You can also supply an API Key SID and API Key Secret. The Auth Token is required to verify inbound webhook signatures.

**2. Enable forwarding**

Skyvern configures forwarding when it sets up a managed number.

For Twilio, enable the number in the integration panel. If it already has inbound SMS routing, review that routing before you confirm its replacement. When you disable the number, Skyvern restores the previous routing only if the number still points to Skyvern.

**3. Link the number to a login**

Create or edit a password credential. Under **Two-Factor Authentication**, choose **Text Message**, select the enabled number, and click **Save**.

Start a run with that credential and send a verification SMS to the number. Skyvern extracts the code and supplies it to the waiting run.

> [!NOTE]
>
> A saved authenticator secret takes precedence over SMS. Skyvern generates a code from that secret before it polls for SMS codes. The credential form shows this notice when the credential has a saved authenticator secret, even though the secret itself stays hidden.

Some services do not send verification codes to VoIP numbers. Test your target service before you depend on SMS forwarding.

When you release a subscribed Skyvern number, billing and forwarding continue until the current billing period ends. For billing-exempt numbers without a subscription, forwarding stops immediately. Skyvern then holds the number for 30 days before it can be reassigned.

The [API reference](https://skyvern.com/docs/api-reference) covers managed numbers under `/v1/sms-numbers` and Twilio credentials and numbers under `/v1/integrations/twilio`.

### Push a code

Without a connected inbox or an enabled SMS number, deliver email and SMS codes yourself.

> [tab] In the app
>
> While a run waits for a code, its page shows a "needs 2FA" banner with a countdown. Click **Enter Code** and paste the message.
>
> Or open the **2FA** tab on the Credentials page and use **Push a 2FA Code**:
>
> * **Identifier**: the email or phone that received the code.
> * **Verification content**: the whole message, or just the code. Skyvern extracts the code.
> * **OTP Type**: **Numeric code**, or **Magic link** for a one-time sign-in link.
> * **Add optional metadata**: when several runs wait on the same identifier, add the **Agent run ID**, **Agent ID** or **Task ID** to target one.
>
> Click **Send 2FA Code** (it reads **Send Magic Link** for magic links). The table below the form lists every code received, with filters for identifier, OTP type and count.
>
> ![2FA tab with the Automatic 2FA from your inbox card and the Push a 2FA Code form](https://mintcdn.com/skyvern/amcBKP0IBwdNswdN/images/cloud/credentials-2fa-push-form.png?fit=max\&auto=format\&n=amcBKP0IBwdNswdN\&q=85\&s=ff487b37f98ec2ca3e706a7a419d4805)

> [tab] With code
>
> Post the code to Skyvern yourself, or have a [relay](#forward-codes-automatically) do it. Send the whole message body; Skyvern extracts the code.
>
> ```python Python theme={null}
> await client.send_totp_code(
>     totp_identifier="you@example.com",
>     content="Your verification code is 847291",
>     source="email_forwarder",
> )
> ```
>
> ```typescript TypeScript theme={null}
> await client.sendTotpCode({
>   totp_identifier: "you@example.com",
>   content: "Your verification code is 847291",
>   source: "email_forwarder",
> });
> ```
>
> ```bash cURL theme={null}
> curl -X POST "https://api.skyvern.com/v1/credentials/totp" \
>   -H "x-api-key: $SKYVERN_API_KEY" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "totp_identifier": "you@example.com",
>     "content": "Your verification code is 847291",
>     "source": "email_forwarder"
>   }'
> ```
>
> * `source` is a free-text label for your own tracking.
> * Add `workflow_run_id`, `workflow_id` or `task_id` to target one run.
> * A sign-in link needs no extra field. Skyvern detects a magic link from the content; the `type` field is deprecated and ignored.
> * List received codes with `GET /v1/credentials/totp`. Filter with `totp_identifier`, `workflow_run_id` and `otp_type` (`totp` or `magic_link`); `limit` defaults to 50, maximum 200.

### Forward codes automatically

A relay takes the person out of the loop: the inbox forwards the code email to a service that posts it to `POST /v1/credentials/totp`. Every inbox can forward to the same relay, so this scales to as many accounts as you have.

**1. Build the relay**

Use anything that receives mail and makes an HTTP request: [Zapier](https://skyvern.com/docs/cloud/integrations.md#zapier), Make, n8n, a mail-handling Lambda, or the Apps Script below.

In Zapier, an inbound email trigger needs a paid plan:

1. Trigger: **Email by Zapier** → **New Inbound Email**. Zapier gives you an address such as `codes@robot.zapier.com`. Keep that address: it is the relay address below.
2. Action: **Webhooks by Zapier** → **POST**.
   * **URL**: `https://api.skyvern.com/v1/credentials/totp`
   * **Payload Type**: `json`
   * **Data**: `totp_identifier` → **To Email**, `content` → **Body Plain**, `source` → `email`
   * **Headers**: `x-api-key` → your Skyvern API key
3. Publish the Zap.

`totp_identifier` has to come out as the address that received the code, not the relay address. The last step checks it.

**2. Forward the code mail from each inbox**

In Gmail, open **Settings → Forwarding and POP/IMAP → Add a forwarding address**, enter the relay address and confirm the verification mail Gmail sends to it. In Zapier, that mail shows up in the trigger's test data.

Then open **Settings → Filters and Blocked Addresses → Create a new filter**. Match the mail that carries codes, by the sender address of the site or by a subject such as `verification code`. Click **Create filter**, check **Forward it to** and pick the relay address.

Repeat for each inbox. They can all forward to the same relay address.

**3. Point the credential at the inbox**

On the password credential, set **Two-Factor Authentication** to **Email** and put that inbox's address in **TOTP Identifier (Username or Email)**.

Matching an email identifier ignores case and nothing else. `you+codes@example.com` and `you@example.com` are two different identifiers.

**4. Test it end to end**

Forward an old code email to the relay address by hand, then read back what Skyvern stored:

```bash theme={null}
curl -G "https://api.skyvern.com/v1/credentials/totp" \
  -H "x-api-key: $SKYVERN_API_KEY" \
  --data-urlencode "totp_identifier=you@example.com"
```

The newest entry's `totp_identifier` is the inbox address and `code` is the code from the mail. If the identifier came out as the relay address or carries a display name, map `totp_identifier` from a different field of the trigger, or give that inbox its own relay address and set `totp_identifier` to a fixed value in its Zap.

> [accordion] Forward from Gmail with Google Apps Script
>
> No second service, and the identifier can't come out wrong, because the script runs inside the inbox. Set it up once per inbox.
>
> 1. In Gmail, create a filter for the code mail with **Apply the label** → a new label `skyvern-2fa`.
>
> 2. Open [script.google.com](https://script.google.com), create a project and paste:
>
>    ```javascript theme={null}
>    const LABEL = 'skyvern-2fa';
>
>    function forwardCodes() {
>      const apiKey = PropertiesService.getScriptProperties().getProperty('SKYVERN_API_KEY');
>      const label = GmailApp.getUserLabelByName(LABEL);
>      const identifier = Session.getEffectiveUser().getEmail();
>      label.getThreads(0, 10).forEach((thread) => {
>        const message = thread.getMessages().pop();
>        UrlFetchApp.fetch('https://api.skyvern.com/v1/credentials/totp', {
>          method: 'post',
>          contentType: 'application/json',
>          headers: { 'x-api-key': apiKey },
>          payload: JSON.stringify({
>            totp_identifier: identifier,
>            content: message.getPlainBody(),
>            source: 'gmail-apps-script',
>          }),
>        });
>        thread.removeLabel(label);
>      });
>    }
>    ```
>
> 3. In **Project Settings → Script properties**, add `SKYVERN_API_KEY` with your API key.
>
> 4. In **Triggers**, add a trigger for `forwardCodes`, **Time-driven** → **Minutes timer** → **Every minute**, and approve the Gmail and external-request permissions.
>
> A code waits up to a minute for the next trigger. Runs wait 15 minutes for a code, so that leaves room, but push a code by hand if the site expires its codes faster.

### Let Skyvern fetch codes from your endpoint

If codes land in a system you control, give Skyvern a URL to ask instead. Skyvern sends a `POST` with `task_id`, plus `workflow_run_id` and `workflow_permanent_id` for agent runs, signed with the `x-skyvern-signature` header (see [Going to production](https://skyvern.com/docs/developers/production.md) to verify it).

Answer with HTTP 200 and JSON:

```json theme={null}
{"verification_code": "123456"}
```

Until you have a code, answer 200 with `{"verification_code": ""}`. Any other status, or a body that isn't a JSON object, counts as an error. If the wait runs out, the run fails with `OTP_TIMEOUT` and its failure reason lists the status codes your endpoint returned.

Where to set the URL:

* **One-off login or task**: pass `totp_url`.
* **Agent**: set **2FA Verification URL** on the block that signs in. The run-level `totp_url` does not fill in a block whose field is empty. For a URL that changes per run, add an input such as `totp_url`, set the block field to `{{ totp_url }}` and pass the URL in `parameters`.

### Magic links

Some sites email a one-time sign-in link instead of a code. With a [connected inbox](#connect-an-inbox), Skyvern finds and opens the link. Otherwise paste the message into **Push a Magic Link** on the **Magic Links** tab, or push it with `send_totp_code`. A [relay](#forward-codes-automatically) handles magic links too: post the whole message and Skyvern detects the link. Skyvern attaches the link to the waiting run and opens it.

### Passkey

**Passkey** appears as a 2FA option marked **Enterprise**. See [https://www.skyvern.com/pricing](https://www.skyvern.com/pricing) for plans. Passkey credentials can't use **Save browser session for future logins**.

### Passkeys from a connected Bitwarden account

You need an Enterprise plan and a connected Bitwarden account to use these passkeys.

1. Choose a Bitwarden login item that holds the passkey.
2. Add the website's address to that item with Bitwarden's default, Host, or Base domain match.
3. Add the item to Skyvern as a connected credential.
4. Select the credential in your workflow.

The item's website addresses limit where Skyvern can answer a passkey request.

Set the workflow's or run's browser type to Stealth Chromium, Google Chrome, or Microsoft Edge.
Vendor browsers, persistent browser sessions, and browsers attached by address are not supported.

Your own passkey in Bitwarden keeps working. Skyvern keeps the signature counter at 0.
Skyvern skips a passkey whose counter is above 0.
The private key stays in Skyvern and never enters the browser.

Skyvern answers only within 10 seconds after the agent clicks or types in that tab.
If the site asks first, the agent clicks the site's retry or passkey button.
Passkey autofill is not supported.
Skyvern does not answer inside sandboxed frames or frames from another site.

Passkeys stored in Skyvern keep working.
If the same passkey exists in both places, Skyvern uses the stored copy.

### Timing

* Skyvern checks for pushed or fetched codes every 10 seconds, for up to 15 minutes.
* A pushed code stays usable for 10 minutes. The site's own code may expire sooner.
* Authenticator codes rotate every 30 seconds. When a code is split across several boxes (one digit per box), or the run uses the `skyvern-3.0` engine, Skyvern waits for a fresh code if fewer than 20 seconds remain, so the code doesn't expire mid-entry.

## Troubleshoot logins

| Symptom | Check |
| - | - |
| Run ends on the login page and nothing was typed | The Login block's **Credential** field or the credential input is set, and the credential still exists. For a vault source, the vault tile on Integrations is connected and the item IDs are right. |
| "Invalid username or password" | Sign in by hand with the same values. Some sites want the email, not a username. Re-enter the password to rule out a trailing space. |
| Run times out waiting for a code (`OTP_TIMEOUT`) | The identifier on the credential or run matches the one you push to, character for character. Check the code list on the **2FA** tab or `GET /v1/credentials/totp`. For an endpoint, read the failure reason for the status codes it returned. |
| A connected inbox never produces a code | The account's row on [Integrations](https://skyvern.com/docs/cloud/integrations.md) shows **Read inbox**, the credential's identifier is that inbox's address, and the code mail arrives after the run starts waiting. If the inbox can't be connected, [forward the codes](#forward-codes-automatically). |
| Your relay gets an error from `POST /v1/credentials/totp` | 502 means code extraction is briefly unavailable, so have the relay retry. 400 and 402 come from very large bodies: post the plain-text part of the mail, not the full HTML. |
| "Code expired" or the code is rejected | Push codes as soon as they arrive. On self-hosted servers, keep the clock in sync (NTP). Retry the run. |
| **Save browser session for future logins** fails | The **Login Page URL** opens the sign-in form. Add **Login instructions (optional)** for unusual flows such as SSO. Inline testing is not available for every 2FA method. |
| Extra captchas or "new device" prompts | Turn on **Use a consistent IP address**, or reuse a signed-in [browser profile](https://skyvern.com/docs/browsers.md#browser-profiles). |

For problems that aren't specific to credentials, see [Troubleshooting](https://skyvern.com/docs/developers/troubleshooting.md).

## Next

* [Browsers: sessions and profiles](https://skyvern.com/docs/browsers.md): keep a signed-in browser and reuse it.
* [Build in Studio](https://skyvern.com/docs/cloud/studio.md): add a Login block and credential inputs.
* [Integrations](https://skyvern.com/docs/cloud/integrations.md): connect inboxes and vaults.


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