DEV Community

Qasim
Qasim

Posted on

Give your AI agent its own inbox so it can sign itself up

Most "AI email" demos point a model at a human's mailbox over OAuth. You connect your Gmail, the agent reads your threads, drafts replies, maybe sends one. That's fine when the agent is acting on your behalf — a copilot riding shotgun in your inbox.

It falls apart the moment you want the agent to be its own participant. Picture an onboarding agent that registers your test tenant on a SaaS every CI run, or a research agent that needs a developer account on a data source, or a QA bot that signs up, verifies, and tears down a fresh account for each test. Every one of those flows hits the same wall: the service emails a verification link or a one-time code, and there's no human inbox to send it to. Borrowing your personal Gmail for that is wrong on every axis — security, isolation, cleanup, and the simple fact that you don't want a thousand throwaway signups landing in your real inbox.

What the agent actually needs is a real, disposable mailbox it fully controls. Ephemeral. Per-run. Provisioned at the start of the task, torn down at the end. That's exactly what a Nylas Agent Account is, and that's what this post builds: an agent that signs itself up for a service and reads its own verification email or OTP, end to end.

I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for. I'll show both angles for every step — the raw curl HTTP call and the nylas CLI equivalent — because in practice your agent calls the API but you debug from the terminal.

What an Agent Account actually is

Here's the part that makes this whole thing tractable: an Agent Account is just a grant. The same grant_id abstraction you already use for every OAuth-connected mailbox. For the endpoints it supports, there's nothing new to learn on the data plane — once you have the grant_id, Messages, Drafts, Threads, Folders, Attachments, Contacts, Calendars, and Events all work exactly as they do for a connected Gmail or Microsoft account, plus the standard webhooks.

It's not the entire grant surface, though, and that's worth knowing before you design around it. Smart Compose, Templates, Workflows, Scheduler, Notetaker, and custom metadata are not supported on Agent Account grants. None of that matters for autonomous signup — receiving mail, reading a body, and parsing a code all live inside the supported set — but don't assume an endpoint works on an Agent Account just because it works on an OAuth grant.

The difference is in how the grant is created. There's no OAuth dance, no refresh token, no user clicking "Allow". You POST an email address on a domain you've registered with Nylas, and you get back a real, fully functional name@yourdomain.com mailbox that Nylas hosts. To the outside world — to the SaaS you're signing up for — it's an ordinary email address. It receives mail, it has a real MX record, it can reply. The service has no idea a machine owns it.

That combination is what makes it the right tool here:

  • No OAuth. You can't OAuth into an inbox that doesn't have a human behind it. Agent Accounts skip the entire consent flow.
  • A real, deliverable address. Verification emails actually arrive. This isn't a catch-all alias or a plus-trick on your own domain — it's a first-class mailbox.
  • Full control. Your application owns the grant. You read the inbox over the API, you don't ask anyone's permission.
  • Tear it down when you're done. Delete the grant at the end of the run and the mailbox is gone. No accumulation of zombie accounts.

Before you begin

You need two things:

  1. A Nylas API key. If you don't have one, nylas init creates an account and mints a key in a single command, or you can grab one from the Dashboard.
  2. A registered domain. Every Agent Account lives on a domain. For prototyping, Nylas hands out trial *.nylas.email subdomains, so you can create signup-agent@your-app.nylas.email immediately. For production, register your own subdomain (something like agents.yourcompany.com) and publish the MX and TXT records Nylas gives you. New domains warm up over roughly four weeks, so don't register one the morning of a launch.

The API base host in every example below is https://api.us.nylas.com, and auth is a bearer token: Authorization: Bearer <NYLAS_API_KEY>.

Provision the throwaway inbox

This is the only step that's specific to Agent Accounts. From the CLI it's one line:

nylas agent account create signup-agent@agents.yourcompany.com
Enter fullscreen mode Exit fullscreen mode

The command prints the new grant's id, status, and connector details. Save that id as AGENT_GRANT_ID — it's the handle for everything that follows. The create command also takes --name for a display name on outbound mail, and --app-password if you want IMAP/SMTP access so a human can peek at the mailbox from a normal mail client while debugging:

