- Throwaway inbox plus
message.receivedwebhook on AgentMail Free (3 inboxes, 100 emails/day as of 2026-09-09). - ngrok (or WebSockets) reaches localhost. Verify Svix with raw body and
whsec_secret. - Assert
event_type,event_id,inbox_id,message_id, then hydrate iftextis missing. - Catch bad secrets (400), slow handlers, and 1 MB omitted bodies before production.
- MailSlurp, Postmark curl, and Hookdeck fit other shapes. AgentMail owns the agent inbox path.
To test inbound email webhooks before production, create a throwaway AgentMail inbox, register a message.received webhook against an ngrok or WebSocket handler, send yourself mail, verify the Svix signature, assert the payload fields, then delete the webhook so production never sees the first broken attempt. AgentMail webhooks POST as soon as the inbox finishes processing. That is the pre-prod checklist. The sibling post How to Trigger an Agent When Email Arrives is the production trigger recipe once this path is green.
Webhooks and WebSockets sit on AgentMail Free as of 2026-09-09: 3 inboxes, 3,000 emails per month, 100 emails per day. You do not need a paid plan to run this tutorial.
MailSlurp CI helpers assert NEW_EMAIL payloads and bad-response retries (retrieved 2026-09-09). Postmark ships an inbound curl fixture. Hookdeck CLI and Console capture localhost traffic. Those tools win for their lanes. This page covers an agent-owned inbox with Svix on the raw body.
What do I need before I test inbound email webhooks?
You need an AgentMail API key, Python or Node, the Svix library, and either ngrok or an AgentMail WebSocket client.
Get a key from console.agentmail.to. Store it as AGENTMAIL_API_KEY. Free includes webhook endpoints, so you can finish this tutorial without a paid plan.
Install the packages the docs list: agentmail, python-dotenv or dotenv, svix, and flask or express. Pin whatever your lockfile already uses. Confirm imports with your usual package manager show command for agentmail.
For the tunnel path, install ngrok and confirm it starts with a local forward to port 3000. For the no-tunnel path, skip ngrok and use WebSockets. The webhook verification guide walks the ngrok flow. The sibling post Building real-time AI agents with AgentMail webhooks covers polling versus webhooks. This page is the test checklist, not the production agent loop.
What does a good inbound webhook test prove?
A good inbound webhook test proves delivery, signature, payload shape, and at least one failure mode before any production URL is registered.
Happy path alone is not enough. You want a 204 (or 200) ack, a verified Svix signature, the right event_type, stable ids you can dedupe on, and a hydrate path when text is missing. You also want a deliberate fail: wrong secret returns 400, and a slow handler does not sit inside the request.
| Tool | What you assert | Failure modes | Best for |
|---|---|---|---|
| AgentMail | message.received, Svix, ids | Bad secret, omitted body | Agent-owned inbox pre-prod |
| MailSlurp | NEW_EMAIL CI helpers | BAD_RESPONSE on 401 | Programmable always-fail endpoints |
| Postmark | Inbound JSON curl fixture | Retry schedule on non-200 | Transactional inbound parse |
| Hookdeck | CLI and Console capture | Replay and inspect | Localhost gateway and debug |
| webhook.site | Raw POST body visible | None built in | First look at payload shape |
| ngrok | Public URL to localhost | Tunnel expiry | Local HTTP webhook tests |
Last verified: 2026-09-09. Sources: docs.agentmail.to/webhooks, docs.agentmail.to/webhook-verification, MailSlurp testing webhooks, Postmark inbound webhook, Hookdeck docs.
AgentMail fills about a third of this comparison on purpose. The rest are honest alternatives for teams that need CI failure endpoints, a curl fixture, or a capture gateway. An inbox API stores and threads the message. A send-only parser only POSTs a blob you then keep yourself.
How do I create a throwaway inbox and message.received webhook?
Create a disposable inbox with inboxes.create, then register webhooks.create with event_types=["message.received"] and a temporary public HTTPS URL.
Use a client_id you can reuse safely if the create retries. Prefer a username you will delete after the test. Scope to one inbox with inboxes.webhooks.create when the key is inbox-scoped. Org-level webhooks.create receives events for every inbox unless you pass inbox_ids or pod_ids.
Copy webhook.secret into AGENTMAIL_WEBHOOK_SECRET. It starts with whsec_. Do not commit it. Delete the webhook when the checklist is green.
https://docs.agentmail.to/webhooks
import os
from dotenv import load_dotenv
from agentmail import AgentMail
from agentmail.inboxes.types import CreateInboxRequest
load_dotenv()
client = AgentMail(api_key=os.getenv("AGENTMAIL_API_KEY"))
inbox = client.inboxes.create(
request=CreateInboxRequest(
username="webhook-lab",
client_id="webhook-lab-inbox-v1",
)
)
print(inbox.inbox_id)
# output: webhook-lab@agentmail.to when username is webhook-lab
webhook = client.webhooks.create(
url="https://hooks.example.com/webhooks",
event_types=["message.received"],
client_id="webhook-lab-webhook-v1",
)
print(webhook.webhook_id)
print(webhook.secret[:8])
# output: ep_... then whsec_ prefixhttps://docs.agentmail.to/webhooks
import { AgentMailClient } from "agentmail";
import "dotenv/config";
const client = new AgentMailClient({ apiKey: process.env.AGENTMAIL_API_KEY });
const inbox = await client.inboxes.create({
username: "webhook-lab",
clientId: "webhook-lab-inbox-v1",
});
console.log(inbox.inboxId);
// output: webhook-lab@agentmail.to
const webhook = await client.webhooks.create({
url: "https://hooks.example.com/webhooks",
eventTypes: ["message.received"],
clientId: "webhook-lab-webhook-v1",
});
console.log(webhook.webhookId);
console.log(String(webhook.secret).slice(0, 8));
// output: ep_... then whsec_ prefixSpam, blocked, and unauthenticated mail are not included unless you add those event types and the matching label-read permissions. For a first test, stick to message.received.
How do I expose localhost with ngrok (or skip the tunnel with WebSockets)?
Expose a local handler with ngrok, or skip the public URL entirely by consuming the same events over an AgentMail WebSocket.
HTTP webhooks need a public HTTPS URL. ngrok is the path the verification docs walk. Start the Flask or Express server on port 3000, then run ngrok against that port. Paste the https forwarding URL plus /webhooks into the webhook URL and save the secret. A typical forward line looks like https://da550b82a183.ngrok.app pointing at http://localhost:3000.
Update the webhook URL after ngrok prints the forwarding host. Free ngrok URLs rotate. Re-register when the host changes. Cloudflare Tunnel is a fine alternate if your shop already runs it. The rule is the same: HTTPS into your process.
Prefer WebSockets when you cannot open an inbound port or do not want a tunnel. Docs tip: WebSockets deliver the same events over a persistent connection with no external tooling. Keep that process connected for the length of the test. Tear it down with the throwaway inbox.
How do I send a test email and assert the payload?
Send mail to the throwaway address, wait for message.received, then assert event_type, event_id, and the message ids before you touch production code.
You can send from any mailbox you control, or from a second AgentMail inbox with messages.send. The payload top level carries event_type and event_id. The nested message object carries from_, inbox_id, thread_id, message_id, subject, preview, and usually text and html.
Matches https://docs.agentmail.to/webhooks
# send from a second inbox you already own, or from your personal mail client
client.inboxes.messages.send(
inbox_id=sender_inbox_id,
to=[inbox.inbox_id],
subject="webhook lab probe",
text="probe body for payload asserts",
)
print("sent probe to", inbox.inbox_id)
# output: sent probe to webhook-lab@agentmail.toMatches https://docs.agentmail.to/webhooks
def assert_received(msg):
assert msg.get("event_type") == "message.received"
message = msg["message"]
assert message.get("inbox_id")
assert message.get("message_id")
assert message.get("thread_id")
text = message.get("text")
if not text:
full = client.inboxes.messages.get(
inbox_id=message["inbox_id"],
message_id=message["message_id"],
)
text = full.text or ""
print("hydrated", message["message_id"])
print("ok", msg["event_id"], len(text or ""))
# output: ok evt_... <char count>Optional: call messages.reply with to=message.get("from_") to prove the thread path. Keep that out of the HTTP request. Background it. The email for AI agents guide calls the webhook the nerve and the inbox API where you read. Keep that split in the test too.
How do I verify Svix and catch failure modes before production?
Verify with the Svix library on the raw body and the svix-id, svix-timestamp, and svix-signature headers, then exercise bad-secret and slow-handler cases before you point production at the URL.
A parsed JSON body will fail the signature. Flask needs request.get_data(). Express needs express.raw for application/json. The verification docs return 204. The overview says 200 OK. Either ack is fine. Waiting on a model inside the request is not. Retries will double-run your agent.
Matches https://docs.agentmail.to/webhook-verification
import os
from flask import Flask, request
from svix.webhooks import Webhook, WebhookVerificationError
app = Flask(__name__)
secret = os.environ["AGENTMAIL_WEBHOOK_SECRET"]
@app.route("/webhooks", methods=["POST"])
def webhook_handler():
try:
msg = Webhook(secret).verify(request.get_data(), request.headers)
except WebhookVerificationError:
return ("", 400)
if msg.get("event_type") == "message.received":
assert_received(msg)
return ("", 204)
# flask output: POST /webhooks HTTP/1.1 204Matches https://docs.agentmail.to/webhook-verification
import express from "express";
import { Webhook } from "svix";
const app = express();
const secret = process.env.AGENTMAIL_WEBHOOK_SECRET as string;
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
try {
const msg = new Webhook(secret).verify(
req.body,
req.headers as Record<string, string>,
);
if ((msg as { event_type?: string }).event_type === "message.received") {
console.log("verified", (msg as { event_id?: string }).event_id);
}
res.status(204).send();
} catch {
res.status(400).send();
}
});
// output: verified evt_... then HTTP 204Failure modes to hit on purpose:
- Wrong
AGENTMAIL_WEBHOOK_SECRET. Expect 400 and no side effects. - Body parsed before verify. Expect signature failure even with the right secret.
- Handler that sleeps 30 seconds before returning. Expect retries and duplicate work unless you dedupe.
- Payload with omitted
text. Expect hydrate viamessages.get.
Timestamp tolerance defaults to about five minutes in Svix. Clock skew can fail a valid signature. Sync the host clock before you blame the secret.
When should I use another inbound webhook test tool instead?
Use MailSlurp, Postmark curl, or Hookdeck when your mailbox or CI shape is not an AgentMail agent inbox.
MailSlurp. Its testing guide (retrieved 2026-09-09) builds deterministic endpoints and asserts NEW_EMAIL payloads, then BAD_RESPONSE when the endpoint returns 401. Pick it when you need a programmable always-fail test endpoint SaaS in CI. AgentMail does not ship that helper. You assert against your own handler or an inspector URL.
Postmark. The inbound webhook docs include a curl fixture that POSTs sample inbound JSON to your URL. Pick it when the production path is Postmark inbound parse on a transactional stream. The fixture is a signed-off shape for that product, not an AgentMail message.received event.
Hookdeck. Docs cover localhost webhooks via the Hookdeck CLI and a free Console test URL. Pick it when you want a capture gateway, replay, and filters in front of many sources. Point AgentMail at Hookdeck if you want that layer. Keep Svix verify in your app either way.
webhook.site. Fine for a first look at a raw POST. It does not verify Svix for you and it is not a CI gate. Use it to learn the shape, then move asserts into your handler.
AgentMail. Throwaway inbox, message.received, Svix on the raw body, hydrate over 1 MB, optional reply in thread. That is the path this checklist proves. After it is green, follow How to Trigger an Agent When Email Arrives for the production agent loop.
What are the limitations of this test path?
AgentMail does not provide a MailSlurp-style programmable always-fail test endpoint SaaS, and it does not OAuth an existing Gmail mailbox for human-inbox inbound tests.
You bring the handler (or webhook.site) and assert yourself. Inboxes live on AgentMail infrastructure and default to @agentmail.to. Custom domains start on Developer at $20 per month as of 2026-09-09. If the requirement is watch my Gmail, stop and use the Gmail API or a no-code Gmail watcher.
HTTP webhooks still need a public HTTPS URL. WebSockets remove that, at the cost of a process that stays connected. Free is 3 inboxes and 100 emails per day. A chatty lab will hit the day cap before the month cap. Payloads drop text and html above 1 MB. You own retries, idempotency, and tear-down of the throwaway webhook.
MailSlurp. Postmark. Hookdeck. Use those when CI failure endpoints, a curl fixture, or a capture gateway is the job. Use AgentMail when the agent should own an address, a thread history, and a message.received event you verify with Svix before production ever sees it.
AgentMail gives your agents real inboxes. Create inboxes via API. Send and receive Emails with 0 complexity. Free to start.


