How to handle 2FA
in an AI agent script.
When the login screen asks for a six-digit code, the agent should answer — without waking you up at 3am to read your authenticator app.
Any agent that logs into real services eventually hits 2FA. The service asks for a code; the agent has no way to produce one; the workflow stalls. The fix is to give the agent its own TOTP store — it enrolls the service once, then generates codes on demand just like an authenticator app.
This recipe walks through both the TOTP case (for services that support authenticator apps) and the email-fallback case (for services that send codes via email).
1. Enroll the TOTP secret in the vault
When you set up 2FA on the target service, you get a QR code or an otpauth:// URL. This URL contains the shared secret. Store it in the vault under a label — do this once, from your dev box, not from the agent.
curl -X POST https://api.loomal.ai/v0/vault \
-H "Authorization: Bearer $LOOMAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "crm-totp",
"otpauth": "otpauth://totp/Service:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Service"
}'2. Get the current code from Python
When the agent hits a 2FA prompt, call the vault's totp endpoint with the label. It returns the current six-digit code, plus seconds remaining in the current window so you know whether to wait for the next one.
import os, requests
def get_code(label: str) -> str:
res = requests.get(
f"https://api.loomal.ai/v0/vault/{label}/totp",
headers={"Authorization": f"Bearer {os.environ['LOOMAL_API_KEY']}"},
timeout=5,
)
res.raise_for_status()
payload = res.json()
# payload = {"code": "123456", "remainingSeconds": 22}
if payload["remainingSeconds"] < 3:
# Wait for a fresh window so the code doesn't expire mid-submit
import time
time.sleep(3)
return get_code(label)
return payload["code"]
code = get_code("crm-totp")
# Submit `code` to the 2FA form3. Same thing in TypeScript
TypeScript/Node version — same endpoint, same payload. Use this inside a Playwright or browser-automation script where the agent is driving a login form.
async function getCode(label: string): Promise<string> {
const res = await fetch(
`https://api.loomal.ai/v0/vault/${label}/totp`,
{ headers: { Authorization: `Bearer ${process.env.LOOMAL_API_KEY}` } },
);
const payload = (await res.json()) as { code: string; remainingSeconds: number };
if (payload.remainingSeconds < 3) {
await new Promise((r) => setTimeout(r, 3000));
return getCode(label);
}
return payload.code;
}4. Fallback: codes delivered by email
Some services skip TOTP and just email a one-time code. If the agent has its own Loomal mailbox, the code lands in its inbox and the agent reads it with mail.list_messages. Look for the most recent message from the service, extract the code with a regex, submit it.
import os, re, requests, time
def latest_code_from(sender: str, pattern: str = r"\b(\d{6})\b") -> str | None:
# Wait up to 30s for the code email to arrive
for _ in range(15):
res = requests.get(
f"https://api.loomal.ai/v0/messages",
headers={"Authorization": f"Bearer {os.environ['LOOMAL_API_KEY']}"},
params={"from": sender, "labels": "unread", "limit": 1},
timeout=5,
)
messages = res.json()["messages"]
if messages:
match = re.search(pattern, messages[0]["extractedText"])
if match:
return match.group(1)
time.sleep(2)
return NoneFAQ
Isn't storing the TOTP secret server-side weaker than an authenticator app?
The secret is encrypted at rest in the agent's vault, scoped to the agent's identity, and never returned in plaintext — only the derived code is. Equivalent threat model to a password manager holding TOTP (Bitwarden, 1Password), which is widely accepted practice.
What if the service uses WebAuthn / passkeys?
Not covered by TOTP. For services that only support passkeys, you'll need platform-specific handling — there's no general solution. Push the vendor toward TOTP where possible.
How do I rotate a TOTP secret?
Re-enroll 2FA on the target service (you'll get a new QR), then overwrite the vault entry with the new otpauth URL. Old codes stop working immediately; the agent picks up the new secret on the next vault.totp call.
Related reading
Last updated: 2026-04-15