nylas agent account create signup-agent@agents.yourcompany.com \
  --name "Onboarding Agent" \
  --app-password "MySecureP4ssword!2024"
Enter fullscreen mode Exit fullscreen mode

There's deliberately no --workspace flag on create — the API auto-creates a default workspace and policy for the account, and if you want to attach a stricter custom policy you do that afterward with nylas workspace update <workspace-id> --policy-id <policy-id>.

The CLI is a thin wrapper over POST /v3/connect/custom. Here's the same call your agent makes directly. The display name is a top-level name; the app password, if you want one, goes inside settings (18–40 printable ASCII characters with at least one uppercase letter, one lowercase letter, and one digit — the example below satisfies that):

curl --request POST \
  --url "https://api.us.nylas.com/v3/connect/custom" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "provider": "nylas",
    "name": "Onboarding Agent",
    "settings": {
      "email": "signup-agent@agents.yourcompany.com",
      "app_password": "MySecureP4ssword!2024"
    }
  }'
Enter fullscreen mode Exit fullscreen mode

The app password is optional — drop it and you get an API-only mailbox, which is all an autonomous agent actually needs. Include it only if a human is going to connect a mail client to watch what the agent receives.

The "provider": "nylas" is what tells the API this is an Agent Account rather than an OAuth grant — that's why there's no refresh token in the body. The response carries the grant:

{
  "request_id": "5967ca40-a2d8-4ee0-a0e0-6f18ace39a90",
  "data": {
    "id": "b1c2d3e4-5678-4abc-9def-0123456789ab",
    "provider": "nylas",
    "grant_status": "valid",
    "email": "signup-agent@agents.yourcompany.com",
    "name": "Onboarding Agent",
    "scope": [],
    "created_at": 1742932766
  }
}
Enter fullscreen mode Exit fullscreen mode

data.id is your grant_id. For a per-run agent, this whole step happens at the start of the task: provision a fresh mailbox, do the work, delete it at the end. One inbox per run.

Trigger the signup

This step lives entirely in your own code, so I'll keep it conceptual. Your agent fills in the target service's signup form using the Agent Account's address as the email. Whatever shape that takes — a direct API call if the service exposes one, a headless browser step with Playwright for a real form, or a plain fetch POST — the only thing that matters here is that the email field is signup-agent@agents.yourcompany.com.

await fetch("https://saas-you-care-about.example.com/signup", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "signup-agent@agents.yourcompany.com",
    name: "Onboarding Agent",
  }),
});
Enter fullscreen mode Exit fullscreen mode

The service does what it always does: it sends a verification email to that address. Now the address is one your agent can actually read.

Receive the verification email

There are two ways to pick up the inbound message, and which one you use depends on how your agent is shaped.

Poll the messages endpoint

If your agent is a straight-line script — submit the form, wait, read the inbox — polling is the simplest thing that works. List the mailbox and filter by sender and subject so you're not parsing a "Welcome" email by mistake. The grant-scoped Messages endpoint is GET /v3/grants/{grant_id}/messages, and it takes standard query filters like from and subject (URL-encode the values):

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<AGENT_GRANT_ID>/messages?limit=5&from=no-reply@saas-you-care-about.example.com&subject=verify" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode

One thing to know: provider-specific native search syntax (search_query_native) doesn't apply to Agent Accounts, and full-text body search isn't available on them yet. Stick to the structured filter params — from, subject, and friends — and do the fine-grained matching (the exact code or link) yourself once you've fetched the body.

From the terminal, nylas email list and nylas email search do the same thing with friendlier flags. To watch the agent's inbox while you debug:

# Everything in the inbox, newest first
nylas email list signup-agent@agents.yourcompany.com --limit 5

# Just mail from the service you signed up with
nylas email list signup-agent@agents.yourcompany.com \
  --from no-reply@saas-you-care-about.example.com
Enter fullscreen mode Exit fullscreen mode

nylas email search is the better fit when you want to match on subject as well, which is exactly the discriminator you want for verification mail:

