myagentmail

2026-03-26 · 2fa verification websocket tutorial ai-agents

Handling 2FA Verification Codes in AI Agent Workflows

Your AI agent is signing up for a service. It fills out the registration form, clicks submit, and the service says "We've sent a verification code to your email." Now your agent needs to read that email, extract the code, and enter it — all within the next 60 seconds before the code expires.

This is the inline 2FA pattern, and it's one of the most common reasons agents need real-time email access. Here's how to build it properly.

Why This Is Hard with Traditional Approaches

Polling Doesn't Work

The naive approach is to poll the inbox on an interval:

// Don't do this
while (!verificationEmail) {
  await sleep(5000);
  const messages = await fetchInbox();
  verificationEmail = messages.find(m => m.subject.includes("verification"));
}

Polling introduces 0-30 second latency depending on your interval. Verification codes typically expire in 60-120 seconds. By the time you detect the email, extract the code, and submit it, you've burned through most of your window. And if the service rate-limits code requests, a failed attempt means starting the whole flow over.

Webhooks Are Close, but Fragile

Webhook-based inbound email gets you closer — the email provider POSTs to your endpoint when mail arrives. But there are problems:

  1. You need a public URL. Your agent needs a reachable HTTP endpoint, which means either a deployed server or a tunnel.
  2. Delivery isn't instant. Webhook systems batch and retry. You might see 2-10 seconds of latency, sometimes more.
  3. Correlation is tricky. Your agent is mid-flow, waiting for a specific email. The webhook hits a separate endpoint. You need shared state (a database, a queue, a pub/sub channel) to bridge the two.
  4. Timeout handling is your problem. If the webhook never fires (email delayed, service down), your agent hangs.

WebSocket Is the Answer

A persistent WebSocket connection gives you sub-second delivery with no public URL required. The agent opens a connection, receives events as they happen, and processes them inline in the same execution context.

The Full Implementation

Here's a complete TypeScript implementation of the inline 2FA pattern using myagentmail's WebSocket API.

import WebSocket from "ws";

interface EmailEvent {
  type: "message.received";
  data: {
    id: string;
    inboxId: string;
    from: string;
    subject: string;
    text: string;
    html: string;
    receivedAt: string;
  };
}

function extractVerificationCode(text: string): string | null {
  // Match 4-8 digit codes, which covers most 2FA patterns
  const patterns = [
    /\b(\d{6})\b/,           // 6-digit (most common)
    /\b(\d{4})\b/,           // 4-digit
    /\b(\d{8})\b/,           // 8-digit
    /code[:\s]+(\d{4,8})/i,  // "code: 123456"
    /verify[:\s]+(\d{4,8})/i // "verify: 123456"
  ];

  for (const pattern of patterns) {
    const match = text.match(pattern);
    if (match) return match[1];
  }
  return null;
}

async function waitForVerificationCode(
  apiKey: string,
  inboxId: string,
  timeoutMs: number = 60000
): Promise<string> {
  return new Promise((resolve, reject) => {
    const ws = new WebSocket(
      `wss://myagentmail.com/v1/ws?inboxId=${inboxId}`,
      { headers: { "X-API-Key": apiKey } }
    );

    const timeout = setTimeout(() => {
      ws.close();
      reject(new Error("Verification code not received within timeout"));
    }, timeoutMs);

    ws.on("message", (raw: Buffer) => {
      const event: EmailEvent = JSON.parse(raw.toString());

      if (event.type === "message.received") {
        const code = extractVerificationCode(event.data.text);
        if (code) {
          clearTimeout(timeout);
          ws.close();
          resolve(code);
        }
      }
    });

    ws.on("error", (err) => {
      clearTimeout(timeout);
      reject(err);
    });
  });
}

Using It in an Agent Flow

Here's how this fits into a complete sign-up automation:

async function signUpForService(
  agentEmail: string,
  inboxId: string,
  apiKey: string
) {
  // Step 1: Start listening BEFORE triggering the email
  const codePromise = waitForVerificationCode(apiKey, inboxId);

  // Step 2: Submit the sign-up form (via browser automation, API, etc.)
  await submitSignUpForm({
    email: agentEmail,
    password: generateSecurePassword(),
    name: "Alex from Acme",
  });

  // Step 3: Wait for the code (WebSocket delivers it in real time)
  const code = await codePromise;
  console.log(`Received verification code: ${code}`);

  // Step 4: Submit the verification code
  await submitVerificationCode(code);
  console.log("Account verified successfully");
}

The critical detail is step ordering: you start the WebSocket listener before triggering the verification email. This eliminates the race condition where the email arrives before your listener is ready.

Handling Edge Cases

Multiple Emails Arriving

Sometimes a service sends a welcome email before the verification email, or sends both simultaneously. The extractVerificationCode function filters for messages that actually contain codes, so non-verification emails are ignored.

Code Expiration and Retry

If the code expires before your agent can use it, you need a retry loop:

async function signUpWithRetry(
  agentEmail: string,
  inboxId: string,
  apiKey: string,
  maxRetries: number = 3
) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const codePromise = waitForVerificationCode(apiKey, inboxId, 45000);

      if (attempt > 1) {
        await requestNewVerificationCode(agentEmail);
      } else {
        await submitSignUpForm({ email: agentEmail });
      }

      const code = await codePromise;
      await submitVerificationCode(code);
      return; // Success
    } catch (err) {
      console.log(`Attempt ${attempt} failed: ${err.message}`);
      if (attempt === maxRetries) throw err;
    }
  }
}

Parsing Complex Verification Emails

Some services don't put the code in plain text — they embed it in HTML or use "click this link" instead of a code. For link-based verification:

function extractVerificationLink(html: string): string | null {
  const patterns = [
    /href="(https?:\/\/[^"]*verify[^"]*)"/i,
    /href="(https?:\/\/[^"]*confirm[^"]*)"/i,
    /href="(https?:\/\/[^"]*activate[^"]*)"/i,
  ];

  for (const pattern of patterns) {
    const match = html.match(pattern);
    if (match) return match[1];
  }
  return null;
}

Why This Pattern Matters

The inline 2FA pattern isn't just about sign-ups. Any workflow where an agent needs to receive a time-sensitive email and act on it benefits from the same architecture:

In each case, the pattern is identical: start listening, trigger the action, process the response in real time. The WebSocket approach makes this reliable and fast, without the complexity of webhook infrastructure or the latency of polling.

The difference between a 200ms response and a 15-second polling delay might not matter for a human checking their inbox. For an agent in the middle of an automated workflow with a ticking clock, it's the difference between success and failure.


← All posts