Changelog and releases
Latest AgentMail API and SDK updates.
Latest API and SDK updates, newest first. Subscribe via RSS · Discord
Build AgentID sign-in flows with a scoped P-256 credential while keeping private key material in your own keystore. The API reference now defines a dedicated public-key lifecycle for generated SDKs, and the new guide provides strict Python and TypeScript approval helpers.
What’s new?
New endpoints:
POST /v0/api-keys/public-keys- Register only a public P-256 JWK and receive the server-ownedapi_key_idused askidGET /v0/api-keys/public-keys- List public-key credentials without mixing in bearer credentialsPATCH /v0/api-keys/public-keys/{api_key_id}- Rename a credential without mutating security-relevant fieldsDELETE /v0/api-keys/public-keys/{api_key_id}- Revoke one public-key credentialPOST /v0/api-keys/public-keys/agentid-sign-in/revoke-all- Idempotently invalidate every current AgentID sign-in key in an organization
New AgentID endpoint (served by the AgentID issuer, not part of the AgentMail REST API or generated SDKs; call it directly as shown in the guide):
POST https://auth.agentid.com/authorize/approve- Submit one strict ES256 approval assertion without a bearer credential
New features:
- Scoped credentials: Register organization-, pod-, or inbox-scoped keys with inherited scope and expiry defaults.
- Generated SDK contract: Generate P-256 keys, register only public coordinates, pin the approval header and claims, and keep private keys below model context once corresponding SDK releases are published.
Use cases
Build agents that:
- Approve AgentID sign-in while the private key stays in a keystore or HSM
- Delegate sign-in authority to one organization, pod, or inbox
- Rotate credentials with a create-new, deploy-new, delete-old sequence
- Fence every active AgentID sign-in key with an idempotent emergency operation
Follow the AgentID public-key authentication guide for complete Python and TypeScript helpers, lifecycle rules, and the accepted browser-session intent limitation.
Agents can now create reply, reply-all, and forward drafts directly from a message, instead of rebuilding the subject and threading (and, for replies, the recipients) by hand. The draft is saved rather than sent, so a human can review it before it goes out, then send it with Send Draft. This makes human-in-the-loop review and scheduled follow-ups a first-class part of the reply flow.
What’s new?
Create Draft now builds replies and forwards. Pass a source message to inboxes.drafts.create and AgentMail carries over the subject and threading (and, for replies, the recipients):
in_reply_to— create a draft replying to the sender. Addreply_allto address the whole thread.forward_of— create a draft forwarding the message. Recipients stay caller-supplied.
in_reply_to and forward_of are mutually exclusive, and reading the referenced message requires the message_read permission.
New features:
- Replies: recipients, subject, and threading (
in_reply_to/references) are taken from the original message, so you only supply your note. - Forwards: the subject, threading, and forwarded body and attachments come from the source (the body and attachments are merged in at send time); recipients are caller-supplied and optional, so a forward draft can be saved now and addressed later.
- Composable: pass
send_atto schedule the draft, or review and send it later withinboxes.drafts.send.
Changes:
- Draft responses now include
forward_of, the ID of the message a forward draft was created from.
Use cases
Build agents that:
- Draft a reply for human approval before anything leaves the inbox
- Forward a flagged message to a teammate, then send once reviewed
- Schedule a reply-all for the recipient’s business hours
- Prepare a response while still gathering data, and finalize it later
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# create a reply draft from a received message
draft = client.inboxes.drafts.create(
inbox_id="[email protected]",
in_reply_to="<[email protected]>",
text="Thanks — looping in my manager for approval.",
)
# review, then send when ready
client.inboxes.drafts.send(inbox_id="[email protected]", draft_id=draft.draft_id)See the Drafts guide for the full reply and forward flow.
You can now create and manage webhooks scoped to a single pod or inbox from dedicated endpoints, instead of only filtering an organization-level webhook with pod_ids / inbox_ids. The scope comes from the path, so pod- and inbox-scoped API keys can manage just their own webhooks.
What’s new?
New endpoints:
GET|POST /v0/pods/:pod_id/webhooksandGET|PATCH|DELETE /v0/pods/:pod_id/webhooks/:webhook_id- Manage webhooks scoped to a pod.GET|POST /v0/inboxes/:inbox_id/webhooksandGET|PATCH|DELETE /v0/inboxes/:inbox_id/webhooks/:webhook_id- Manage webhooks scoped to an inbox.
Behavior:
- A pod-scoped webhook receives events for the whole pod, and can be narrowed to specific inboxes in the pod with
inbox_ids. You don’t passpod_ids; the pod is the path. - An inbox-scoped webhook is fixed to that inbox; only its
event_typescan be changed. - A scoped webhook must always keep at least one pod or inbox subscription.
Use cases
Build agents that:
- Give each tenant’s pod-scoped API key its own webhook, isolated from other tenants
- Register a webhook for a single high-volume inbox without touching org-wide delivery
- Let an inbox-scoped key manage only its own event subscriptions
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# webhook scoped to a single pod
client.pods.webhooks.create(
pod_id="pod_abc123",
url="https://your-server.com/webhooks",
event_types=["message.received"],
)See Scoping a webhook to a pod or inbox for details.
You can now track cumulative usage over time. The new usage endpoint returns running totals of storage, messages, threads, inboxes, domains, and pods, for your whole organization, a single pod, or a single inbox. Event counts also move to a dedicated /metrics/events path, so the metrics API now cleanly separates “what happened” (events) from “what you have” (usage).
What’s new?
New endpoints:
GET /v0/metrics/usage- Cumulative usage series for the organization.GET /v0/pods/:pod_id/metrics/usage- Cumulative usage series for a pod.GET /v0/inboxes/:inbox_id/metrics/usage- Cumulative usage series for an inbox.GET /v0/metrics/events(and pod/inbox variants) - The canonical path for event counts, replacing bareGET /v0/metrics.
New features:
- Usage series: Each point is the running total of a usage type at that timestamp, not the change within the bucket. An idle scope renders a flat line at its current level, so charts stay meaningful even with no activity in the window.
- Usage types:
storage_bytes,message_count,thread_count,inbox_count,domain_count, andpod_count. Filter with theusage_typesquery parameter, or omit it to get every type the scope carries. Inboxes carry the first three; pods addinbox_countanddomain_count; organizations addpod_count. - Bucketing:
periodsets the bucket size in seconds. The range divided byperiodmust not exceed 1000 buckets; narrow the range or coarsen the period for fine-grained series.
Changes:
GET /v0/metrics(and its pod/inbox variants) is replaced by/metrics/events. The old path still responds, so existing clients keep working, but it’s removed from the docs and SDKs; useclient.metrics.queryEvents()in place ofclient.metrics.query().- Metric queries now clamp a future
endto the current time instead of returning phantom future points, andperiod/limitmust be whole numbers. - The documented metric event types now match what the API accepts: added
message.received.spam,message.received.blocked,message.received.unauthenticated, anddomain.verified; removedmessage.delayed, which the API never accepted.
Use cases
Build agents that:
- Chart storage growth over time and archive old threads before hitting quota
- Monitor message and thread volume per inbox to spot runaway automations
- Verify cleanup actually happened by watching usage drop after bulk deletes
- Compare pods by inbox and domain footprint to balance workloads
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# storage growth for the last week, one point per hour
usage = client.metrics.query_usage(
usage_types=["storage_bytes"],
start="2026-06-05T00:00:00Z",
period=3600,
)
for point in usage["storage_bytes"]:
print(point.timestamp, point.value)
# usage for a single inbox (storage, messages, threads)
inbox_usage = client.inboxes.metrics.query_usage(inbox_id="[email protected]")
# event counts now live at /metrics/events
events = client.metrics.query_events(
event_types=["message.sent", "message.bounced"],
period=3600,
)Check out the Metrics API reference for full parameter details.
You can now create inboxes on any subdomain of a verified domain without registering each subdomain separately. Enable subdomains_enabled on a domain, publish the single wildcard MX record it returns, and create inboxes on any subdomain on demand. Build agents that spin up addresses like [email protected] or [email protected] the moment you need them.
What’s new?
New features:
- Subdomains: Opt in per domain with
subdomains_enabled. When enabled, the domain’s verification records include a wildcard MX record (*.<domain>) to publish on the top-level domain. Once it is published and verified, inboxes can be created on any subdomain of that domain.
Changes:
POST /v0/domainsaccepts an optionalsubdomains_enabledflag (defaults tofalse).PATCH /v0/domains/:domain_idnow acceptssubdomains_enabledand applies partial updates: send at least one offeedback_enabledorsubdomains_enabled, and omitted fields are left unchanged. Enabling subdomains on an already-verified domain returns it topendinguntil the new wildcard MX record is published; sending is not interrupted.- Domain responses now include the
subdomains_enabledfield. - Creating an inbox on a subdomain of a domain that does not have subdomains enabled returns a
422error.
Use cases
Build agents that:
- Provision a dedicated inbox per customer or workspace under one verified domain (
[email protected]) - Separate agent traffic onto purpose-named subdomains (
billing.,outreach.,support.) without registering each one - Stand up short-lived inboxes on fresh subdomains for one-off tasks, then tear them down
- Keep all agent addresses under a single domain you verify and manage once
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# Enable subdomains on a verified domain
domain = client.domains.update("example.com", subdomains_enabled=True)
# Publish the new wildcard MX record, then create inboxes on any subdomain
inbox = client.inboxes.create(username="agent", domain="bot.example.com")
print(inbox.inbox_id) # [email protected]Learn more in the Setting Up Subdomains guide.
You can now search messages and threads by keyword. Full-text search ranks results by relevance across the sender, recipients, subject, and message body, and works per-inbox or across your entire organization. List endpoints also gained substring filters, so you can narrow a list to a specific sender, recipient, or subject without paging through everything. Build agents that find the right conversation instead of scanning every thread.
What’s new?
New endpoints:
GET /v0/inboxes/:inbox_id/messages/search- Full-text search of messages in an inbox, ranked by relevance.GET /v0/threads/search- Org-wide full-text search across threads in every inbox.GET /v0/inboxes/:inbox_id/threads/search- Full-text search of threads in a single inbox.GET /v0/pods/:pod_id/threads/search- Full-text search of threads in a pod.
New features:
- Full-text search: A
qquery matches against the sender, recipients, and subject (substring) and the message body (tokenized full text). Results are ordered by relevance. Spam, trash, blocked, and unauthenticated items are always excluded, andlimitis capped at 100. - Match highlights: Each search result includes an optional
highlightsobject with the matched fragments per field, with matched terms wrapped in**. A field appears only when it matched, so the present keys also tell you which fields produced the hit.
Changes:
GET /v0/inboxes/:inbox_id/messagesnow acceptsfrom,to, andsubjectsubstring filters.tomatches theto,cc, orbccfields.GET /v0/threads,GET /v0/inboxes/:inbox_id/threads, andGET /v0/pods/:pod_id/threadsnow acceptsenders,recipients, andsubjectsubstring filters.- Filtered list requests are served by search and cap
limitat 100; results keep the usual newest-first ordering.
Use cases
Build agents that:
- Pull up every thread mentioning an order number, invoice, or customer name across all of your inboxes
- Find the conversation a reply belongs to by searching the subject or body, instead of paging through history
- Narrow a list to a single sender or recipient before processing, using the new substring filters
- Surface the matched snippet to a human reviewer using per-field
highlights
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# org-wide full-text search across every inbox
results = client.threads.search(q="invoice overdue")
for thread in results.threads:
print(thread.thread_id, thread.subject)
# highlights tells you which fields matched
if thread.highlights:
print(thread.highlights)
# scope a search to one inbox's messages
inbox_results = client.inboxes.messages.search(
inbox_id="[email protected]",
q="refund requested",
)
# or just filter a list by subject, no relevance ranking
filtered = client.inboxes.messages.list(
inbox_id="[email protected]",
subject=["invoice"],
)Inboxes now support custom metadata: your own key-value data attached to any inbox. Link an inbox to records in your own system, such as a tenant ID, user ID, or feature flags, and read it back on every inbox response. Build agents that carry your application’s context wherever an inbox goes.
What’s new?
New features:
- Inbox metadata: Attach custom key-value pairs to an inbox. Values may be a string, number, or boolean, with up to 256 keys per inbox.
Changes:
- The
Inboxobject now includes an optionalmetadatafield, returned on get, list, and create responses. POST /v0/inboxesaccepts ametadatafield to set metadata at creation time.PATCH /v0/inboxes/:inbox_idaccepts ametadatafield. Updates merge into existing metadata: keys you include are added or overwritten, and keys you omit are preserved. Send a key with a null value to remove it, or setmetadatato null to clear everything. Each update must include at least one ofdisplay_nameormetadata.
Use cases
Build agents that:
- Tag each inbox with a tenant or customer ID so you can map inboxes back to your own data model
- Store per-inbox feature flags or routing hints that your agent reads at runtime
- Track lifecycle state, such as an onboarding step or campaign name, directly on the inbox
- Filter and organize a large fleet of inboxes by the attributes that matter to your application
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# attach metadata when creating an inbox
inbox = client.inboxes.create(
username="support-agent",
metadata={"tenant_id": "acme", "tier": "pro", "active": True},
)
# merge in a change; omitted keys are preserved
client.inboxes.update(
inbox_id=inbox.inbox_id,
metadata={"tier": "enterprise"},
)Learn more about attaching and updating inbox data in the Inboxes metadata guide.
Inbox-scoped API keys let you generate credentials that are restricted to a single inbox. This gives agents and integrations the minimum access they need, reducing the blast radius if a key is compromised.
What’s new?
New endpoints:
GET /v0/inboxes/:inbox_id/api-keys- List all API keys scoped to an inboxPOST /v0/inboxes/:inbox_id/api-keys- Create an API key scoped to an inboxDELETE /v0/inboxes/:inbox_id/api-keys/:api_key- Delete an inbox-scoped API key
Updated types:
ApiKeyandCreateApiKeyResponsenow include an optionalinbox_idfield when the key is scoped to an inbox
Use cases
Build agents that:
- Operate with least-privilege access to a single inbox rather than an entire pod or organization
- Issue short-lived, narrowly scoped keys to third-party integrations that only need access to one address
- Rotate credentials per inbox without affecting other inboxes or pods
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# create an api key scoped to a single inbox
key = client.inboxes.api_keys.create(
inbox_id="[email protected]",
name="integration-key"
)
print(key.api_key)Learn more about API key scoping in the API Keys reference.
Webhooks & Events – receive email and domain events via HTTP callbacks. Subscribe to message lifecycle events (received, sent, delivered, bounced, complained, rejected) and domain verification. Use Svix headers for verification and filter by inbox or pod. Perfect for agents that need reliable, async notifications without keeping a WebSocket open.
What’s new?
Webhook events:
message.received- New inbound emailmessage.sent- Outbound message sentmessage.delivered- Delivery confirmedmessage.bounced- Bounce (with type and recipients)message.complained- Spam complaintmessage.rejected- Rejection (e.g. validation)domain.verified- Domain verification succeeded
Delivery & verification:
- Svix-style headers:
svix-id,svix-signature,svix-timestampfor verification - Filter by inbox or pod (up to 10 per webhook)
- Payloads include inbox_id, thread_id, message_id, timestamps, and event-specific data
Use cases
Build agents that:
- React to new emails, bounces, and complaints via HTTP
- Sync email state to your database or queue
- Trigger workflows on domain verification
- Verify webhook signatures for security
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# in your webhook handler: verify signature and handle event
# (use Svix or the raw headers for verification)
def handle_webhook(request):
event_id = request.headers.get("svix-id")
signature = request.headers.get("svix-signature")
payload = request.json()
if payload.get("event_type") == "message.received":
message = payload.get("message")
# process new email
elif payload.get("event_type") == "domain.verified":
domain = payload.get("domain")
# domain is verifiedSet up and verify webhooks in our Webhooks documentation.
Introducing Custom Domains – add and verify your own domains for sending and receiving email. Use DNS verification (TXT, MX), export zone files for easy DNS setup, and control feedback (bounce and complaint) delivery. Perfect for agents that need to send from your brand’s domain with full control over deliverability.
What’s new?
New endpoints:
GET /domains- List all domainsGET /domains/{domain_id}- Get domain details and verification recordsPOST /domains- Create (add) a domainDELETE /domains/{domain_id}- Remove a domainGET /domains/{domain_id}/zone-file- Download zone file for DNS setupPOST /domains/{domain_id}/verify- Trigger domain verification
Domain features:
- DNS verification with TXT and MX records
- Verification status: NOT_STARTED, PENDING, VERIFYING, VERIFIED, FAILED, INVALID
- Per-record status (MISSING, INVALID, VALID) for targeted fixes
- Zone file export for quick import at your DNS provider
- Optional feedback (bounce/complaint) delivery per domain
Use cases
Build systems where:
- Agents send from your verified custom domain
- You manage DNS in one place and sync via zone file
- Verification status drives onboarding or monitoring
- Bounce and complaint handling is configured per domain
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# create a domain
domain = client.domains.create(
domain="mail.example.com",
feedback_enabled=True
)
# get verification records and status
domain = client.domains.get(domain_id=domain.domain_id)
for record in domain.records:
print(f"{record.type} {record.name}: {record.status}")
# trigger verification after updating DNS
client.domains.verify(domain_id=domain.domain_id)Learn more in our Custom Domains and Managing Domains guides.
Introducing the Drafts API – compose and manage email drafts before sending. Create drafts, update them over time, schedule send times, and send when ready. Perfect for agents that need to build messages incrementally, support reply threading, or queue emails for later delivery.
What’s new?
New endpoints:
GET /drafts- List all drafts (with optional filters)GET /drafts/{draft_id}- Get a draftPOST /inboxes/{inbox_id}/drafts- Create a draft in an inboxPATCH /inboxes/{inbox_id}/drafts/{draft_id}- Update a draftPOST /inboxes/{inbox_id}/drafts/{draft_id}/send- Send a draftDELETE /inboxes/{inbox_id}/drafts/{draft_id}- Delete a draft
Draft features:
- Compose with to, cc, bcc, subject, plain text, and HTML body
- Reply threading via
in_reply_toandreferences - Schedule send with
send_atfor delayed delivery - Attachments and labels
- List and filter drafts by inbox, labels, or time range
Use cases
Build agents that:
- Compose multi-step replies before sending
- Schedule follow-up emails for optimal delivery
- Queue outbound messages and send in batches
- Edit drafts based on new context or user feedback
- Maintain proper email threads with
in_reply_to
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# create a draft in an inbox
draft = client.inboxes.drafts.create(
inbox_id="[email protected]",
to=["[email protected]"],
subject="Re: Your request",
text="We're looking into it.",
in_reply_to="<[email protected]>"
)
# update the draft
client.inboxes.drafts.update(
inbox_id="[email protected]",
draft_id=draft.draft_id,
text="We've resolved your request."
)
# send the draft
client.inboxes.drafts.send(
inbox_id="[email protected]",
draft_id=draft.draft_id
)Learn more about composing and sending in our Drafts documentation.
We’re excited to introduce Metrics Endpoints - two new powerful endpoints that give you deep insights into your email deliverability and agent performance. Track critical events like bounces, deliveries, rejections, and complaints with detailed timestamps to build smarter, self-optimizing email agents.
What’s new?
New endpoints:
GET /metrics- Get comprehensive metrics across all your inboxesGET /inboxes/{inbox_id}/metrics- Get metrics for a specific inbox
Metrics tracked:
- Delivery events: sent, delivered, bounced, rejected
- Error tracking: complaints, spam reports
- Time-series data with detailed timestamps
Use cases
Build agents that:
- Monitor their own bounce rates in real-time
- Optimize send timing based on historical performance
- Automatically adjust behavior based on deliverability metrics
- Pause campaigns when performance drops below thresholds
- Implement intelligent retry strategies for better inbox placement
Ready to build smarter agents? Check out our Metrics API documentation to get started.
Introducing WebSocket Streaming - receive email events in real-time as they happen. Build reactive agents that respond instantly to new messages, deliveries, and bounces without polling. Perfect for building interactive, event-driven email experiences.
What’s new?
WebSocket endpoint:
wss://ws.agentmail.to/v0- Real-time event streaming
Events streamed:
message.received- New inbound email detectedmessage.sent- Outbound email sent successfullymessage.delivered- Delivery confirmed by recipient servermessage.bounced- Bounce detected (permanent or temporary)message.complained- Spam complaint received
Connection features:
- JWT-based authentication for secure connections
- Automatic reconnection with exponential backoff
- Event filtering by inbox for targeted subscriptions
- Low-latency delivery (typically under 100ms)
- Support for thousands of concurrent connections
Use cases
Build agents that:
- Respond to emails within seconds of receipt
- Monitor deliverability in real-time across all inboxes
- Trigger workflows instantly on specific events
- Build interactive conversational email experiences
- Scale to handle high-volume email operations
- React to bounces and complaints immediately
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# subscribe to events for an inbox
async with client.websockets.subscribe(
inbox_id="[email protected]"
) as ws:
async for event in ws:
if event.type == "message.received":
print(f"New email from: {event.data.from_}")
response = await generate_response(event.data.text)
await client.messages.reply(
message_id=event.data.message_id,
text=response
)Get started with WebSocket Streaming to build real-time email agents.
Introducing Pods - team collaboration spaces for AgentMail. Share inboxes, domains, and resources across your organization while maintaining granular control. Perfect for teams building multi-agent email systems that need organized resource management.
What’s new?
New endpoints:
POST /pods- Create a new pod (team workspace)GET /pods- List all pods in your organizationGET /pods/{pod_id}- Get pod detailsDELETE /pods/{pod_id}- Delete a podPOST /pods/{pod_id}/inboxes- Create inbox within a podPOST /pods/{pod_id}/domains- Add custom domain to a podGET /pods/{pod_id}/threads- List threads within a podGET /pods/{pod_id}/metrics- Get metrics for a pod
Pod features:
- Shared inbox access across team members
- Per-pod domain configuration
- Isolated metrics and analytics per pod
- Organized resource hierarchy
Use cases
Build systems where:
- Multiple agents share email infrastructure
- Different teams manage their own inboxes independently
- Resources are organized by department or project
- Analytics are tracked per team workspace
- Billing and usage can be attributed to specific teams
from agentmail import AgentMail
client = AgentMail(api_key="your-api-key")
# create a pod for your sales team
pod = client.pods.create(
name="Sales Team",
description="Shared resources for sales agents"
)
# create an inbox in the pod
inbox = client.pods.inboxes.create(
pod_id=pod.pod_id,
inbox_id="[email protected]"
)
# list all pods
pods = client.pods.list()
for pod in pods.pods:
print(f"Pod: {pod.name} ({len(pod.inbox_ids)} inboxes)")Learn more about organizing teams with Pods in our documentation.