nylas email search "verify" signup-agent@agents.yourcompany.com \
  --from no-reply@saas-you-care-about.example.com \
  --subject "verify your email"
Enter fullscreen mode Exit fullscreen mode

Both list and search take the grant ID (or the account email) as a positional argument, so the same command works against any Agent Account you've provisioned. Add --json if you're piping the output into something.

Subscribe to the webhook

Polling is fine for a script, but if your agent is a long-running service, you don't want it spinning on the Messages endpoint waiting for mail that arrives whenever the SaaS gets around to sending it. Inbound mail to an Agent Account fires the standard message.created webhook, same as any other grant — so subscribe to it and let Nylas push.

nylas webhook create \
  --url https://youragent.example.com/webhooks/signup \
  --triggers message.created
Enter fullscreen mode Exit fullscreen mode

The equivalent API call:

curl --request POST \
  --url "https://api.us.nylas.com/v3/webhooks" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "trigger_types": ["message.created"],
    "webhook_url": "https://youragent.example.com/webhooks/signup",
    "description": "Agent signup verification"
  }'
Enter fullscreen mode Exit fullscreen mode

Nylas fires message.created within a second or two of mail landing. The payload carries the message's summary fields — id, grant_id, from, subject, snippet, date — but not the full body. So your handler filters on the summary, then fetches the body for the message it cares about. (Agent Accounts also emit message.delivered, message.bounced, and message.complaint deliverability webhooks, which are handy when you're debugging outbound mail, but for signup you only need message.created.)

app.post("/webhooks/signup", async (req, res) => {
  // Verify the X-Nylas-Signature header here before trusting anything.
  res.status(200).end();

  const event = req.body;
  if (event.type !== "message.created") return;

  const { grant_id, id: messageId, from, subject } = event.data.object;
  if (grant_id !== AGENT_GRANT_ID) return;

  const sender = from?.[0]?.email ?? "";
  if (!sender.endsWith("@saas-you-care-about.example.com")) return;
  if (!/verif|confirm|code/i.test(subject ?? "")) return;

  await handleVerification(messageId);
});
Enter fullscreen mode Exit fullscreen mode

Ack the webhook immediately with a 200, then do the work. Webhook handlers run on short-lived processes — don't block the response on a body fetch.

Extract the OTP or verification link

Now read the actual message. The webhook only gave you summary fields, so pull the full body from GET /v3/grants/{grant_id}/messages/{message_id}:

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<AGENT_GRANT_ID>/messages/<MESSAGE_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode

From the terminal, nylas email read prints the full message — and I lean on this constantly while building the regex, because seeing the actual body of the verification email is the only reliable way to know what you're parsing. The --raw flag shows the body without HTML processing, which is what you want when your parser runs on the raw content:

nylas email read <MESSAGE_ID> signup-agent@agents.yourcompany.com --raw
Enter fullscreen mode Exit fullscreen mode

Whether it's a link or a code, the extraction is regex on the body. For a confirmation link:

