Skip to content
Blog / News

Introducing Polling Webhooks

Binoy Perera

Polling webhooks let an AI agent fetch its email events from a queue whenever it runs, with no public URL or open connection. Built in partnership with Svix.

  • webhooks
  • polling-webhooks
  • svix
  • ai-agents
  • serverless

TL;DR

  • Polling webhooks let an AI agent fetch its email events from a queue whenever it runs, with no public URL and no open connection.
  • If the agent is offline, events wait for up to 90 days and it picks up where it left off. That fits agents in sandboxes, serverless functions, cron jobs and laptops.
  • We built polling webhooks in partnership with Svix, who already deliver AgentMail's webhooks. They're available now in the Python and TypeScript SDKs, in the US region.

An agent that handles email needs to know when a message arrives. Until now, AgentMail gave you two ways to find out: a push webhook, where we send a POST to a URL you host, or a WebSocket, where you keep a connection open and events stream in.

Both assume your agent runs on infrastructure that's always there. A lot of agents don't. They run in a Claude Code or Codex sandbox, inside a Lambda or Modal function, as a cron job, on a laptop, or behind a corporate firewall. Some of those have no public address at all, and most of them aren't running most of the time.

Polling webhooks are for those agents. You subscribe to the same event types as a regular webhook, and AgentMail queues the matching events. Your agent fetches them when it runs, marks what it handled, and the next fetch starts from there. We built it in partnership with Svix, the team that already delivers our push webhooks.

Why do agents need polling webhooks?

A push webhook needs a public endpoint that returns a 2xx within 15 seconds. If your code isn't up, we retry eight times over about 27.5 hours. If every delivery fails for five days, the endpoint is disabled. You don't get a notification, and events from the gap are not redelivered.

That works for a server that's always up. An agent that wakes for twenty minutes a day has no URL to give us, and nothing listening when the retries come.

A WebSocket gets rid of the URL and replaces it with an uptime requirement. The connection has to stay open, and an event that arrives while the agent isn't connected never reaches it.

Both options were designed around a process that keeps running. Agents are often short-lived processes that start, do some work and exit.

How is a polling webhook different from a regular webhook?

A polling webhook is a webhook you read instead of one we deliver. Nothing in your setup has to be reachable from the internet. It covers the same events a regular webhook does, from message.received to the calendar events.

Push webhookWebSocketPolling webhook
Public URLRequiredNot requiredNot required
Open connectionNot requiredRequiredNot required
While the agent is offlineRetried, then the endpoint is disabledEvents are missedEvents wait for up to 90 days
When events arriveAs they happenAs they happenOn the agent's next fetch
FitsServersLong-running processes that need low latencySandboxes, serverless functions, cron jobs, laptops

Svix runs the queue, on the same infrastructure that already delivers our push webhooks. Your agent reads it with the AutoConfigConsumer from the Svix SDK.

What can you build with polling webhooks?

  • A coding agent that checks its inbox when it starts. A Claude Code or Codex session drains the queue on startup, handles every message that arrived since its last run, and commits before it exits. Nothing has to run between sessions.
  • A serverless support agent. A Lambda or Modal function runs every few minutes, fetches new message.received events and replies. Each reply goes out on the first run after the email arrives.
  • An agent behind a corporate firewall. The agent only makes outbound requests to read the queue, so it works on a network that blocks inbound traffic. On a laptop, it picks up the backlog when the machine wakes.
  • A second reader for the same events. Give an audit log or an analytics job its own consumer ID on the same token. It sees every event without moving the main worker's position.
  • A scheduled job that reacts to calendar events. Subscribe to calendar.event.starting, and the job picks up the meetings that started since its last run. It acts up to one interval late, so anything that has to happen on the minute belongs on a push webhook or a WebSocket.

How do you set up a polling webhook?

Install the AgentMail SDK and the Svix SDK. You need both: AgentMail creates the subscription, and Svix reads the queue.

Bash

pip install agentmail svix# ornpm install agentmail svix

Create the polling webhook. client_id is required.

Python

from agentmail import AgentMailclient = AgentMail()subscription = client.webhooks.polling.create(    client_id="support-agent",    event_types=["message.received"],    inbox_ids=["support@yourdomain.com"],)token = subscription.token

TypeScript

import { AgentMailClient } from "agentmail";const client = new AgentMailClient();const subscription = await client.webhooks.polling.create({  clientId: "support-agent",  eventTypes: ["message.received"],  inboxIds: ["support@yourdomain.com"],});const token = subscription.token;

Leave out inbox_ids to cover the whole organization. To scope it to one pod or inbox, create it through client.pods.webhooks.polling or client.inboxes.webhooks.polling instead.

The token only comes back from create. get and list never return it, so store it like an API key. If you lose it, call create again with the same client_id. You get the same subscription with a new token, and the old token stops working right away. That's also why each running worker should have its own client_id.

Then read events with the Svix consumer. Pass it an empty configuration, because AgentMail already set up the subscription.

Python

