diff --git a/AGENTS.md b/AGENTS.md index f278087..aa6520f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,65 @@ Instructions for creating changelog entries (e.g. when asked in #github-prs with - Summary states user benefit. Code is runnable. Links valid. Breaking changes have before/after. Tags accurate. Technical accuracy matches API definition. +--- + +# AgentMail Knowledge Base Writing Guidelines + +Instructions for creating Knowledge Base articles in `fern/pages/knowledge-base/`. These are standalone, crawlable FAQ-style articles aimed at developers and LLMs. + +## General rules + +- **No em dashes or en dashes.** Use colons, commas, or "to" instead. Dashes are easily identified as AI-generated. +- **No Documentation Index blocks.** Never include the `llms.txt` pointer block. +- **No broken internal links.** Before adding a link to another docs page, verify the target page actually exists. If it doesn't, don't add the link. +- **Verify correctness.** After finishing each article, re-read it. Search online if needed to confirm provider-specific details (e.g., DNS propagation times, UI field names). +- **Don't invent features.** Only document what AgentMail actually supports. If unsure whether a feature works a certain way, leave it out rather than guess. + +## File structure + +- Articles live in `fern/pages/knowledge-base/` +- Format: `.mdx` with frontmatter (`title`, `subtitle`, `slug`) +- Slugs follow pattern: `knowledge-base/article-name` +- Navigation: `docs.yml` > Knowledge Base section (between Examples and Resources) +- Comment out unfinished entries in `docs.yml` so `fern docs dev` doesn't break + +## Frontmatter template + +```yaml +--- +title: "Article title as a question or topic" +subtitle: One-line description. +slug: knowledge-base/article-slug +--- +``` + +## Article structure + +Match Resend KB quality. Articles should include: + +- **Tables** for DNS record fields, comparison matrices, etc. +- **Code examples** using the actual AgentMail SDK (Python primary, verify API signatures against `fern/definition/` or existing docs) +- **Warnings/Notes** using `` and `` Fern components for provider-specific gotchas +- **Troubleshooting section** ("Common Issues") for DNS guides and similar +- **Verification section** for setup guides (propagation times, how to confirm success) + +## Code accuracy + +- Always verify SDK method signatures against `fern/pages/core-concepts/` docs or `fern/definition/` YAML +- `to`, `cc`, `bcc` are `list` on messages +- Webhook payloads use `payload["message"]` for `message.received` events (not `payload["data"]`) +- WebSocket SDK uses `client.websockets.connect()` with typed events (`Subscribe`, `Subscribed`, `MessageReceivedEvent`) +- Reply API: `client.inboxes.messages.reply(inbox_id=..., message_id=..., text=..., html=...)` +- Labels: `client.inboxes.messages.update(..., add_labels=[...], remove_labels=[...])` +- Lists: `client.lists.create(direction, type, entry=...)` where direction is "send"/"receive" and type is "allow"/"block" +- Drafts: `client.inboxes.drafts.create(inbox_id=..., to=[...], ...)` then `client.inboxes.drafts.send(inbox_id=..., draft_id=...)` + +## Reference + +- Linear ENG-323 has 24 KB article drafts. Use as reference but write the best version independently. +- Introduction page (`introduction.mdx`) uses `` components to link to all articles by category. +- Categories: Getting Started (4), Agent Patterns (6), Domains & Deliverability (5), Troubleshooting (4), DNS Guides (4) + ## Triggering Fern Writer (API changelog) The workflow does **not** post to Slack. It comments on the PR with the diff and uploads the `api-changelog-diff` artifact. To get a changelog draft: diff --git a/fern/docs.yml b/fern/docs.yml index 3690738..02c86c4 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -70,6 +70,9 @@ tabs: api: display-name: API Reference icon: terminal + kb: + display-name: Knowledge Base + icon: light book-open changelog: display-name: Changelog icon: light clock @@ -84,6 +87,9 @@ redirects: - source: /integrations/moltbot destination: /integrations/openclaw permanent: true + - source: /integrations/ai-onboarding + destination: /integrations/agent-onboarding + permanent: true # --- 404 redirects from crawl report --- @@ -239,9 +245,9 @@ navigation: path: pages/core-concepts/pods.mdx - section: Integrations contents: - - page: AI Onboarding + - page: Agent Onboarding icon: fa-solid fa-user-plus - path: pages/integrations/ai-onboarding.mdx + path: pages/integrations/agent-onboarding.mdx # - page: Overview # icon: fa-solid fa-list # path: pages/integrations/overview.mdx @@ -406,4 +412,91 @@ navigation: - websockets - lists - metrics + - api-keys + - pods + - organizations + - tab: kb + layout: + - page: Introduction + icon: fa-solid fa-book-open + path: pages/knowledge-base/introduction.mdx + - section: Getting Started + contents: + - page: What is AgentMail? + icon: fa-regular fa-circle-question + path: pages/knowledge-base/what-is-agentmail.mdx + - page: What can I do with an inbox? + icon: fa-solid fa-inbox + path: pages/knowledge-base/inbox-capabilities.mdx + - page: Creating your first inbox + icon: fa-solid fa-plus + path: pages/knowledge-base/creating-first-inbox.mdx + - page: Getting your API key + icon: fa-solid fa-key + path: pages/knowledge-base/getting-api-key.mdx + - section: Agent Patterns + contents: + - page: Handling inbound emails + icon: fa-solid fa-right-left + path: pages/knowledge-base/handling-inbound-emails.mdx + - page: Allowlists & blocklists + icon: fa-solid fa-filter + path: pages/knowledge-base/allowlists-blocklists.mdx + - page: Managing threaded conversations + icon: fa-regular fa-comments + path: pages/knowledge-base/threaded-conversations.mdx + - page: Human-in-the-loop workflows + icon: fa-solid fa-user-check + path: pages/knowledge-base/human-in-the-loop.mdx + - page: Pods for multi-tenant email + icon: fa-solid fa-users + path: pages/knowledge-base/pods-multi-tenant.mdx + - page: Using labels to track state + icon: fa-solid fa-tags + path: pages/knowledge-base/labels-track-state.mdx + - section: Domains & Deliverability + contents: + - page: Custom domain setup + icon: fa-solid fa-globe + path: pages/knowledge-base/custom-domain-setup.mdx + - page: SPF, DKIM, and DMARC setup + icon: fa-solid fa-shield-halved + path: pages/knowledge-base/spf-dkim-dmarc.mdx + - page: Emails going to spam + icon: fa-solid fa-triangle-exclamation + path: pages/knowledge-base/emails-going-to-spam.mdx + - page: Warming Up + icon: fa-solid fa-temperature-half + path: pages/knowledge-base/domain-warming.mdx + - page: MX record conflicts + icon: fa-solid fa-code-merge + path: pages/knowledge-base/mx-record-conflicts.mdx + - section: Troubleshooting + contents: + - page: Rate limits + icon: fa-solid fa-gauge-high + path: pages/knowledge-base/rate-limits.mdx + - page: Preventing duplicate sends + icon: fa-solid fa-clone + path: pages/knowledge-base/preventing-duplicate-sends.mdx + - page: Domain not verifying + icon: fa-solid fa-circle-xmark + path: pages/knowledge-base/domain-not-verifying.mdx + - page: Emails bouncing + icon: fa-solid fa-rotate-left + path: pages/knowledge-base/emails-bouncing.mdx + - section: DNS Guides + contents: + - page: Cloudflare + icon: fa-solid fa-cloud + path: pages/knowledge-base/dns-cloudflare.mdx + - page: GoDaddy + icon: fa-solid fa-globe + path: pages/knowledge-base/dns-godaddy.mdx + - page: Route 53 (AWS) + icon: fa-brands fa-aws + path: pages/knowledge-base/dns-route53.mdx + - page: Namecheap + icon: fa-solid fa-n + path: pages/knowledge-base/dns-namecheap.mdx - tab: changelog diff --git a/fern/pages/integrations/ai-onboarding.mdx b/fern/pages/integrations/agent-onboarding.mdx similarity index 97% rename from fern/pages/integrations/ai-onboarding.mdx rename to fern/pages/integrations/agent-onboarding.mdx index fbb6314..d6e69fd 100644 --- a/fern/pages/integrations/ai-onboarding.mdx +++ b/fern/pages/integrations/agent-onboarding.mdx @@ -1,7 +1,7 @@ --- -title: AI Onboarding +title: Agent Onboarding subtitle: Everything you need to onboard your AI agent to AgentMail -slug: ai-onboarding +slug: agent-onboarding description: >- Resources for AI coding assistants, MCP servers, skills, and agent-friendly documentation. --- @@ -250,7 +250,7 @@ AgentMail integrates with popular AI development platforms: ## What Makes AgentMail Different? -Unlike traditional email APIs (SendGrid, Resend, Mailgun) that are built for one-way transactional email, AgentMail is built for **two-way agent communication**: +Unlike traditional email APIs that are built for one-way transactional email, AgentMail is built for **two-way agent communication**: | Feature | AgentMail | Traditional Email APIs | | -- | -- | -- | diff --git a/fern/pages/knowledge-base/allowlists-blocklists.mdx b/fern/pages/knowledge-base/allowlists-blocklists.mdx new file mode 100644 index 0000000..4552365 --- /dev/null +++ b/fern/pages/knowledge-base/allowlists-blocklists.mdx @@ -0,0 +1,91 @@ +--- +title: "How do I set up allowlists and blocklists?" +subtitle: Control who your AI agent can send to and receive from. +slug: knowledge-base/allowlists-blocklists +--- + +Allowlists and blocklists let you control who your AI agent can communicate with. This is a critical safety feature for autonomous agents running in production with minimal human oversight. + +## How Lists work + +AgentMail provides four list types based on two dimensions, **direction** (send or receive) and **type** (allow or block): + +| List | What it does | +| --- | --- | +| Receive allow | Only accept emails from these addresses or domains | +| Receive block | Reject emails from these addresses or domains | +| Send allow | Only send emails to these addresses or domains | +| Send block | Prevent sending emails to these addresses or domains | + +Each entry can be either a full email address (e.g., `partner@example.com`) or an entire domain (e.g., `example.com`). + +## Setting up lists via the SDK + +### Add an entry + +```python title="Python" +from agentmail import AgentMail + +client = AgentMail() + +# Allow receiving from a specific domain +client.lists.create("receive", "allow", entry="trusted-corp.com") + +# Block a specific sender (with optional reason) +client.lists.create("receive", "block", entry="spam@example.com", reason="spam") + +# Restrict sending to only certain addresses +client.lists.create("send", "allow", entry="verified-prospect@example.com") + +# Prevent sending to a domain +client.lists.create("send", "block", entry="competitor.com") +``` + +### List entries + +```python title="Python" +# View all entries in a list +entries = client.lists.list("receive", "allow") +``` + +### Remove an entry + +```python title="Python" +client.lists.delete("receive", "block", entry="spam@example.com") +``` + +## Common patterns for agents + +**Outreach agent:** Use a send allowlist to restrict your agent to only email verified prospects. This prevents the agent from accidentally emailing the wrong people. + +```python title="Python" +# Only allow sending to verified prospects +for prospect in verified_prospects: + client.lists.create("send", "allow", entry=prospect.email) +``` + +**Personal Agent (Openclaw, Manus, etc.):** Use a receive allowlist to restrict your agent to only respond to emails from specific people or domains. + +```python title="Python" +# Only accept emails from your own address and trusted domains +client.lists.create("receive", "allow", entry="you@yourcompany.com") +client.lists.create("receive", "allow", entry="trusted-partner.com") +``` + +**Anti-spam:** Use a receive blocklist to filter out known spam senders or unwanted automated emails. + +```python title="Python" +# Block known spam domains +client.lists.create("receive", "block", entry="spam-domain.com", reason="spam") +``` + +## Why this matters for agents + +Without guardrails, an autonomous agent could email the wrong people, respond to phishing attempts, or get caught in infinite email loops with another bot. Lists are your safety rails. They are especially important for: + +- **Production agents** operating with minimal human oversight +- **Outreach agents** that should only contact approved recipients +- **Support agents** that should only respond to known customers +- **Any agent** that needs protection from spam, phishing, or abuse + +For more details on the Lists API, see the [Lists core concept](/lists) documentation. diff --git a/fern/pages/knowledge-base/creating-first-inbox.mdx b/fern/pages/knowledge-base/creating-first-inbox.mdx new file mode 100644 index 0000000..c384b3e --- /dev/null +++ b/fern/pages/knowledge-base/creating-first-inbox.mdx @@ -0,0 +1,96 @@ +--- +title: "How do I create my first inbox?" +subtitle: Get up and running with your first AgentMail inbox. +slug: knowledge-base/creating-first-inbox +--- + +Creating an inbox gives your AI agent its own email address. You can create inboxes on the default `@agentmail.to` domain or on your own custom domain. + +## Install the SDK + +```bash title="TypeScript" +npm install agentmail +``` + +## Create an inbox + +```typescript title="TypeScript" +import { AgentMailClient } from "agentmail"; + +const client = new AgentMailClient({ apiKey: "am_..." }); + +// Create an inbox with a random address on agentmail.to +const inbox = await client.inboxes.create(); +console.log(`Inbox created: ${inbox.inboxId}`); +``` + +The inbox now has a unique email address (e.g., `randomname@agentmail.to`) and can send and receive emails immediately. + +## Customize your inbox + +You can specify a username, domain, and display name: + +```typescript title="TypeScript" +const inbox = await client.inboxes.create({ + username: "support", + domain: "yourcompany.com", + displayName: "Support Agent", +}); + +// inbox.inboxId will be support@yourcompany.com +console.log(`Inbox created: ${inbox.inboxId}`); +``` + + +Using a custom domain requires a verified domain. See the [Creating Custom Domains](/custom-domains) guide to set one up. If you don't specify a domain, AgentMail uses the default `@agentmail.to` domain. + + +## Use client_id for idempotency + +If your agent creates inboxes programmatically (e.g., on startup), use `clientId` to prevent duplicates. If an inbox with the same `clientId` already exists, AgentMail returns the existing inbox instead of creating a new one: + +```typescript title="TypeScript" +const inbox = await client.inboxes.create({ + username: "my-agent", + clientId: "my-agent-inbox-v1", + displayName: "My Agent", +}); + +// Safe to call multiple times: same inbox returned every time +``` + +## Send your first email + +Once the inbox is created, you can send an email: + +```typescript title="TypeScript" +await client.inboxes.messages.send(inbox.inboxId, { + to: "recipient@example.com", + subject: "Hello from my agent!", + text: "This is a plain text version.", + html: "

This is an HTML version.

", +}); +``` + + +Always provide both `text` and `html` when sending emails. This ensures readability across all email clients and improves deliverability. + + +## List your inboxes + +```typescript title="TypeScript" +const inboxes = await client.inboxes.list(); +console.log(`You have ${inboxes.count} inboxes.`); + +for (const inbox of inboxes.inboxes) { + console.log(` ${inbox.inboxId}`); +} +``` + +## Next steps + +Now that you have an inbox, explore what you can do with it: + +- [Send and receive emails](/sending-receiving-email) in a conversational loop +- [Set up webhooks](/webhook-setup) to get notified when emails arrive +- [Use labels](/labels) to track message state in your agent workflows diff --git a/fern/pages/knowledge-base/custom-domain-setup.mdx b/fern/pages/knowledge-base/custom-domain-setup.mdx new file mode 100644 index 0000000..36f0170 --- /dev/null +++ b/fern/pages/knowledge-base/custom-domain-setup.mdx @@ -0,0 +1,95 @@ +--- +title: "How do I set up a custom domain?" +subtitle: Send emails from your own domain instead of @agentmail.to. +slug: knowledge-base/custom-domain-setup +--- + +Custom domains let your agent send emails from your brand (e.g., `agent@yourcompany.com`) instead of the default `@agentmail.to`. This improves deliverability and builds trust with recipients. + + +Custom domains are available on the **Developer plan and above**. The free tier uses `@agentmail.to` only. See the [pricing page](https://agentmail.to/pricing) for details. + + +## Steps + +1. Add your domain in the [AgentMail Console](https://console.agentmail.to) or via the API +2. Add the DNS records AgentMail provides to your DNS provider +3. Wait for verification +4. Create inboxes on your custom domain + +## Adding a domain + +You can add a domain through the [AgentMail Console](https://console.agentmail.to) (go to **Domains** and click **Add Domain**) or via the API: + +```typescript title="TypeScript" +import { AgentMailClient } from "agentmail"; + +const client = new AgentMailClient({ apiKey: "am_..." }); + +// Add your domain +const domain = await client.domains.create("yourcompany.com"); + +// View the DNS records you need to add +for (const record of domain.records) { + console.log(`${record.type} | ${record.name} | ${record.value}`); +} +``` + +The response includes all the DNS records you need to add at your DNS provider. + +## DNS records you need to add + +| Record type | Purpose | +| --- | --- | +| TXT (SPF) | Authorizes AgentMail to send email on behalf of your domain | +| CNAME (DKIM) | Cryptographic signature proving emails are authentically from you | +| MX | Routes incoming mail for your domain to AgentMail | + + +The MX record is only needed if you want to **receive** emails on your custom domain. If you only need to send, you can skip the MX record. + + +For step-by-step DNS setup instructions, see our provider guides: +[Cloudflare](/knowledge-base/dns-cloudflare), [GoDaddy](/knowledge-base/dns-godaddy), [Route 53](/knowledge-base/dns-route53), [Namecheap](/knowledge-base/dns-namecheap). + +## Verifying your domain + +After adding DNS records, verify your domain: + +```typescript title="TypeScript" +await client.domains.verify(domain.domainId); +``` + +You can also verify from the [AgentMail Console](https://console.agentmail.to) by navigating to the Domains section and clicking **Verify Domain**. + +Verification status will progress through these stages: + +| Status | Meaning | +| --- | --- | +| `NOT_STARTED` | You need to click Verify Domain to start the process | +| `PENDING` | DNS records still need to be added or fixed | +| `INVALID` | Some records are misconfigured; double check the values | +| `VERIFYING` | DNS records are correct and authorization is in progress | +| `VERIFIED` | Domain is ready for sending and receiving | + +## Creating inboxes on your domain + +Once verified, you can create inboxes using your custom domain: + +```typescript title="TypeScript" +const inbox = await client.inboxes.create({ + username: "support", + domain: "yourcompany.com", + displayName: "Support Agent", +}); + +console.log(`Created: ${inbox.inboxId}`); +// support@yourcompany.com +``` + +## Tips + +- **Use a subdomain** (e.g., `mail.yourcompany.com`) if you don't want to modify your root domain's MX records or risk conflicts with existing email services +- **Verification time** varies by DNS provider, from a few minutes (Cloudflare, Route 53) to 30 minutes or more (GoDaddy, Namecheap) +- **One SPF record per domain:** if you already have an SPF record, merge AgentMail's `include:` into the existing record rather than creating a second one +For a detailed walkthrough, see the [Creating Custom Domains](/custom-domains) guide. diff --git a/fern/pages/knowledge-base/dns-cloudflare.mdx b/fern/pages/knowledge-base/dns-cloudflare.mdx new file mode 100644 index 0000000..d02d34d --- /dev/null +++ b/fern/pages/knowledge-base/dns-cloudflare.mdx @@ -0,0 +1,71 @@ +--- +title: "DNS Guide: Cloudflare" +subtitle: Step-by-step instructions for adding AgentMail DNS records in Cloudflare. +slug: knowledge-base/dns-cloudflare +--- + +## Steps + +1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com) +2. Select your domain +3. Go to **DNS**, then click **Records** +4. Click **Add Record** for each record AgentMail requires + +## Adding a TXT Record (SPF) + +| Field | Value | +| --- | --- | +| Type | `TXT` | +| Name | `@` (or your subdomain) | +| Content | The SPF value from AgentMail (e.g., `v=spf1 include:agentmail.to ~all`) | +| TTL | `Auto` | + + +If you already have an SPF record for your domain, do **not** create a second one. Instead, add `include:agentmail.to` to your existing SPF record. Having multiple SPF records on the same domain will cause authentication failures. + + +## Adding a CNAME Record (DKIM) + +| Field | Value | +| --- | --- | +| Type | `CNAME` | +| Name | The DKIM selector from AgentMail (e.g., `agentmail._domainkey`) | +| Target | The DKIM target from AgentMail | +| Proxy status | **DNS only** (grey cloud) | +| TTL | `Auto` | + + +DKIM CNAME records **must** have Cloudflare proxy disabled (grey cloud icon). If the orange cloud proxy is enabled, Cloudflare replaces the CNAME with its own proxy addresses, which prevents email servers from looking up your DKIM public key. This will cause DKIM verification to fail. If you see Error 1004 when saving, confirm that proxy is set to DNS only. + + +Cloudflare automatically strips your domain from the Name field. Enter only the subdomain portion (e.g., `agentmail._domainkey`), not the full `agentmail._domainkey.yourdomain.com`. + +## Adding an MX Record (Receiving) + +| Field | Value | +| --- | --- | +| Type | `MX` | +| Name | `@` (or your subdomain) | +| Mail server | The MX target from AgentMail | +| Priority | `10` | +| TTL | `Auto` | + +If you want to receive emails on a subdomain to avoid conflicts with your existing email provider, enter the subdomain instead of `@` in the Name field. + +## Verification + +After adding all records, go back to the [AgentMail Console](https://console.agentmail.to) and click **Verify Domain**. + +Cloudflare DNS typically propagates within **1 to 5 minutes**, making it one of the fastest providers for DNS updates. + +## Common Cloudflare Issues + +- **Proxy must be off for CNAME records:** The orange cloud (Proxied) means traffic goes through Cloudflare's reverse proxy, which only handles HTTP/HTTPS. Email-related CNAME records like DKIM need direct DNS resolution and must be set to DNS only (grey cloud). If you forget, DKIM verification will silently fail. + +- **CNAME flattening at root:** Cloudflare automatically flattens CNAME records at the zone apex (root domain), returning A/AAAA records instead of the CNAME. This generally does not affect DKIM setup since the selector (e.g., `agentmail._domainkey`) is a subdomain. However, if you encounter unexpected behavior, consider using a subdomain for sending. + +- **Existing SPF record:** If you already have a TXT record starting with `v=spf1`, add `include:agentmail.to` before the `~all` or `-all` in that existing record. Do not create a second SPF TXT record. + +- **Error 1004 when adding CNAME:** This error typically means the record cannot be proxied. Switch the proxy status to DNS only (grey cloud) and try saving again. + +- **Name field auto-strips domain:** Cloudflare removes the domain portion from the Name field automatically. If your DKIM selector is `agentmail._domainkey`, enter just that. Do not enter `agentmail._domainkey.yourdomain.com`, or the record will be created incorrectly. diff --git a/fern/pages/knowledge-base/dns-godaddy.mdx b/fern/pages/knowledge-base/dns-godaddy.mdx new file mode 100644 index 0000000..0b128d0 --- /dev/null +++ b/fern/pages/knowledge-base/dns-godaddy.mdx @@ -0,0 +1,68 @@ +--- +title: "DNS Guide: GoDaddy" +subtitle: Step-by-step instructions for adding AgentMail DNS records in GoDaddy. +slug: knowledge-base/dns-godaddy +--- + +## Steps + +1. Log in to your [GoDaddy account](https://www.godaddy.com) +2. Go to **My Products**, find your domain, and click **DNS** +3. Scroll down to the **DNS Records** section +4. Click **Add** to create each record AgentMail requires + +## Adding a TXT Record (SPF) + +| Field | Value | +| --- | --- | +| Type | `TXT` | +| Name | `@` (or your subdomain) | +| Value | The SPF value from AgentMail (e.g., `v=spf1 include:agentmail.to ~all`) | +| TTL | `1 Hour` | + + +GoDaddy automatically wraps TXT values in double quotes, so do **not** add your own quotes around the value. If you do, the record will end up double-quoted and fail validation. Also, if you already have an SPF record, add `include:agentmail.to` to the existing record rather than creating a second one. + + +## Adding a CNAME Record (DKIM) + +| Field | Value | +| --- | --- | +| Type | `CNAME` | +| Name | The DKIM selector from AgentMail (e.g., `agentmail._domainkey`) | +| Value | The DKIM target from AgentMail | +| TTL | `1 Hour` | + + +GoDaddy automatically appends your domain to the Name field. Enter only the subdomain portion. For example, enter `agentmail._domainkey`, not `agentmail._domainkey.yourdomain.com`. + + +## Adding an MX Record (Receiving) + +| Field | Value | +| --- | --- | +| Type | `MX` | +| Name | `@` (or your subdomain) | +| Value | The MX target from AgentMail | +| Priority | `10` | +| TTL | `1 Hour` | + +If you want to receive emails on a subdomain to avoid conflicts with your existing email provider, use the subdomain instead of `@` in the Name field. + +## Verification + +After adding all records, go back to the [AgentMail Console](https://console.agentmail.to) and click **Verify Domain**. + +GoDaddy DNS typically propagates within **15 to 30 minutes**, but it can occasionally take longer. If verification does not succeed right away, wait and try again. + +## Common GoDaddy Issues + +- **Double-quoted TXT values:** GoDaddy adds quotes around TXT records automatically. If you paste a value that already includes quotes, the final record will be double-quoted and will fail. Paste the raw value without quotes. + +- **Name field auto-appends domain:** GoDaddy appends your domain to the Name field. Enter only the subdomain portion (e.g., `agentmail._domainkey`). If you enter the full hostname, it will be duplicated as `agentmail._domainkey.yourdomain.com.yourdomain.com`. + +- **Existing SPF record:** If you already have a TXT record starting with `v=spf1`, add `include:agentmail.to` before the `~all` or `-all` in that existing record. Do not create a second SPF TXT record. + +- **DNS managed elsewhere:** If you have changed your nameservers away from GoDaddy (e.g., to Cloudflare or Route 53), records added in the GoDaddy DNS panel will have no effect. Add records at the provider that controls your nameservers instead. + +- **Parked domain or forwarding:** If your domain is parked or has forwarding enabled, GoDaddy may override your MX records. Disable parking and forwarding before setting up email receiving. diff --git a/fern/pages/knowledge-base/dns-namecheap.mdx b/fern/pages/knowledge-base/dns-namecheap.mdx new file mode 100644 index 0000000..0e41f72 --- /dev/null +++ b/fern/pages/knowledge-base/dns-namecheap.mdx @@ -0,0 +1,70 @@ +--- +title: "DNS Guide: Namecheap" +subtitle: Step-by-step instructions for adding AgentMail DNS records in Namecheap. +slug: knowledge-base/dns-namecheap +--- + +## Steps + +1. Log in to your [Namecheap account](https://www.namecheap.com) +2. Go to **Domain List** → click **Manage** next to your domain +3. Select the **Advanced DNS** tab +4. Click **Add New Record** for each record AgentMail requires + + +Make sure your domain is using **Namecheap BasicDNS** or **Namecheap PremiumDNS** as its nameserver. If you've pointed your nameservers to another provider (e.g., Cloudflare), you'll need to add records there instead. + + +## Adding a TXT Record (SPF) + +| Field | Value | +| --- | --- | +| Type | `TXT Record` | +| Host | `@` (or your subdomain) | +| Value | The SPF value from AgentMail (e.g., `v=spf1 include:agentmail.to ~all`) | +| TTL | `Automatic` | + + +If you already have an SPF record for your domain, do **not** create a second one. Instead, add `include:agentmail.to` to your existing SPF record. Having multiple SPF records will cause validation failures. + + +## Adding a CNAME Record (DKIM) + +| Field | Value | +| --- | --- | +| Type | `CNAME Record` | +| Host | The DKIM selector from AgentMail (e.g., `agentmail._domainkey`) | +| Value | The DKIM target from AgentMail | +| TTL | `Automatic` | + + +**Namecheap automatically appends your domain to the Host field.** Enter only the subdomain portion. For example, enter `agentmail._domainkey`, not `agentmail._domainkey.yourdomain.com`. + + +## Adding an MX Record (Receiving) + +| Field | Value | +| --- | --- | +| Type | `MX Record` | +| Host | `@` (or your subdomain) | +| Value | The MX target from AgentMail | +| Priority | `10` | +| TTL | `Automatic` | + +If you want to receive emails on a subdomain to avoid conflicts with your existing email provider, use the subdomain (e.g., `mail`) instead of `@` in the Host field. + +## Verification + +After adding all records, go back to the [AgentMail Console](https://console.agentmail.to) and click **Verify Domain**. + +Namecheap DNS typically propagates within **15 to 30 minutes**, but in some cases it can take up to a few hours. If verification doesn't succeed immediately, wait and try again. + +## Common Namecheap Issues + +- **Host field auto-appends domain:** Namecheap adds your domain automatically. If your DKIM selector is `agentmail._domainkey`, enter just that, not the full `agentmail._domainkey.yourdomain.com`. If you enter the full value, the actual record will end up as `agentmail._domainkey.yourdomain.com.yourdomain.com`. + +- **Existing SPF record:** If you already have a TXT record starting with `v=spf1`, add `include:agentmail.to` before the `~all` or `-all` in that existing record. Do not create a second SPF TXT record. + +- **Wrong nameserver:** If your domain uses **Custom DNS** nameservers (e.g., pointing to Cloudflare or Route 53), records added in the Namecheap Advanced DNS panel will have no effect. Add your records at the provider that controls your nameservers instead. + +- **Propagation delays:** Unlike some providers, Namecheap propagation can take 15 to 30 minutes. If verification fails, wait and retry before troubleshooting further. diff --git a/fern/pages/knowledge-base/dns-route53.mdx b/fern/pages/knowledge-base/dns-route53.mdx new file mode 100644 index 0000000..5a8568c --- /dev/null +++ b/fern/pages/knowledge-base/dns-route53.mdx @@ -0,0 +1,71 @@ +--- +title: "DNS Guide: Route 53 (AWS)" +subtitle: Step-by-step instructions for adding AgentMail DNS records in AWS Route 53. +slug: knowledge-base/dns-route53 +--- + +## Steps + +1. Log in to the [AWS Management Console](https://console.aws.amazon.com) +2. Navigate to **Route 53**, then select **Hosted Zones** +3. Click on your domain's hosted zone +4. Click **Create Record** for each record AgentMail requires + + +If your domain is registered with a different registrar but uses Route 53 for DNS, make sure the NS records at your registrar match the name servers listed in your hosted zone. + + +## Adding a TXT Record (SPF) + +| Field | Value | +| --- | --- | +| Record name | Leave blank for root domain, or enter your subdomain | +| Record type | `TXT` | +| Value | `"v=spf1 include:agentmail.to ~all"` | +| TTL | `300` | + + +Route 53 requires TXT values to be **wrapped in double quotes**. If you omit the quotes, the record will fail validation. Also, if you already have an SPF record, add `include:agentmail.to` to the existing record rather than creating a second one. Multiple SPF records on the same domain will cause authentication failures. + + +## Adding a CNAME Record (DKIM) + +| Field | Value | +| --- | --- | +| Record name | The DKIM selector from AgentMail (e.g., `agentmail._domainkey`) | +| Record type | `CNAME` | +| Value | The DKIM target from AgentMail | +| TTL | `300` | + +CNAME values should **not** be wrapped in quotes. + +## Adding an MX Record (Receiving) + +| Field | Value | +| --- | --- | +| Record name | Leave blank for root domain, or enter your subdomain | +| Record type | `MX` | +| Value | `10 inbound.agentmail.to` | +| TTL | `300` | + +Route 53 MX records use the format `priority server` separated by a space (e.g., `10 inbound.agentmail.to`). Do not wrap MX values in quotes. + +If you want to receive emails on a subdomain to avoid conflicts with your existing email provider, enter the subdomain in the Record name field instead of leaving it blank. + +## Verification + +After adding all records, go back to the [AgentMail Console](https://console.agentmail.to) and click **Verify Domain**. + +Route 53 name servers typically pick up changes within **60 seconds**, but full propagation to all DNS resolvers may take longer depending on TTL and resolver caching. In practice, most changes are visible within a few minutes. + +## Common Route 53 Issues + +- **TXT records must be quoted:** Unlike most DNS providers, Route 53 requires double quotes around TXT record values. If your SPF or other TXT records are missing quotes, they won't validate. + +- **CNAME at root domain:** Route 53 does not allow CNAME records on the root domain (zone apex). If you need to set up DKIM, the selector (e.g., `agentmail._domainkey`) is a subdomain, so this is typically not an issue. However, if you run into conflicts, consider using a subdomain for sending. + +- **Existing SPF record:** If you already have a TXT record starting with `v=spf1`, add `include:agentmail.to` before the `~all` or `-all` in that existing record. Do not create a second SPF TXT record. + +- **Routing policy:** When creating records, use **Simple routing** unless you have a specific reason to use weighted, latency, or other routing policies. Other policies can cause unexpected DNS behavior for email records. + +- **Multiple values in one record:** Route 53 lets you add multiple values to a single record. If you need to add a second MX entry, add it as a new line in the same MX record rather than creating a separate record. diff --git a/fern/pages/knowledge-base/domain-not-verifying.mdx b/fern/pages/knowledge-base/domain-not-verifying.mdx new file mode 100644 index 0000000..4d39b88 --- /dev/null +++ b/fern/pages/knowledge-base/domain-not-verifying.mdx @@ -0,0 +1,75 @@ +--- +title: "Why is my domain not verifying?" +subtitle: What to do when your domain verification is stuck. +slug: knowledge-base/domain-not-verifying +--- + +If your domain is stuck in a pending or failed verification state, work through these common causes. + +## 1. DNS propagation delay + +DNS changes can take anywhere from a few minutes to 48 hours to propagate, depending on your provider. Most providers propagate within 30 minutes, but some take longer. + +**What to do:** Wait 30 minutes, then click **Verify Domain** in the [AgentMail Console](https://console.agentmail.to) to trigger a recheck. You can also use [dnschecker.org](https://dnschecker.org) to confirm your records are visible globally before retrying. + +| DNS provider | Typical propagation time | +| --- | --- | +| Cloudflare | 1 to 5 minutes | +| Route 53 | 1 to 5 minutes | +| GoDaddy | 15 to 30 minutes | +| Namecheap | 15 to 30 minutes | + +## 2. Wrong record type + +Make sure you are adding the correct record type as specified by AgentMail. A common mistake is adding a TXT record where a CNAME is needed, or vice versa. + +| Record | Correct type | +| --- | --- | +| SPF | TXT | +| DKIM | CNAME | +| MX | MX | + +Double check the records in the AgentMail Console under **Domains** to see the exact type and value required. + +## 3. Extra quotes or spaces + +Some DNS providers (like GoDaddy) automatically wrap TXT values in quotes. If you paste a value that already includes quotes, you end up with double-quoted values that fail verification. + +**What to do:** Paste the value exactly as provided by AgentMail. If your DNS provider adds quotes automatically, do not add your own. + +Route 53 is the opposite: it **requires** quotes around TXT values. If you omit them, the record will not validate. + +## 4. Domain name auto-appended + +Many DNS providers (Namecheap, GoDaddy, Cloudflare) automatically append your domain to the record name. If you enter `agentmail._domainkey.yourcompany.com` instead of just `agentmail._domainkey`, the actual record becomes `agentmail._domainkey.yourcompany.com.yourcompany.com`, which will fail. + +**What to do:** Enter only the subdomain portion. For example, enter `agentmail._domainkey`, not the full hostname. + +## 5. Conflicting records + +CNAME records cannot coexist with other record types on the same name. If you have an existing TXT or A record on the same subdomain as your DKIM CNAME, verification may fail. + +**What to do:** Check your DNS dashboard for conflicts on the DKIM selector subdomain and remove any conflicting records. + +## 6. Nameservers pointing elsewhere + +If your domain's nameservers are pointed to a different provider than where you are adding records, the records will have no effect. For example, if your domain uses Cloudflare nameservers but you add records in the Namecheap panel, those records are ignored. + +**What to do:** Add your DNS records at the provider that actually controls your nameservers. + +## Verification statuses + +You can check your domain's status in the [AgentMail Console](https://console.agentmail.to): + +| Status | Meaning | +| --- | --- | +| `NOT_STARTED` | Click Verify Domain to begin | +| `PENDING` | DNS records need to be added or corrected | +| `INVALID` | Records are misconfigured; review the values | +| `FAILED` | Records look correct but need a manual recheck; click Verify Domain again | +| `VERIFYING` | Records are correct, authorization in progress | +| `VERIFIED` | Domain is ready | + +## Still not working? + +If you have checked all of the above and your domain is still not verifying, email [support@agentmail.cc](mailto:support@agentmail.cc) with your domain name and a screenshot of your DNS records. We can help diagnose the issue. diff --git a/fern/pages/knowledge-base/domain-warming.mdx b/fern/pages/knowledge-base/domain-warming.mdx new file mode 100644 index 0000000..cc6b995 --- /dev/null +++ b/fern/pages/knowledge-base/domain-warming.mdx @@ -0,0 +1,78 @@ +--- +title: "Warming Up" +subtitle: Gradually build sending reputation on a new domain or inbox. +slug: knowledge-base/domain-warming +--- + +Warming up is the process of gradually increasing your email volume on a new domain to build sender reputation with mailbox providers like Gmail, Outlook, and Yahoo. +You can connect AgentMail inboxes to Instantly and Smartlead for programmatic warm up using [SMTP credentials](https://docs.agentmail.to/imap-smtp#finding-your-credentials). + +## Why warming matters + +Mailbox providers track sender reputation per domain. A brand new domain has zero reputation. If you send thousands of emails on day one, providers will flag your domain as suspicious and route your emails to spam. + +By starting slow and increasing volume gradually, you signal that you are a legitimate sender with engaged recipients. + +## Warmup schedule + +| Day | Daily volume | What to send | +| --- | --- | --- | +| 1 to 3 | 10 to 20 | Send to your most engaged recipients (people likely to open and reply) | +| 4 to 7 | 50 to 100 | Expand to known, verified addresses | +| 8 to 14 | 200 to 500 | Broader audience; monitor bounce rates closely | +| 15 to 21 | 500 to 1,000 | Continue scaling; watch for spam complaints | +| 22 to 30 | 1,000 to 5,000 | Approaching full volume | +| 30+ | Full volume | Maintain healthy engagement metrics | + + +If your bounce rate exceeds 5% or you see a spike in spam complaints at any stage, slow down immediately. Continuing to send at high volume with poor metrics will damage your domain reputation. + + +## Tips for warming with agents + +**Spread sends across multiple inboxes.** Instead of sending 100 emails from 1 inbox, send 10 emails each from 10 inboxes on the same domain. AgentMail makes it easy to create many inboxes, and this approach distributes volume naturally while warming the domain faster. + +```typescript title="TypeScript" +import { AgentMailClient } from "agentmail"; + +const client = new AgentMailClient({ apiKey: "am_..." }); + +// Create multiple inboxes on your domain +const inboxes = []; +for (let i = 0; i < 10; i++) { + const inbox = await client.inboxes.create({ + username: `agent-${i}`, + domain: "yourcompany.com", + clientId: `warmup-agent-${i}`, + }); + inboxes.push(inbox); +} + +// Distribute sends across inboxes +for (const [index, recipient] of recipients.entries()) { + const inbox = inboxes[index % inboxes.length]; + await client.inboxes.messages.send(inbox.inboxId, { + to: recipient, + subject: "Hello", + text: "Plain text version", + html: "

HTML version

", + }); +} +``` + +**Start with replies, not cold outreach.** If possible, have your agent respond to inbound emails first. Replies build reputation much faster than cold sends because they demonstrate two-way engagement. + +**Use multiple domains and subdomains for high volume.** Register multiple domains or use subdomains (e.g., `mail.yourcompany.com`, `outreach.yourcompany.com`) and warm each one separately. This protects your primary domain's reputation if one sending domain gets flagged. + +**Only send to verified addresses.** During warmup, never send to purchased lists or unverified addresses. High bounce rates during the warmup period will severely damage your new domain's reputation. + +## Key metrics to monitor + +| Metric | Healthy range | Action if outside range | +| --- | --- | --- | +| Bounce rate | Under 2% | Pause and clean your recipient list | +| Spam complaint rate | Under 0.1% | Reduce volume and review content | + +You can track bounces and complaints by using [Query Metrics](https://docs.agentmail.to/api-reference/metrics/query) in AgentMail's API or by viewing the Metrics Page on the console. + +For more deliverability best practices, see the [Email Deliverability](/email-deliverability) guide. diff --git a/fern/pages/knowledge-base/emails-bouncing.mdx b/fern/pages/knowledge-base/emails-bouncing.mdx new file mode 100644 index 0000000..2629f1b --- /dev/null +++ b/fern/pages/knowledge-base/emails-bouncing.mdx @@ -0,0 +1,88 @@ +--- +title: "Why are my emails bouncing?" +subtitle: Diagnose and resolve email bounce issues. +slug: knowledge-base/emails-bouncing +--- + +A bounced email means the recipient's mail server rejected your message. Understanding the bounce type helps you take the right action. + +## Bounce types + +### Permanent bounces + +The address is permanently unreachable. Common causes: + +- The email address does not exist +- The domain does not exist +- The recipient's mailbox has been deleted + +**Action:** Remove permanently bounced addresses from your sending lists immediately. Continuing to send to them will damage your sender reputation. + +### Transient bounces + +Temporary delivery failure. Common causes: + +- Recipient's mailbox is full +- Recipient's mail server is temporarily unavailable +- Message is too large +- Greylisting (server temporarily rejects first-time senders) + +**Action:** AgentMail automatically retries transient bounces. If delivery fails after multiple attempts, the bounce is treated as permanent. + +## Monitoring bounces with webhooks + +Subscribe to the `message.bounced` webhook event to track bounces in real time: + +```typescript title="TypeScript" +import { serialization } from "agentmail"; + +async function handleWebhook(payload: Record) { + if (payload.event_type === "message.bounced") { + const event = await serialization.events.MessageBouncedEvent.parse(payload); + + console.log(`Bounce type: ${event.bounce.type}`); // "Permanent" or "Transient" + console.log(`Sub type: ${event.bounce.subType}`); // e.g., "General" + console.log(`Message: ${event.bounce.messageId}`); + + for (const recipient of event.bounce.recipients) { + console.log(`Bounced address: ${recipient.address}`); + + if (event.bounce.type === "Permanent") { + // Remove from your sending lists + await removeFromList(recipient.address); + } + } + } +} +``` + +Register a webhook to receive bounce events: + +```typescript title="TypeScript" +import { AgentMailClient } from "agentmail"; + +const client = new AgentMailClient({ apiKey: "am_..." }); + +await client.webhooks.create({ + url: "https://your-domain.ngrok-free.app/webhooks", + events: ["message.bounced"], +}); +``` + +## Automatic suppression + + +AgentMail automatically prevents you from sending to addresses that have previously bounced, been rejected, or filed a spam complaint. This protects your sender reputation. Keep your account bounce rate under 4%, otherwise your account may be placed under review. + + +## Keeping bounce rates low + +| Practice | Why it matters | +| --- | --- | +| Validate addresses before sending | Catches typos and nonexistent addresses before they bounce | +| Remove permanent bounces immediately | Repeated sends to invalid addresses damage reputation fast | +| Avoid purchased or scraped lists | These lists have high rates of invalid addresses | +| Warm up new domains gradually | Sudden high volume from a new domain triggers suspicion | +| Monitor bounce rate | Keep it under 2% for healthy reputation; under 4% to avoid account review | + +For more on maintaining healthy sending metrics, see the [Email Deliverability](/email-deliverability) best practices. diff --git a/fern/pages/knowledge-base/emails-going-to-spam.mdx b/fern/pages/knowledge-base/emails-going-to-spam.mdx new file mode 100644 index 0000000..730c095 --- /dev/null +++ b/fern/pages/knowledge-base/emails-going-to-spam.mdx @@ -0,0 +1,68 @@ +--- +title: "Why are my emails going to spam?" +subtitle: Troubleshoot and fix spam folder placement issues. +slug: knowledge-base/emails-going-to-spam +--- + +If your agent's emails are landing in spam instead of the inbox, work through these common causes in order. The most frequent issues are at the top. + +## 1. Missing or incorrect DNS records + +This is the most common cause. SPF, DKIM, and DMARC must all be configured correctly for receiving servers to trust your emails. + +**How to check:** Go to the [AgentMail Console](https://console.agentmail.to), navigate to **Domains**, and verify that all records show as verified. + +If any records are missing or misconfigured, see the [SPF, DKIM, and DMARC setup](/knowledge-base/spf-dkim-dmarc) guide. + +## 2. New domain without warmup + +New domains have zero sender reputation. If you start sending hundreds of emails immediately, providers will flag your domain as suspicious. + +**Fix:** Warm up gradually: + +| Timeframe | Volume | +| --- | --- | +| Week 1 | 10 to 20 emails per day | +| Week 2 | 50 to 100 emails per day | +| Week 3 | Scale up based on engagement | + +Maintain low bounce rates and low spam complaint rates throughout. See the [domain warming schedule](/knowledge-base/domain-warming) for a detailed plan. + +## 3. Sending HTML without a text version + +Always include both `html` and `text` versions of your email. Spam filters flag emails that contain only HTML with no plain text fallback. + +```typescript title="TypeScript" +await client.inboxes.messages.send(inbox.inboxId, { + to: "user@example.com", + subject: "Hello", + text: "Plain text version of your email", + html: "

HTML version of your email

", +}); +``` + +## 4. Spammy content + +Email providers use content analysis to flag spam. Avoid these patterns in your subject lines and body: + +- ALL CAPS subjects +- Excessive exclamation marks +- Trigger words like "FREE", "BUY NOW", "ACT NOW", "URGENT" +- Too many links in a single email +- Large images with very little text + +## 5. High bounce rate + +Sending to invalid email addresses damages your sender reputation quickly. If too many emails bounce, providers start sending your future emails to spam. + +**Fix:** Validate email addresses before sending. Remove bounced addresses from your lists immediately. You can track bounces using the `message.bounced` webhook event. + +## 6. Using @agentmail.to for production + +The shared `@agentmail.to` domain is great for testing, but it has shared reputation across all users. For production sending, consider using a [custom domain](/knowledge-base/custom-domain-setup) with proper DNS authentication. + +## Still landing in spam? + +If you have checked all of the above and emails are still going to spam, reach out in our [Discord](https://discord.com/invite/hTYatWYWBc) support channel. + +For a comprehensive guide to maximizing deliverability, see the [Email Deliverability](/email-deliverability) best practices. \ No newline at end of file diff --git a/fern/pages/knowledge-base/getting-api-key.mdx b/fern/pages/knowledge-base/getting-api-key.mdx new file mode 100644 index 0000000..0fcb9ff --- /dev/null +++ b/fern/pages/knowledge-base/getting-api-key.mdx @@ -0,0 +1,58 @@ +--- +title: "How do I get my API key?" +subtitle: Create and manage your AgentMail API keys. +slug: knowledge-base/getting-api-key +--- + +You need an API key to authenticate requests to the AgentMail API. API keys start with `am_` and are created in the AgentMail Console. + +## Steps + +1. Go to the [AgentMail Console](https://console.agentmail.to) +2. Sign up or log in +3. Navigate to the **API Keys** section in the left side of the dashboard +4. Click **Create New API Key** +5. Give it a descriptive name (e.g., "my-support-agent") +6. Copy the key immediately and store it securely + + +Your API key is only shown once when it is created. If you lose it, you will need to create a new one. Never commit API keys to version control or share them publicly. + + +## Using your API key + +Store your API key in an environment variable to keep it out of your source code: + +```bash title="Bash" +# .env file +AGENTMAIL_API_KEY=am_your_key_here +``` + +Then initialize the SDK using the environment variable: + +```typescript title="TypeScript" +import { AgentMailClient } from "agentmail"; +import "dotenv/config"; + +const client = new AgentMailClient({ + apiKey: process.env.AGENTMAIL_API_KEY, +}); + +// Test the connection by creating an inbox +const inbox = await client.inboxes.create(); +console.log(`Connected! Created inbox: ${inbox.inboxId}`); +``` + +## Free tier + +No credit card required to get started. The free tier includes: + +| Feature | Limit | +| --- | --- | +| Inboxes | 3 | +| Emails per month | 3,000 | +| Storage | 3 GB | +| API access | Full (REST, WebSockets, webhooks) | +| SDKs | Python and TypeScript | + +To create more inboxes or send higher volumes, see the [pricing page](https://agentmail.to/pricing). diff --git a/fern/pages/knowledge-base/handling-inbound-emails.mdx b/fern/pages/knowledge-base/handling-inbound-emails.mdx new file mode 100644 index 0000000..869518a --- /dev/null +++ b/fern/pages/knowledge-base/handling-inbound-emails.mdx @@ -0,0 +1,107 @@ +--- +title: "How do I handle inbound emails with my agent?" +subtitle: Compare Webhooks and WebSockets for processing incoming emails. +slug: knowledge-base/handling-inbound-emails +--- + +AgentMail offers two ways to process incoming emails, each suited to different use cases. + +## 1. Webhooks (Recommended for Production) + +Configure a webhook URL and AgentMail will send a POST request to your endpoint whenever an email arrives. This is the most reliable approach for production applications. + +```python title="Python" +from flask import Flask, request +from agentmail import AgentMail + +app = Flask(__name__) +client = AgentMail() + +@app.route("/webhooks", methods=["POST"]) +def handle_webhook(): + payload = request.json + + if payload["event_type"] == "message.received": + message = payload["message"] + + # Your agent processes the email here + reply_text = your_agent.process(message) + + # Reply in the same thread + client.inboxes.messages.reply( + inbox_id=message["inbox_id"], + message_id=message["message_id"], + text=reply_text + ) + + return "OK", 200 +``` + +Register your webhook via the API: + +```python title="Python" +client.webhooks.create( + url="https://your-domain.ngrok-free.app/webhooks", + events=["message.received"], +) +``` + + +Always return a `200 OK` immediately and process the webhook in the background. If your endpoint takes too long to respond, AgentMail will retry delivery. Also, filter out `message.sent` events to prevent your agent from replying to its own messages in a loop. + + +For local development, use [ngrok](https://ngrok.com) to expose your local server. See the [Webhook Setup Guide](/webhook-setup) for full instructions. + +## 2. WebSockets (Best for Real-Time, No Public URL) + +Stream email events over a persistent connection. No public URL or ngrok needed, which makes this ideal for local development and desktop agents. + +```python title="Python" +import asyncio +from agentmail import AsyncAgentMail, Subscribe, Subscribed, MessageReceivedEvent + +client = AsyncAgentMail() + +async def main(): + async with client.websockets.connect() as socket: + await socket.send_subscribe(Subscribe( + inbox_ids=["agent@agentmail.to"] + )) + + async for event in socket: + if isinstance(event, Subscribed): + print(f"Subscribed to: {event.inbox_ids}") + elif isinstance(event, MessageReceivedEvent): + print(f"New email from: {event.message.from_}") + print(f"Subject: {event.message.subject}") + +asyncio.run(main()) +``` + +The SDK also provides a synchronous client if you prefer: + +```python title="Python" +from agentmail import AgentMail, Subscribe, MessageReceivedEvent + +client = AgentMail() + +with client.websockets.connect() as socket: + socket.send_subscribe(Subscribe( + inbox_ids=["agent@agentmail.to"] + )) + + for event in socket: + if isinstance(event, MessageReceivedEvent): + print(f"New email from: {event.message.from_}") +``` + +See the [WebSocket Overview](/websockets) for more details. + +## Which should I use? + +| Method | Best for | Requires public URL? | Real-time? | +| --- | --- | --- | --- | +| Webhooks | Production applications | Yes | Yes | +| WebSockets | Local dev, desktop agents | No | Yes | + +For most production use cases, **webhooks** are recommended. They are reliable, event-driven, and integrate well with serverless platforms. If you need real-time events without exposing a public URL, **WebSockets** are the best option. diff --git a/fern/pages/knowledge-base/human-in-the-loop.mdx b/fern/pages/knowledge-base/human-in-the-loop.mdx new file mode 100644 index 0000000..5082243 --- /dev/null +++ b/fern/pages/knowledge-base/human-in-the-loop.mdx @@ -0,0 +1,103 @@ +--- +title: "How do I build a human-in-the-loop workflow?" +subtitle: Keep humans in control of your agent's email communications. +slug: knowledge-base/human-in-the-loop +--- + +AgentMail provides several mechanisms for keeping humans involved when agents send emails. You can combine these approaches to match the level of oversight your workflow requires. + +## 1. CC or BCC a human on every email + +The simplest approach: copy a human on every email your agent sends so they have full visibility. + +```python title="Python" +client.inboxes.messages.send( + inbox_id="agent@yourdomain.com", + to=["customer@example.com"], + cc=["manager@yourcompany.com"], + subject="Re: Your request", + text="I've processed your refund...", + html="

I've processed your refund...

" +) +``` + +The manager sees every outgoing email and can step in if something looks wrong. Use `bcc` instead of `cc` if you want the oversight to be invisible to the recipient. + +## 2. Drafts for review before sending + +Use Drafts to let your agent compose emails that a human reviews and approves before they go out. This is ideal for high-stakes emails like contracts, legal communications, or financial matters. + +```python title="Python" +# Agent composes a draft +draft = client.inboxes.drafts.create( + inbox_id="agent@yourdomain.com", + to=["important-client@example.com"], + subject="Contract proposal", + text="Here is our proposal...", + html="

Here is our proposal...

" +) + +print(f"Draft created: {draft.draft_id}") + +# Human reviews the draft, then approves it +sent_message = client.inboxes.drafts.send( + inbox_id="agent@yourdomain.com", + draft_id=draft.draft_id +) +``` + +You can also list all pending drafts across your entire organization, which is useful for building a central approval dashboard: + +```python title="Python" +# List all drafts across every inbox in the organization +all_drafts = client.drafts.list() +print(f"{all_drafts.count} drafts pending review") +``` + +For more details, see the [Drafts core concept](/drafts) documentation. + +## 3. Labels for escalation + +Use labels to flag messages that need human attention. Your agent can detect situations it cannot handle and tag them for review. + +```python title="Python" +# Agent detects a situation that needs human review +client.inboxes.messages.update( + inbox_id="agent@yourdomain.com", + message_id=msg.message_id, + add_labels=["needs-human-review", "escalation"] +) +``` + +Then build a dashboard or scheduled job that queries for flagged messages: + +```python title="Python" +# Periodically check for messages that need human review +flagged = client.inboxes.messages.list( + inbox_id="agent@yourdomain.com", + labels=["needs-human-review"] +) + +for msg in flagged.messages: + notify_human(msg) +``` + +## 4. Allowlists as guardrails + +Use a send allowlist to restrict which addresses your agent can email. If the agent tries to send to an address or domain not on the list, AgentMail will reject the request and the email will not go out. + +```python title="Python" +# Only allow sending to known customer domains +client.lists.create("send", "allow", entry="trusted-customer.com") +client.lists.create("send", "allow", entry="partner-corp.com") +``` + +This acts as a hard safety boundary. Your agent can only email recipients you have explicitly approved, regardless of what the agent logic tries to do. + +## Best practices + +- **Start with CC + drafts** for new agents until you trust their behavior +- **Graduate to autonomous sending** for routine emails, and keep drafts for high-value communications +- **Always have an escalation path:** agents should know when to stop and ask for help +- **Use labels consistently** across your agents so dashboards and alerting work reliably +- **Combine approaches:** for example, use allowlists for autonomous sending to known recipients, drafts for unknown recipients, and CC a human on everything during the first week diff --git a/fern/pages/knowledge-base/inbox-capabilities.mdx b/fern/pages/knowledge-base/inbox-capabilities.mdx new file mode 100644 index 0000000..0649ffe --- /dev/null +++ b/fern/pages/knowledge-base/inbox-capabilities.mdx @@ -0,0 +1,95 @@ +--- +title: "What can I do with an AgentMail inbox?" +subtitle: A complete overview of inbox capabilities for AI agents. +slug: knowledge-base/inbox-capabilities +--- + +An AgentMail inbox is a full email account for your AI agent. Each inbox gets a unique email address and can send, receive, reply, forward, and manage emails entirely through the API. + +## Sending + +- **Send emails** to anyone on the internet +- **CC and BCC AI agents in email threads** +- **Forward emails to humans and AI agents** +- **Create and send drafts** +- **HTML and plain text** +- **Attachments** with Base64 encoding (PDFs, images, documents) +- **Custom display names** +- **Labeling email threads** +- **Schedule send emails for later** + +## Receiving + +- **Receive emails** from anyone: your inbox has a real email address +- **Webhooks** for real-time notifications when an email arrives +- **WebSockets** for persistent event streaming without needing a public URL +- **Spam and virus detection** on all incoming emails +- **Attachment downloads** to programmatically access files from received emails +- **Reply extraction** with built-in `extracted_text` and `extracted_html` fields that strip quoted text + +## Threading and conversations + +- **Automatic threading:** replies are grouped into conversation threads using standard email headers +- **Reply-to messages** to maintain context in multi-turn conversations +- **Reply all** to respond to all recipients on a thread +- **Forward messages** to other addresses or agents +- **Org-wide thread listing** to query conversations across every inbox in your organization + +## Organization and filtering + +- **Labels:** add custom string tags to messages (e.g., `urgent`, `sales`, `needs-response`) +- **Filter by label:** list only messages or threads matching specific labels +- **Allowlists and blocklists:** control who an inbox can send to and receive from +- **Pods:** isolate groups of inboxes per customer for multi-tenant applications + +## Identity and authentication + +- **Custom domains:** send from your own domain (e.g., `agent@yourcompany.com`) instead of `@agentmail.to` +- **SPF, DKIM, and DMARC:** full email authentication for production deliverability +- **Idempotent inbox creation:** use the `client_id` parameter to safely create inboxes without duplicates + +## Access methods + +- **REST API:** full CRUD on inboxes, messages, threads, drafts, and attachments +- **Python SDK:** `pip install agentmail` +- **TypeScript SDK:** `npm install agentmail` +- **SMTP:** connect email clients or existing systems for sending +- **MCP Server:** use with Claude Code, Cursor, and other AI coding tools +- **IMAP:** coming soon + +## Quick example + +```python title="Python" +from agentmail import AgentMail + +client = AgentMail() + +# Create an inbox with a display name +inbox = client.inboxes.create(display_name="Support Agent") + +# Send an email with a human CC'd +client.inboxes.messages.send( + inbox_id=inbox.inbox_id, + to=["customer@example.com"], + cc=["manager@yourcompany.com"], + subject="Your support request #1234", + text="Hi! I've reviewed your request and here is what I found...", + html="

Hi! I've reviewed your request and here is what I found...

", + labels=["support", "tier-1"] +) + +# Check for replies +threads = client.inboxes.threads.list(inbox_id=inbox.inbox_id) + +if threads.threads: + # Get the full conversation + thread = client.threads.get(thread_id=threads.threads[0].thread_id) + + # Reply to the latest message + client.inboxes.messages.reply( + inbox_id=inbox.inbox_id, + message_id=thread.messages[-1].message_id, + text="Following up: have you had a chance to try the fix?", + html="

Following up: have you had a chance to try the fix?

" + ) +``` diff --git a/fern/pages/knowledge-base/introduction.mdx b/fern/pages/knowledge-base/introduction.mdx new file mode 100644 index 0000000..2309507 --- /dev/null +++ b/fern/pages/knowledge-base/introduction.mdx @@ -0,0 +1,193 @@ +--- +title: Knowledge Base +subtitle: A collection of answers to frequently asked questions. +slug: knowledge-base +--- + +## Getting Started + + + + How AgentMail compares to Resend, SendGrid, and other email providers. + + + CC, BCC, forwarding, threading, attachments, labels, IMAP, and more. + + + Get up and running with your first AgentMail inbox. + + + How to create and manage your API keys. + + + +## Agent Patterns + + + + Webhooks vs WebSockets vs polling for receiving emails. + + + Control who can send to and receive from your inboxes. + + + Build agents that maintain context across email threads. + + + Add human review and approval steps to your agent workflows. + + + Isolate inboxes and data across tenants with Pods. + + + Use labels to manage agent workflow state on emails. + + + +## Domains & Deliverability + + + + Send emails from your own domain instead of @agentmail.to. + + + Authenticate your domain for better deliverability. + + + Troubleshoot and fix spam folder placement issues. + + + Gradually build sending reputation on a new domain. + + + Add AgentMail DNS records without breaking existing email. + + + +## Troubleshooting + + + + Understand AgentMail's rate limits and how to work within them. + + + Use idempotency to avoid sending the same email twice. + + + What to do when your domain verification is stuck. + + + Diagnose and resolve email bounce issues. + + + +## DNS Guides + +Step-by-step instructions for verifying your domain with popular DNS providers. + + + + Configure AgentMail DNS records in Cloudflare. + + + Configure AgentMail DNS records in GoDaddy. + + + Configure AgentMail DNS records in AWS Route 53. + + + Configure AgentMail DNS records in Namecheap. + + diff --git a/fern/pages/knowledge-base/labels-track-state.mdx b/fern/pages/knowledge-base/labels-track-state.mdx new file mode 100644 index 0000000..3242c13 --- /dev/null +++ b/fern/pages/knowledge-base/labels-track-state.mdx @@ -0,0 +1,131 @@ +--- +title: "How do I use labels to track email state?" +subtitle: Use labels to manage agent workflow state on emails and threads. +slug: knowledge-base/labels-track-state +--- + +Labels are string-based tags you attach to messages and threads. They are the primary way agents track state, classify emails, and filter conversations in AgentMail. + +## Adding labels when sending + +You can attach labels directly when sending a message: + +```python title="Python" +client.inboxes.messages.send( + inbox_id="agent@yourdomain.com", + to=["prospect@example.com"], + subject="Quick question", + text="Hi, I wanted to ask about...", + html="

Hi, I wanted to ask about...

", + labels=["outreach", "first-touch", "tech-vertical"] +) +``` + +## Updating labels on existing messages + +Add or remove labels on messages that have already been sent or received. This is how agents change the state of a conversation as they process it: + +```python title="Python" +# Mark a message as processed +client.inboxes.messages.update( + inbox_id="agent@yourdomain.com", + message_id=msg.message_id, + add_labels=["processed", "positive-sentiment"], + remove_labels=["unread"] +) +``` + +## Filtering by label + +List messages or threads that match specific labels. This is where labels become powerful for building agent workflows: + +```python title="Python" +# Get all unread messages in an inbox +unread = client.inboxes.messages.list( + inbox_id="agent@yourdomain.com", + labels=["unread"] +) + +# Get threads that need a follow-up from a specific campaign +follow_ups = client.inboxes.threads.list( + inbox_id="agent@yourdomain.com", + labels=["q4-campaign", "needs-response"] +) + +# Get escalations that need human review +escalations = client.inboxes.messages.list( + inbox_id="agent@yourdomain.com", + labels=["escalation", "needs-human-review"] +) +``` + +## Common label patterns for agents + +| Label | Purpose | +| --- | --- | +| `unread` / `read` | Track which messages the agent has seen | +| `unreplied` / `replied` | Track which threads need a response | +| `outreach` / `inbound` | Classify message direction | +| `needs-human-review` | Route to a human for oversight | +| `escalation` | Flag high-priority issues | +| `processed` | Agent has finished handling this message | +| `positive` / `negative` | Sentiment classification | +| `campaign-{name}` | Track which campaign generated the email | + +Labels are free-form strings. Use whatever naming convention makes sense for your agent's workflow. + +## Example: agent workflow with labels + +Here is a complete pattern for an agent that processes inbound emails, classifies them, and tracks state: + +```python title="Python" +from agentmail import AgentMail + +client = AgentMail() + +# Find threads that need a reply +unreplied = client.inboxes.threads.list( + inbox_id="support@yourdomain.com", + labels=["unreplied"] +) + +for thread_item in unreplied.threads: + # Get the full thread + thread = client.threads.get(thread_id=thread_item.thread_id) + last_message = thread.messages[-1] + + # Classify the message (your agent logic here) + category = classify(last_message.extracted_text or last_message.text) + + if category == "needs-human": + # Escalate: add label, skip auto-reply + client.inboxes.messages.update( + inbox_id="support@yourdomain.com", + message_id=last_message.message_id, + add_labels=["needs-human-review", "escalation"], + remove_labels=["unreplied"] + ) + else: + # Auto-reply and mark as handled + reply_text = generate_reply(last_message, category) + client.inboxes.messages.reply( + inbox_id="support@yourdomain.com", + message_id=last_message.message_id, + text=reply_text + ) + client.inboxes.messages.update( + inbox_id="support@yourdomain.com", + message_id=last_message.message_id, + add_labels=["replied", "processed", category], + remove_labels=["unreplied"] + ) +``` + +## Best practices + +- **Be consistent:** Pick a naming convention (e.g., `kebab-case`) and stick with it across all your agents +- **Use prefixes for grouping:** Labels like `status-pending`, `priority-high`, `campaign-q4` are easier to manage at scale +- **Keep it concise:** A message with too many labels becomes hard to query. Aim for a meaningful, focused set +- **Combine with threads:** Filter threads by label to find conversations in a specific state, then get the thread details to process them + +For more details, see the [Labels core concept](/labels) documentation. diff --git a/fern/pages/knowledge-base/mx-record-conflicts.mdx b/fern/pages/knowledge-base/mx-record-conflicts.mdx new file mode 100644 index 0000000..21c140b --- /dev/null +++ b/fern/pages/knowledge-base/mx-record-conflicts.mdx @@ -0,0 +1,80 @@ +--- +title: "How do I avoid MX record conflicts?" +subtitle: Add AgentMail DNS records without breaking existing email. +slug: knowledge-base/mx-record-conflicts +--- + +If you already use Gmail, Outlook, or another email provider for your domain, adding AgentMail's MX records could conflict with your existing setup. Here is how to avoid that. + +## Use a subdomain + +The simplest and recommended approach: set up AgentMail on a subdomain instead of your root domain. + +| Domain | Used by | No conflict | +| --- | --- | --- | +| `yourcompany.com` | Gmail / Outlook (your team) | Unchanged | +| `agents.yourcompany.com` | AgentMail (your agents) | Separate MX records | + +Your agents send and receive from addresses like `support@agents.yourcompany.com`, while your team keeps using `name@yourcompany.com`. Both work independently with no overlap. + +## How to set it up + +When adding your domain in AgentMail, use the subdomain: + +```typescript title="TypeScript" +import { AgentMailClient } from "agentmail"; + +const client = new AgentMailClient({ apiKey: "am_..." }); + +// Use a subdomain to avoid MX conflicts +const domain = await client.domains.create("agents.yourcompany.com"); + +// Create inboxes on the subdomain +const inbox = await client.inboxes.create({ + username: "support", + domain: "agents.yourcompany.com", + displayName: "Support Agent", +}); +// inbox address: support@agents.yourcompany.com +``` + +Then add the MX, SPF, and DKIM records for `agents.yourcompany.com` at your DNS provider. These records are completely separate from your root domain's records and will not affect your existing email. + +## Can I use my root domain? + +Yes, but only in these scenarios: + +**Sending only (no receiving):** If you only need AgentMail to send emails from your root domain, you only need SPF (TXT) and DKIM (CNAME) records. These do not conflict with existing MX records, so your Gmail or Outlook inbound mail is unaffected. + +**Full AgentMail (sending and receiving):** If you want AgentMail to also receive emails on your root domain, you would need to replace your existing MX records with AgentMail's. This means all incoming mail for that domain routes to AgentMail instead of Gmail or Outlook. Only do this if you want AgentMail to handle all inbound email for that domain. + +## Example DNS setup + +**Before (Gmail only):** + +```text +yourcompany.com MX 10 aspmx.l.google.com. +yourcompany.com MX 20 alt1.aspmx.l.google.com. +``` + +**After (Gmail on root + AgentMail on subdomain):** + +```text +yourcompany.com MX 10 aspmx.l.google.com. +yourcompany.com MX 20 alt1.aspmx.l.google.com. +agents.yourcompany.com MX 10 inbound.agentmail.to. +agents.yourcompany.com TXT v=spf1 include:agentmail.to ~all +agents.yourcompany.com CNAME (DKIM record from AgentMail) +``` + +Your team's email continues to flow through Gmail. Agent emails go through AgentMail. Clean separation with no conflicts. + +## Common subdomain choices + +| Subdomain | Example address | +| --- | --- | +| `agents.yourcompany.com` | `support@agents.yourcompany.com` | +| `mail.yourcompany.com` | `outreach@mail.yourcompany.com` | +| `ai.yourcompany.com` | `assistant@ai.yourcompany.com` | + +Pick whatever makes sense for your brand. The important thing is that it is a separate subdomain from your root domain's email. diff --git a/fern/pages/knowledge-base/pods-multi-tenant.mdx b/fern/pages/knowledge-base/pods-multi-tenant.mdx new file mode 100644 index 0000000..7defccd --- /dev/null +++ b/fern/pages/knowledge-base/pods-multi-tenant.mdx @@ -0,0 +1,116 @@ +--- +title: "How do I use Pods for multi-tenant email?" +subtitle: Isolate inboxes, domains, and data across tenants with Pods. +slug: knowledge-base/pods-multi-tenant +--- + +Pods provide tenant isolation for multi-tenant applications. Each Pod is an isolated workspace containing its own inboxes, domains, threads, and drafts, completely separated from other Pods. + +## When to use Pods + +Pods are designed for scenarios where you need to keep different customers' or agents' email data separate: + +- **SaaS platforms** where each customer's agents need their own email inboxes +- **Agency setups** where each client gets isolated email infrastructure +- **AI agent platforms** where different agents need dedicated, isolated workspaces +- **White-label products** where end users get email under their own brand + +If you are only managing email for your own organization, Pods are optional. You can work directly with inboxes without creating them. + +## The hierarchy + +``` +Organization (your business) + └── Pod (Customer A) + ├── Inbox: support@customera.com + ├── Inbox: sales@customera.com + └── Domain: customera.com + └── Pod (Customer B) + ├── Inbox: hello@customerb.com + └── Domain: customerb.com +``` + +Everything inside a Pod is isolated. Customer A cannot see Customer B's emails, threads, or drafts. + +## Creating a Pod and its resources + +```python title="Python" +from agentmail import AgentMail + +client = AgentMail() + +# Create a Pod for a customer +pod = client.pods.create(name="Acme Corp") + +# Create inboxes within that Pod +support_inbox = client.pods.inboxes.create( + pod_id=pod.pod_id, + username="support", + domain="acme.com", + display_name="Acme Support" +) + +sales_inbox = client.pods.inboxes.create( + pod_id=pod.pod_id, + username="sales", + domain="acme.com", + display_name="Acme Sales" +) +``` + + +Use the `client_id` parameter when creating a Pod to set your own unique identifier. This way you can map Pods to your internal customer IDs without maintaining a separate mapping table. + + +```python title="Python" +# Use your internal customer ID as the client_id +pod = client.pods.create( + name="Acme Corp", + client_id="customer_12345" +) +``` + +## Listing resources within a Pod + +You can list inboxes, threads, drafts, and domains scoped to a specific Pod: + +```python title="Python" +# List all inboxes in a Pod +inboxes = client.pods.inboxes.list(pod_id=pod.pod_id) + +# List all threads across all inboxes in a Pod +threads = client.pods.threads.list(pod_id=pod.pod_id) + +# List all pending drafts in a Pod +drafts = client.pods.drafts.list(pod_id=pod.pod_id) +``` + +This makes it easy to build features like "show all unread emails for Customer X" or "list all pending drafts for Customer Y." + +## Deleting a Pod + +You cannot delete a Pod that still has inboxes or domains attached to it. Clean up child resources first: + +```python title="Python" +# Delete all inboxes in the Pod first +inboxes = client.pods.inboxes.list(pod_id=pod.pod_id) +for inbox in inboxes.inboxes: + client.inboxes.delete(inbox.inbox_id) + +# Then delete the Pod +client.pods.delete(pod_id=pod.pod_id) +``` + + +When you delete an inbox, all associated messages, threads, and drafts are automatically cleaned up. You do not need to delete them individually. + + +## Things to know + +- **Cross-pod email works normally.** Inboxes in different Pods can send and receive emails from each other, just like any other email addresses. Pods isolate data access, not email delivery. +- **Inboxes cannot move between Pods.** If you need to reassign an inbox, create a new one in the target Pod. +- **Pod-scoped API keys.** You can create API keys scoped to a specific Pod, so each tenant only has access to their own resources. Create them via `POST /pods/{pod_id}/api-keys`. +- **No limit on Pods.** You can create as many Pods as you need for your customers. +- **Domains can be scoped to one Pod or all Pods.** A domain cannot be shared across a subset of Pods. + +For more details, see the [Pods core concept](/pods) documentation. diff --git a/fern/pages/knowledge-base/preventing-duplicate-sends.mdx b/fern/pages/knowledge-base/preventing-duplicate-sends.mdx new file mode 100644 index 0000000..e889b6c --- /dev/null +++ b/fern/pages/knowledge-base/preventing-duplicate-sends.mdx @@ -0,0 +1,92 @@ +--- +title: "How do I prevent duplicate sends?" +subtitle: Use idempotency to avoid sending the same email twice. +slug: knowledge-base/preventing-duplicate-sends +--- + +AI agents can sometimes retry requests due to network errors, timeouts, or logic bugs. Without safeguards, this can cause the same email to be sent multiple times. Here is how to prevent that. + +## Idempotent resource creation with client_id + +AgentMail supports idempotency for all **create** operations via the `clientId` parameter. When you provide a `clientId`, AgentMail checks if a resource with that ID already exists. If it does, it returns the existing resource instead of creating a duplicate. + +This works for creating inboxes, pods, webhooks, and drafts: + +```typescript title="TypeScript" +import { AgentMailClient } from "agentmail"; + +const client = new AgentMailClient({ apiKey: "am_..." }); + +// Safe to call multiple times: only creates the inbox once +const inbox = await client.inboxes.create({ + username: "support", + clientId: "support-inbox-v1", +}); + +// Calling again with the same clientId returns the existing inbox +const sameInbox = await client.inboxes.create({ + username: "support", + clientId: "support-inbox-v1", +}); + +// inbox.inboxId === sameInbox.inboxId +``` + +## Preventing duplicate email sends + +The `clientId` parameter is for resource creation, not for `messages.send`. To prevent duplicate email sends, you need to handle this in your application logic. Here are common patterns: + +### Track sent messages with labels + +Use labels to mark messages that your agent has already processed, so it does not reply twice: + +```typescript title="TypeScript" +// Before replying, check if already handled +const threads = await client.inboxes.threads.list(inbox.inboxId, { + labels: ["unreplied"], +}); + +for (const thread of threads.threads) { + const detail = await client.threads.get(thread.threadId); + const lastMessage = detail.messages[detail.messages.length - 1]; + + // Reply and update labels atomically in your logic + await client.inboxes.messages.reply(inbox.inboxId, lastMessage.messageId, { + text: "Thanks for reaching out!", + }); + + await client.inboxes.messages.update(inbox.inboxId, lastMessage.messageId, { + addLabels: ["replied"], + removeLabels: ["unreplied"], + }); +} +``` + +### Use drafts for critical sends + +For high-stakes emails, use drafts instead of sending directly. Create a draft, verify it has not been sent already, then send: + +```typescript title="TypeScript" +// Create a draft with a deterministic clientId +const draft = await client.inboxes.drafts.create(inbox.inboxId, { + to: ["customer@example.com"], + subject: "Order confirmation", + text: "Your order has been confirmed.", + html: "

Your order has been confirmed.

", + clientId: "order-123-confirmation", +}); + +// Later, send the draft (only works once, draft is deleted after sending) +const sent = await client.inboxes.drafts.send(inbox.inboxId, draft.draftId); +``` + +Since drafts support `clientId`, creating the same draft multiple times is safe. And once a draft is sent, it is deleted, so calling `drafts.send` again will fail rather than send a duplicate. + +## Best practices + +- **Use `clientId` on all create operations** (inboxes, pods, webhooks, drafts) to make them safe to retry +- **Generate `clientId` from your business logic** (e.g., `order-${orderId}-confirmation`), not random UUIDs +- **Track state with labels** to prevent your agent from processing the same message twice +- **Use drafts for critical sends** where duplicates would be harmful + +For more details, see the [Idempotent Requests](/idempotency) guide. diff --git a/fern/pages/knowledge-base/rate-limits.mdx b/fern/pages/knowledge-base/rate-limits.mdx new file mode 100644 index 0000000..81d9257 --- /dev/null +++ b/fern/pages/knowledge-base/rate-limits.mdx @@ -0,0 +1,57 @@ +--- +title: "What are the rate limits?" +subtitle: Understand AgentMail's rate limits and how to work within them. +slug: knowledge-base/rate-limits +--- + +AgentMail is built for high-volume agent workflows. Limits vary by plan. + +## Sending limits by plan + +| Plan | Emails per month | Inboxes | Custom domains | +| --- | --- | --- | --- | +| Free | 3,000 | 3 | None | +| Developer | 10,000 | 10 | 10 | +| Startup | 150,000 | 150 | 150 | +| Enterprise | Custom | Custom | Custom | + +For full plan details, see the [pricing page](https://agentmail.to/pricing). + +## API rate limits + +All API endpoints are rate-limited per API key. If you exceed the limit, the API returns a `429 Too Many Requests` response with a `Retry-After` header indicating how long to wait before retrying. + +```typescript title="TypeScript" +import { AgentMailClient } from "agentmail"; + +const client = new AgentMailClient({ apiKey: "am_..." }); + +async function sendWithRetry(inboxId: string, params: any, maxRetries = 3) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await client.inboxes.messages.send(inboxId, params); + } catch (error: any) { + if (error.statusCode === 429 && attempt < maxRetries - 1) { + const retryAfter = parseInt(error.headers?.["retry-after"] || "5"); + await new Promise((r) => setTimeout(r, retryAfter * 1000)); + } else { + throw error; + } + } + } +} +``` + +## Tips for high-volume agents + +**Distribute sends across inboxes.** Sending from multiple inboxes improves deliverability and avoids per-address throttling by mailbox providers. Instead of 1,000 emails from 1 inbox, send 10 emails each from 100 inboxes. + +**Use idempotency keys for resource creation.** The `clientId` parameter on create operations (inboxes, pods, webhooks, drafts) prevents duplicate resources on retries. For preventing duplicate email sends, track sent message IDs in your application or use the [draft workflow](/knowledge-base/preventing-duplicate-sends). + +**Implement exponential backoff.** When you receive a `429` response, wait for the duration specified in the `Retry-After` header before retrying. Increase the wait time with each consecutive retry. + +**Monitor your usage.** Track your sending volume against your plan limits. You can view usage in the [AgentMail Console](https://console.agentmail.to). + +## Need higher limits? + +If you need higher sending volumes or more inboxes than your current plan allows, contact [support@agentmail.cc](mailto:support@agentmail.cc) or visit the [pricing page](https://agentmail.to/pricing) for enterprise options. diff --git a/fern/pages/knowledge-base/spf-dkim-dmarc.mdx b/fern/pages/knowledge-base/spf-dkim-dmarc.mdx new file mode 100644 index 0000000..de66c5b --- /dev/null +++ b/fern/pages/knowledge-base/spf-dkim-dmarc.mdx @@ -0,0 +1,94 @@ +--- +title: "How do I set up SPF, DKIM, and DMARC?" +subtitle: Authenticate your domain for reliable email deliverability. +slug: knowledge-base/spf-dkim-dmarc +--- + +SPF, DKIM, and DMARC are three email authentication protocols that prove your emails are legitimate. They are essential for deliverability: Gmail, Outlook, and other major providers now require all three for reliable inbox placement. + +AgentMail provides all three records automatically when you add a custom domain. You just need to add them to your DNS provider. + +## SPF (Sender Policy Framework) + +SPF tells receiving servers which mail servers are authorized to send email for your domain. It is a TXT record that lists approved senders. + +**What AgentMail provides:** + +```text +TXT | @ | v=spf1 include:agentmail.to ~all +``` + +This tells receiving servers that AgentMail is an authorized sender for your domain. The `~all` means emails from servers not on the list should be treated as suspicious. + + +You can only have **one** SPF record per domain. If you already have an SPF record (e.g., for Google Workspace or another email service), merge AgentMail's `include:` into the existing record rather than creating a second one. + + +**Example of merging SPF records:** + +```text +# Before (existing SPF for Google Workspace) +v=spf1 include:_spf.google.com ~all + +# After (with AgentMail added) +v=spf1 include:_spf.google.com include:agentmail.to ~all +``` + +## DKIM (DomainKeys Identified Mail) + +DKIM adds a cryptographic signature to every email, proving it was sent by an authorized server and was not tampered with in transit. It uses CNAME records that point to signing keys managed by AgentMail. + +**What AgentMail provides:** + +```text +CNAME | agentmail._domainkey | (target provided by AgentMail) +``` + +AgentMail manages the signing keys automatically. Once you add the CNAME records, every email from your domain is signed with no ongoing maintenance required. + + +If you use Cloudflare, make sure the CNAME proxy status is set to **DNS only** (grey cloud). Proxied CNAME records will break DKIM verification. + + +## DMARC (Domain-based Message Authentication, Reporting, and Conformance) + +DMARC ties SPF and DKIM together. It tells receiving servers what to do when an email fails authentication: reject it, quarantine it (send to spam), or do nothing. + +**What AgentMail provides:** + +```text +TXT | _dmarc | v=DMARC1; p=reject; rua=mailto:dmarc@agentmail.to +``` + +AgentMail sets the policy to `reject` by default, which means any email that fails DMARC authentication (typically when both SPF and DKIM fail to align with the sender domain) will be rejected outright. The `rua` tag sends aggregate reports to AgentMail so we can monitor your domain's deliverability health. + +**If you are setting up DMARC for the first time,** consider starting with a less strict policy and graduating to `reject`: + +| Policy | Behavior | When to use | +| --- | --- | --- | +| `p=none` | Monitor only, no action taken | Initial setup, while confirming everything works | +| `p=quarantine` | Failed emails go to spam | After confirming SPF and DKIM pass consistently | +| `p=reject` | Failed emails are rejected entirely | Production, once you trust your configuration | + +## Do I need all three? + +Yes. Each protocol serves a different purpose: + +| Protocol | What it proves | +| --- | --- | +| SPF | The sending server is authorized to send for your domain | +| DKIM | The email content has not been modified in transit | +| DMARC | What to do when SPF or DKIM fails, plus reporting | + +Without all three, your agent's emails are more likely to land in spam or be rejected. Major providers like Gmail and Yahoo enforce these requirements. + +## How to set them up + +1. Add your domain in the [AgentMail Console](https://console.agentmail.to) or via the API +2. AgentMail provides the exact SPF, DKIM, and DMARC records you need +3. Add those records at your DNS provider +4. Verify your domain in the console + +For step-by-step DNS instructions, see our provider guides: [Cloudflare](/knowledge-base/dns-cloudflare), [GoDaddy](/knowledge-base/dns-godaddy), [Route 53](/knowledge-base/dns-route53), [Namecheap](/knowledge-base/dns-namecheap). + +For a deeper explanation of how these protocols work, see the [SPF, DKIM, DMARC](/email-protocols) documentation. diff --git a/fern/pages/knowledge-base/threaded-conversations.mdx b/fern/pages/knowledge-base/threaded-conversations.mdx new file mode 100644 index 0000000..22e4597 --- /dev/null +++ b/fern/pages/knowledge-base/threaded-conversations.mdx @@ -0,0 +1,127 @@ +--- +title: "How do I manage threaded conversations?" +subtitle: Maintain context across multi-turn email conversations with your agent. +slug: knowledge-base/threaded-conversations +--- + +Threads are how AgentMail organizes conversations. Every time your agent sends a new email, a thread is created. Replies are automatically grouped into the same thread, giving your agent full conversation context. + +## How threads work + +1. Your agent sends an email, and a new thread is created automatically +2. The recipient replies, and the reply is added to the same thread +3. Your agent replies back, and it is added to the same thread +4. The full conversation history stays organized in one place + +You never need to create threads manually. AgentMail handles threading automatically using standard email headers (`Message-ID`, `In-Reply-To`, `References`). + +## Listing threads + +### Per inbox + +```python title="Python" +threads = client.inboxes.threads.list( + inbox_id="agent@agentmail.to" +) + +for t in threads.threads: + print(f"Thread: {t.subject} ({t.message_count} messages)") +``` + +### Across your entire organization + +```python title="Python" +# Get all threads from every inbox in your organization +all_threads = client.threads.list() +``` + +This org-wide query is useful for building supervisor agents that monitor conversations across a fleet of other agents, analytics dashboards, or routing systems that escalate conversations between agents. + +## Getting a full thread + +Retrieve a thread by its ID to access all messages in the conversation: + +```python title="Python" +thread = client.threads.get(thread_id="thread_abc123") + +for message in thread.messages: + print(f"From: {message.from_}") + print(f"Subject: {message.subject}") + print(f"Body: {message.text}") +``` + +## Replying in a thread + +To continue a conversation, reply to the most recent message in the thread: + +```python title="Python" +# Get the thread and find the latest message +thread = client.threads.get(thread_id="thread_abc123") +last_message = thread.messages[-1] + +# Reply to continue the conversation +client.inboxes.messages.reply( + inbox_id="agent@agentmail.to", + message_id=last_message.message_id, + text="Thanks for your message! Here's what I found...", + html="

Thanks for your message! Here's what I found...

" +) +``` + + +Always provide both `text` and `html` when sending replies. This ensures readability across all email clients and improves deliverability. + + +## Handling quoted text in replies + +When people reply to emails, their email client often includes the entire previous conversation as quoted text. AgentMail provides `extracted_text` and `extracted_html` fields on received messages, which contain only the new reply content without the quoted history. + +```python title="Python" +# Use extracted_text to get only the new content +new_content = message.extracted_text + +# Falls back to full text if extraction isn't available +content = message.extracted_text or message.text +``` + +This prevents your agent from re-processing the entire conversation history on every reply. + +## Using labels to track conversation state + +Combine threads with labels to manage your agent's workflow. For example, you can track which threads need a reply: + +```python title="Python" +# Find threads that need a reply +unreplied = client.inboxes.threads.list( + inbox_id="agent@agentmail.to", + labels=["unreplied"] +) + +for thread in unreplied.threads: + thread_detail = client.threads.get(thread.thread_id) + last_message = thread_detail.messages[-1] + + # Process and reply + reply_text = your_agent.process(last_message) + client.inboxes.messages.reply( + inbox_id="agent@agentmail.to", + message_id=last_message.message_id, + text=reply_text, + html=f"

{reply_text}

" + ) + + # Update labels + client.inboxes.messages.update( + inbox_id="agent@agentmail.to", + message_id=last_message.message_id, + add_labels=["replied"], + remove_labels=["unreplied"] + ) +``` + +## Tips + +- Use threads to maintain context in multi-turn conversations, so your agent can reference what was said earlier +- Query org-wide threads with `client.threads.list()` to build dashboards or route conversations between agents +- Use labels like `unreplied`, `replied`, `escalated`, and `resolved` to track conversation state +- Always reply to the **last message** in a thread to keep email headers correct diff --git a/fern/pages/knowledge-base/what-is-agentmail.mdx b/fern/pages/knowledge-base/what-is-agentmail.mdx new file mode 100644 index 0000000..83f8ab9 --- /dev/null +++ b/fern/pages/knowledge-base/what-is-agentmail.mdx @@ -0,0 +1,36 @@ +--- +title: "What is AgentMail and how is it different?" +subtitle: Understand how AgentMail compares to traditional email providers. +slug: knowledge-base/what-is-agentmail +--- + +AgentMail is email infrastructure built specifically for AI agents. Unlike transactional email APIs that focus on one-way sending, AgentMail is built for two-way agent communication: dedicated inboxes, native threading, and full receiving support with no shared sending domains. + +## Key differences + +| Capability | AgentMail | Transactional API | +| --- | --- | --- | +| **Inboxes** | Create thousands of unique inboxes via API, each with its own email address | Shared sending domains, no per-agent inboxes | +| **Receiving email** | First-class support: agents read, parse, and reply to incoming emails | Simple webhook | +| **Threaded conversations** | Native threading, support for multi-threaded (cc, bcc, forwarding)| Build threading yourself | +| **Agent identity** | Each agent gets its own email identity (address, display name) | Not available | +| **Allowlists / Blocklists** | Control who each agent can send to and receive from | Not available | +| **Multi-tenant (Pods)** | Built-in tenant isolation for SaaS applications | Build it yourself | +| **Real-time events** | Webhooks and WebSockets for instant email event streaming | Webhooks only (no WebSocket option) | +| **IMAP / SMTP** | Full protocol access for connecting to email clients | API only (no IMAP/SMTP access) | +| **Reply extraction** | Built-in `extracted_text` and `extracted_html` to strip quoted text from replies | Not available | +| **Semantic search** | Search across inboxes by meaning, not just keywords | Not available | + +## When to use AgentMail + +AgentMail is the right choice when: + +- Your agents are first-class users that need threading, identity, and storage +- Your agent needs its **own email identity** rather than a shared noreply@ address +- You are building **conversational email workflows** like outreach, support, or scheduling +- You need **per-agent or per-customer inbox isolation** (Pods) +- Your agent needs to **sign up for services**, receive verification codes, or act as a user on the internet + +## Can I migrate from other platforms? + +Yes. AgentMail's send API works similarly. Replace your existing send call with `client.inboxes.messages.send()` and you get the same sending capability plus full receiving, threading, and agent identity features on top. See the [Quickstart](/quickstart) to get started in minutes.