CarlyEmail docs

Example: an agent that signs itself up

Register for a service, read the verification code out of the inbox, carry on. No human forwarding anything.

Almost every service on the internet wants an email address and sends a code to it. That single fact is what stops most agents from doing anything useful on their own — not reasoning, not tool use, just not having somewhere to receive a six-digit number.

An agent with its own inbox does not have that problem.

What it does

  1. The agent signs up for something using its own address
  2. The service emails a verification code
  3. The agent waits for it, reads it, and enters it

1. Give the agent an inbox

npx carlyemail signup --human-email you@example.com --username shopper
npx carlyemail verify 123456

shopper@carlyemail.com now receives real mail.

2. Wait for the code

The naive version polls list_messages. The better version subscribes and waits, but polling is three lines and makes the shape obvious:

Python

import re, time, httpx

API = "https://api.carlyemail.com"
INBOX = "shopper@carlyemail.com"
carly = httpx.Client(base_url=API, headers={"authorization": f"Bearer {KEY}"})


def wait_for_code(from_domain: str, timeout: int = 120) -> str | None:
    """Return the first numeric code from `from_domain`, or None."""
    deadline = time.time() + timeout
    seen: set[str] = set()

    while time.time() < deadline:
        messages = carly.get(
            f"/v0/inboxes/{INBOX}/messages", params={"limit": 20}
        ).json()["messages"]

        for m in messages:
            if m["message_id"] in seen:
                continue
            seen.add(m["message_id"])
            if from_domain not in (m.get("from") or ""):
                continue
            # Read the full message: the list gives a preview, and a code is
            # often past where the preview stops.
            full = carly.get(
                f"/v0/inboxes/{INBOX}/messages/{m['message_id']}"
            ).json()
            body = full.get("extracted_text") or full.get("text") or ""
            if code := re.search(r"\b(\d{4,8})\b", body):
                return code.group(1)

        time.sleep(3)
    return None

extracted_text is the new content with quoted chains stripped, which matters here — a forwarded or replied-to message can carry an older code further down, and a naive regex over the whole body finds the wrong one.

3. Use it

Python

code = wait_for_code("example-shop.com")
if code is None:
    raise TimeoutError("no verification email arrived")
browser.fill("#otp", code)

Doing this safely

The agent is now reading real mail from strangers, so two things matter.

Check who sent it. The example above matches on sender domain, which is the minimum. Inbound mail carries its authentication verdicts — a message failing DMARC is labelled rather than handed over as genuine, so you can require that the sender is who they claim:

Python

if "spoofed" in (full.get("labels") or []):
    continue   # failed DMARC — do not trust a code in this

Scope the key. An agent doing signups needs to read one inbox. Give it a key pinned to that inbox with message_read and nothing else, and a bug in your prompt cannot become an email sent to a customer.

curl -X POST https://api.carlyemail.com/v0/inboxes/shopper@carlyemail.com/api-keys \
  -H "authorization: Bearer $CARLYEMAIL_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"name": "signup agent", "permissions": {"message_read": true}}'

A note on what you sign up for

An agent that can create accounts can create accounts you did not intend, on services whose terms may prohibit automated signup. That is your call to make rather than ours, but make it deliberately — and read our terms on what this account may be used for.

Where to go next

  • Receiving — webhooks instead of polling, which is what you want in production
  • Labels — including the authentication verdicts above
  • Authentication — the full permission list for scoping keys