import timefrom svix import AutoConfigConsumerfrom svix.models import SinkInCommonconsumer = AutoConfigConsumer(token, SinkInCommon())while True:    page = consumer.receive("worker-1")    for event in page.data:        if event.event_type == "message.received":            handle(event.payload["message"])        consumer.commit("worker-1", event.offset)    if page.done:        time.sleep(5)

TypeScript

import { AutoConfigConsumer } from "svix";const consumer = new AutoConfigConsumer(token, {});while (true) {  const page = await consumer.receive("worker-1", { limit: 50 });  for (const event of page.data) {    if (event.eventType === "message.received") {      handle(event.payload.message);    }    await consumer.commit("worker-1", event.offset);  }  if (page.done) await new Promise((r) => setTimeout(r, 5000));}

event.payload is the same body a push webhook would have sent, so you can reuse the handlers you wrote for push webhooks. Don't call subscribe() on the consumer. It replaces the event types and inboxes AgentMail configured.

An agent that runs on a schedule doesn't need the loop. It calls receive until page.done is true, commits what it handled, and exits.

What happens when the agent goes offline?

Events queue. When the agent starts again, receive returns the backlog until page.done is true.

commit acknowledges an event and everything before it. Anything you haven't committed comes back on the next receive, so if the agent crashes between handling an event and committing it, the event is still there next time. Delivery is at least once, which means a handler can see the same event twice. Make handlers idempotent.

The "worker-1" in the code is a consumer ID, and each consumer ID keeps its own position in the queue. Two consumer IDs on one token each see every event, which is how you add a second worker that needs the full stream, or replay events without a second subscription. Don't run two processes on the same consumer ID at the same time.

Queued events are kept for 90 days. The queue only holds events from after the polling webhook was created, so create it before the agent needs it.

Which delivery method should you use?

If your agent runs intermittently, or anywhere that can't take an inbound request, use a polling webhook. That covers most agents in sandboxes, serverless functions and cron jobs.

WebSockets are the better choice when you need an event within a second and can keep a process connected. Push webhooks still make sense if you already run a server with a public URL.

All three carry the same event types and the same payloads. Switching later changes how events reach the agent, and the handlers stay the same.

What are the limits?

  • Polling webhooks are available in the US region only for now. They aren't in the EU yet.
  • Each organization, pod and inbox can have up to 10 polling webhooks. Creating an eleventh returns a 403. Calling create again with an existing client_id doesn't count toward the limit.
  • One polling webhook can cover up to 10 pods and inboxes.
  • A token can only read the event types the API key could read when the token was created. If the key loses a permission, delete the polling webhook and create it again.
  • message.received.spam, message.received.blocked and message.received.unauthenticated need the matching permission on the API key, the same as for push webhooks.
  • Events arrive on the agent's next fetch, so latency is higher than with push webhooks or WebSockets.

Hand this to your coding agent

Paste this into Claude Code, Codex or Cursor to set up a polling webhook in your project.

Code

Set up an AgentMail polling webhook so this agent can receive email events without a public URL.Read https://docs.agentmail.to/webhook-polling before writing any code.1. Install the AgentMail and Svix SDKs (pip install agentmail svix, or npm install agentmail svix).2. Create the polling webhook with client.webhooks.polling.create. Use client_id "<AGENT_NAME>", event_types ["message.received"] and inbox_ids ["<INBOX_ADDRESS>"].3. Store the returned token as a secret, never in code. It is only returned once.4. Read events with the Svix SDK's AutoConfigConsumer, passing the token and an empty config. Call receive with consumer ID "worker-1", handle each event, then commit its offset. Do not call subscribe().5. Make the handler idempotent, because delivery is at least once.6. If this agent runs on a schedule, drain the queue until page.done is true, then exit.

FAQ

Questions, answered.

No. Nothing in the setup has to be reachable from the internet. AgentMail queues the events and your agent fetches them with the Svix SDK whenever it runs.

Events you have not committed come back on the next receive. Delivery is at least once, so make handlers idempotent.

Queued events are kept for 90 days. The queue only holds events from after the polling webhook was created.

Yes, with different consumer IDs. Each consumer ID keeps its own position, so each one sees every event. Do not run two processes on the same consumer ID at the same time.

Not yet. Polling webhooks are available in the US region only for now.

A WebSocket needs a connection that stays open, and events that arrive while the agent is disconnected are missed. A polling webhook queues events until the agent fetches them, with higher latency.

Blog

Suggested reading

View all posts

Developer Resources · November 3, 2026

How to Give Your AI Agent a Calendar

A step-by-step guide to giving an AI agent its own calendar with AgentMail: create the inbox, book meetings, answer invitations and act when meetings start, end or move.

Essays · March 6, 2026

We Do Not Build for Humans

Agents are discovering AgentMail on their own, signing up autonomously, and sending us support emails. So we built a CLI for them.

Prompt

Give yourself access to AgentMail by following the guide at:

https://docs.agentmail.to/agent-onboarding.md

and refer to:

https://docs.agentmail.to/llms.txt

for the documentation index.

Start integrating AgentMail into your AI agents today.