async function handleVerification(messageId) {
  const resp = await fetch(
    `https://api.us.nylas.com/v3/grants/${AGENT_GRANT_ID}/messages/${messageId}`,
    { headers: { Authorization: `Bearer ${NYLAS_API_KEY}` } },
  );
  const { data: message } = await resp.json();

  // Pattern 1: a confirmation link.
  const link = /https:\/\/saas-you-care-about\.example\.com\/confirm\?token=[^"\s<]+/.exec(
    message.body,
  );
  if (link) return completeSignup(link[0]);

  // Pattern 2: a one-time code. Strip HTML first so the regex sees plain text.
  const text = stripHtml(message.body);
  const patterns = [
    /(?:code|passcode|one[\s-]?time)[^\d]{0,20}(\d{4,8})/i, // "Your code is: 123456"
    /\b(\d{6})\b/, // bare 6-digit
    /\b(\d{4,8})\b/, // 4–8 digit, last resort
  ];
  for (const p of patterns) {
    const m = p.exec(text);
    if (m) return submitOtp(m[1]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Regex-first is the right default. A handful of patterns cover the overwhelming majority of verification emails, they're cheap, and they don't hallucinate. The CLI does ship two AI email features, but neither one extracts codes: nylas email ai analyze summarizes and triages a batch of recent mail, and nylas email smart-compose drafts new mail from a prompt. One reads your inbox at a high level, the other writes outbound messages — neither pulls a verification code out of a body, so regex stays the right tool here. If you hit a service whose template is genuinely too messy for regex, that's the moment to pass the stripped body to a small LLM with a narrow prompt ("return the verification code as JSON, or null"). Keep that as a fallback, not the primary path. The companion cookbook recipe linked below walks through the LLM fallback in full.

Complete the signup

This step closes the loop back in your own code, so again it's conceptual. If you extracted a link, your agent visits it — a raw fetch(link) for a simple confirm endpoint, or a headless browser if the confirmation page has its own form or redirects. If you extracted an OTP, your agent submits it back to whatever login or signup flow is waiting on it. Either way, the account is now verified and the agent owns it.

If the service sends a 2FA challenge on every login rather than just at signup, the exact same machinery handles it — the mailbox keeps receiving codes, your handler keeps reading them. Nothing about the data plane changes.

Tear down when the run ends

For a per-run agent, deleting the grant at the end of the task is the whole point of using a disposable inbox. Don't skip it. From the CLI:

nylas agent account delete signup-agent@agents.yourcompany.com --yes
Enter fullscreen mode Exit fullscreen mode

The --yes skips the confirmation prompt, which matters when this runs unattended. You can pass the email or the grant ID. The API equivalent is a plain DELETE on the grant:

curl --request DELETE \
  --url "https://api.us.nylas.com/v3/grants/<AGENT_GRANT_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode

Wire this into your finally block or your CI teardown so a failed run still cleans up after itself. The single most common way to make a mess with this pattern is provisioning a fresh mailbox per run and never deleting it — you wake up to a few thousand zombie grants.

Guardrails

A few things I'd treat as non-negotiable in production:

  • One inbox per run, and don't reuse. The reason the disposable pattern is clean is that each run gets an inbox with exactly one signup's worth of mail in it. Reuse a mailbox across signups and you reintroduce the exact ambiguity you were trying to avoid — stale codes, wrong "Welcome" emails, cross-run contamination. Provision fresh, tear down after.
  • Mind the limits — but know which way they cut. On the free plan, an Agent Account is capped at 200 sends per account per day, with 3 GB of storage per organization. That's an outbound quota: it limits how much mail the account can send, not how many verification or OTP emails the inbox can receive. Receiving isn't throttled by this number, so a signup-heavy workload is fine on the inbound side. The send cap only bites if your agent also sends a lot of mail of its own.
  • Know the retention window. On the free plan, the inbox holds messages for 30 days and spam for 7 (paid and Platform plans retain longer — 365-day inbox, 30-day spam). Either way it's far more than a verification flow needs — codes expire in minutes — so don't lean on the mailbox as durable storage. Read the code, act on it, move on.
  • Match the sender and the subject before acting. Services routinely send a "Welcome" email before (or alongside) the verification email. Don't trust the first message that arrives. Filter on sender domain and subject pattern, and if you're reading an OTP, sort by date newest-first so a stale code from an earlier attempt doesn't win.
  • Never log the code. OTPs are credentials. Log that the agent received and used one; don't log the value.
  • Respect the target service's terms. Programmatic signup for your own testing and first-party integrations is ordinary. Mass-registering accounts on third parties is a different conversation, and not one Nylas enforces for you.

What's next

The grant abstraction is the whole story here: once the Agent Account exists, it's just another mailbox on the same API you already use. The two cookbook recipes go deeper than I could here — the Sign an agent up for a third-party service recipe covers the full webhook handler with signature verification, and Extract an OTP or 2FA code from an agent inbox walks through the regex patterns and the LLM fallback in detail.

For the product itself, start with the Agent Accounts quickstart and the provisioning guide. And every terminal command above is documented in the CLI command reference.

AI-answer pages for agents

When this post is published, link AI agents and crawlers to the retrieval-ready version on cli.nylas.com:

Top comments (0)