Integrations · September 22, 2026
Give a Zapier Agent Its Own Email Address
Zapier's email actions send as the account you connected. Give a Zapier agent an email address of its own: the send call, the key, and inbound.
Every email action in Zapier sends as the account you connected to it. The Gmail action
sends as the Google account that authorised the connection. Email by Zapier sends from a
zapiermail.com address and its own help page says plainly that you cannot customise the
From address. Neither of those is the agent. Replies land in a human’s mailbox, the
complaints land on a human’s reputation, and the From line names a person who did not
write the message.
This page wires the other arrangement: a Zapier agent with an email address of its own, on an inbox the project owns, driven by a Webhooks by Zapier action against the messages endpoint. If the prior question is whether the agent should have a separate address at all, why an agent needs its own address settles that first.
What each Zapier email path puts on the From line
Three ways to get mail out of a Zap, and they differ on the only thing that matters here.
| Path | From line | Inbound | Ceiling |
|---|---|---|---|
| Gmail / Outlook action | the human who authorised OAuth | that person’s real mailbox | that provider’s sending policy |
| Email by Zapier | a zapiermail.com address, fixed | a generated zapiermail.com address | 5 a day on Free or trial, 10 an hour on paid |
| Webhooks action to this API | an inbox the project owns | a signed message.received callback | the project’s own allowance |
Email by Zapier is the path most “give your agent an email address” tutorials reach for, and two of its documented properties decide whether it can carry an agent at all. The send limit is per account, not per Zap: up to 5 emails per account per day on a Free or trial plan, and up to 10 emails per account per hour on a paid plan, shared across everyone on a Team or Enterprise account. And every message it sends carries a mandatory CAN-SPAM unsubscribe link, described in Zapier’s own documentation like this: when a recipient clicks the Stop these emails link, your Zap is turned off. One irritated recipient can therefore disable the automation for everyone, which is reasonable behaviour for a notification utility and unworkable for an agent that answers its own correspondence.
Row one stays correct for one case worth naming. If the job is to act inside a specific person’s real mailbox, replying to their threads under their name, a separate address is the wrong shape and the Gmail action is the right tool. Borrowing a credential is only a defect when the agent was supposed to have an identity of its own.
Row three is the rest of this page. Live mail is received and sent on
agents.emailforagents.ai, the REST base is https://api.emailforagents.ai/v1, and the
credential is a bearer token, which is exactly the shape Zapier’s Headers field expects.
The full surface is on the developer reference.
Giving a Zapier agent an email address of its own
Create the inbox once, outside Zapier, and keep the returned id:
curl -X POST "https://api.emailforagents.ai/v1/projects/$EFA_PROJECT_ID/inboxes" \
-H "Authorization: Bearer $EFA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"display_name":"Zapier concierge","local_part":"concierge"}'
That address, and the threads that accumulate against it, are what an agent-owned inbox means in practice. The Zap never creates inboxes; it sends from one.
Now the send step. Add a Webhooks by Zapier action and choose Custom Request, not POST. The distinction is not stylistic. The POST action’s Data field is a key/value editor, and the request body you need is not flat:
{
"to": [{ "email": "ap@example.com", "name": "Accounts Payable" }],
"subject": "Re: invoice 4471",
"reply_to_message_id": "msg_7f21c...",
"text": "Corrected receipt attached."
}
to is an array of objects with an email field. A bare "ap@example.com" is an HTTP
400, and so is a comma-separated list. Zapier’s own guidance is to reach for Custom
Request when you need nested JSON arrays or highly customised headers, and this request
needs both. Set Method to POST, put the JSON above in Data, and set three
Headers:
Authorization: Bearer efa_live_...
Content-Type: application/json
Idempotency-Key: <see the next section>
Four shape rules save a debugging session. The body schema is strict, so an unknown
top-level field is a rejection rather than a warning. subject is required and is 1 to
998 characters. Up to 25 unique recipients across To, Cc and Bcc combined, counted after
deduplication, so the same address in To and Cc costs one. And supplying
reply_to_message_id makes the request require messages:read on top of
messages:send, so a key minted with the send scope alone passes your first manual test
and then 403s on the first real reply.
Both success paths return HTTP 202, and you must branch on result rather than on the
status code:
"result": "queued"carriesmessage_id,thread_idandstate."result": "approval_required"carriesdraft_id,approval_idandreason_codes. No email exists yet. This is not an error and must not be retried.
Webhooks by Zapier is a premium app. Its own help page carries a plan table that, checked 22 September 2026, marks Free as excluded and Professional, Team and Enterprise as included, so on a Free plan this path does not exist at all.
The Idempotency-Key, and the Zap run ID that does not exist
Idempotency-Key is mandatory on this endpoint. A send without it fails with HTTP
400 invalid_request before the body is even parsed, which means a missing header masks
every other mistake in the step until you fix it.
The obvious thing to put in it is the Zap run ID. That field is not available to you.
Checked 22 September 2026, the system variables Zapier exposes to any step are
{{zap_meta_human_now}}, {{zap_meta_timestamp}}, {{zap_meta_utc_iso}} and their
timezone variants. All of them are clocks. No run identifier appears in the data picker,
and exposing the Zap run is a standing feature request rather than a field.
That absence is worth dwelling on, because the nearest substitute is actively harmful. A
key built from {{zap_meta_timestamp}} is evaluated afresh on every attempt, so it
changes precisely when you need it not to. It satisfies the header requirement and
provides none of the protection, which is the worst available combination.
Derive the key from the thing you meant to send instead. In the Idempotency-Key header
field, type the literal text reply-, insert the inbound message ID from the trigger
step using the data picker, then type -v1. The rendered header looks like this:
Idempotency-Key: reply-msg_7f21c8a4-v1
For an inbound-triggered Zap the event or message ID is already a stable unique name for the single reply you intend to make. Where there is no natural business key, compose one from trigger fields that identify the intent, such as an invoice number and a stage.
What the header guarantees is more precise than “no duplicates”. The request is normalised into a canonical payload and hashed, and the key is bound to that exact message rather than to the endpoint. Four outcomes follow, and each has its own code:
| Second request with the same key | Result |
|---|---|
| Identical payload, first completed | the stored 202 is replayed, no second email |
| Identical payload, first still in flight | HTTP 503 temporarily_unavailable, “A matching send is already in progress.” |
| Different payload | HTTP 409 idempotency_conflict |
| Identical payload after 30 days | HTTP 409 idempotency_expired |
The replay window is 30 days and the tombstone runs to 365, so a key is not reusable for
a different message a year later either. An idempotency_conflict is a bug in your key
derivation rather than something to retry around.
Idempotency keys: stop an agent sending duplicate emails
covers the quota consequences and why an unknown outcome must never be auto-resent.
What each of Zapier’s three replays does to your send
Zapier can re-run a step three different ways, and they are not equivalent. The table below is the whole reason to care which key you chose.
| Mechanism | Same Zap run? | What re-executes | Key from trigger data | Key from zap_meta_timestamp |
|---|---|---|---|---|
| Autoreplay | yes | the errored step only | safe | duplicate |
| Manual replay of errored steps | yes | the errored step only | safe | duplicate |
| Replay all steps, from the editor | no, a new run | every step including the trigger and successful ones | safe | duplicate |
Autoreplay is an account-wide setting on Professional plans and higher, and it backs off after each failed attempt on a published schedule of 5 minutes, 30 minutes, 1 hour, 3 hours and 6 hours. An error at 1:00 PM is therefore retried at 1:05, 1:35, 2:35, 5:35 and 11:35. A send step that timed out after the API had already accepted the message is exactly the case those five attempts are dangerous for, and exactly the case a stable key turns into five replays of one stored response.
The third row is the one that surprises people, and it is where Zapier is genuinely better behaved than most orchestrators. Replaying all steps from the editor creates a new run that appears separately in Zap history, and it re-executes successful steps as well as failed ones, counting each against your task usage a second time. In a lot of tools that would make a run-scoped key useless. Here it does not matter, because Zapier’s documentation states that a replayed run reuses the original run’s trigger data. Any key derived from trigger fields is therefore identical in the replay, the payload hashes to the same value, and the API returns the stored 202 rather than sending again. Zapier’s own replay semantics are what make a trigger-derived key survive all three mechanisms, and that is the property to design around rather than the run ID you cannot have.
One consequence for the shape of your Zap: a non-2xx response makes the step error, and
an errored step stops the run there, so a Filter or Paths step placed after it never sees
the body. You cannot branch on a 409 inside the Zap, and Autoreplay will retry it five
times to no purpose. That is the right reason to treat idempotency_conflict as
something to prevent in the key expression rather than something to handle downstream.
The 202 paths are different: both are successes, the step completes, and Paths can branch
on result normally.
Catching inbound mail without destroying the signature
Register a webhook against the project, subscribe to message.received, and point it at
a Zapier trigger URL. Inbound mail then starts a Zap in near real time.
Use Catch Raw Hook, not Catch Hook. Catch Hook parses the request body into mapped fields, and the parsed representation is not the bytes that were signed: key order and whitespace are gone before you ever see it, so no amount of re-serialising in a Code step recovers the original. Catch Raw Hook keeps the body unparsed, up to 2 MB, and includes the request headers, which is the only configuration in which verification is possible at all.
Each callback carries webhook-id (the event ID), webhook-timestamp (Unix seconds) and
webhook-signature, which is v1, followed by the base64 HMAC-SHA256 of
id.timestamp.rawBody keyed on the signing secret shown once at registration. Verify
against those exact bytes in a Code by Zapier step, reject anything whose timestamp is
more than five minutes off, and only then parse.
The events and webhooks reference has the verification code.
Then deduplicate. Delivery is retried up to eight attempts on a fixed backoff of 10s, 60s, 5m, 30m, 2h, 6h and 12h, and a 4xx is terminal except for 408, 409, 425 and 429, so a repeated callback is normal rather than exceptional. A Zap has no memory between runs on its own, so the event ID has to be written to something that does, such as Storage by Zapier or any keyed store the Zap can reach, and checked before the reply step runs.
Keep the event cursor as the recovery path. GET /v1/projects/{project_id}/events
returns ordered events with a next_cursor, and a scheduled Zap reading it catches
anything missed while the trigger URL was unreachable. Persist the cursor after the batch
is processed, not when it arrives. A stale cursor returns HTTP 410 cursor_expired with
retryable: false, meaning restart without a cursor rather than loop.
Connecting over MCP Client by Zapier instead
Zapier ships an MCP Client integration that connects a Zap to a remote MCP server, and it is a real alternative to the Webhooks path for the model-driven half of the work. Its connection form takes a Server URL, a transport choice of Streamable HTTP or SSE, an OAuth yes/no toggle and an optional Bearer Token. Two settings are forced here:
Set OAuth to Yes. Selecting yes causes the Bearer Token field to be ignored, which is
the correct outcome, because an API key is rejected at https://mcp.emailforagents.ai/mcp
by design. A bearer token that is not a connection grant comes back unauthenticated,
and a principal that is not a connection comes back permission_denied. Only an OAuth
connection grant may call the tools.
Choose Streamable HTTP. A GET on /mcp returns 405 with Allow: POST; the endpoint
speaks streamable HTTP only, so an SSE selection fails at connect time and reads like a
network problem that does not exist.
Tools then surface as Zap steps: Run Tool as an action returning one result, and Run
Read-Only Tool as a search returning one or more. The grant decides which tools exist at
all. Requesting the email.draft scope produces a session with five tools and no
send_message in it, so there is no send tool for a Zap step or a prompt-injected
instruction to name, and the agent still composes a fully formed reply through
create_draft, which cannot transmit mail. The presets are on
the integrations page, and the mechanics are in
the MCP email server walkthrough.
Two limits on this path. Zapier documents that MCP Client supports tool calls only, so resources and prompts are out of reach. And a connection grant is created with a 30-day expiry that nothing extends, after which it resolves as denied with no warning in the Zap editor. An API key on the Webhooks path has no equivalent clock, which is the argument for Webhooks when the Zap must run unattended past a month.
What is not shipped, and the limits that apply
There is no Email for Agents app in Zapier’s app directory, checked 22 September 2026, so Webhooks by Zapier is the integration rather than a stopgap. Nothing here is a one-click install, no Zap template exists, and if you want email operations from a dropdown instead of a hand-written JSON body, a competitor is ahead of us: AgentMail ships prebuilt nodes for other orchestrators, and its free tier of three inboxes and 3,000 emails a month is more generous than ours.
The free allowance here is one live inbox and 100 email units a month per verified owner
identity, claimed by that identity’s first eligible workspace. An email unit is one
message received or one recipient sent to, so a send to three people uses three, and a
second workspace does not renew it. When it runs out, sending stops and received mail
stays readable. Builder and Team are proposed plans at proposed prices with checkout
disabled, and custom domains require a paid plan, so every live address today sits on
agents.emailforagents.ai.
Test everything against a test project first. The Zap does not change; the credential
does. Environment is carried in the key prefix and cannot be crossed, so an efa_test_
key resolves only to test projects and test inboxes and no flag lets it reach live. That
matters more in Zapier than in hand-written code, because you will run the Zap a dozen
times getting the JSON right and every one of those is a real send attempt against
whichever credential is selected. Test inboxes use a non-routable .mail.invalid address
and a simulated transport, and
POST /v1/projects/{project_id}/test/inbound injects a synthetic inbound message that
fires message.received and starts the whole loop with nothing leaving the system.
Finally, the thing a Zap will quietly get wrong: HTTP 202 and message.accepted mean the
provider took the message, not that anyone received it. Recipient outcomes arrive
separately as message.delivery_updated, and a send to five people has five delivery
states that can disagree.
What each status actually proves sets out the
diagnosis order. A Zap that writes “sent” to a spreadsheet on the 202 is recording
submission and calling it delivery.
Keep reading
Idempotency Keys: Stop an Agent Sending Duplicate Emails
How send idempotency works: canonical payload hashing, the 30-day replay window, conflict versus expiry, and why retrying an unknown outcome is unsafe.
How to Give an AI Agent Its Own Email Address
Create a dedicated inbox, choose read, draft or send permission, connect over MCP or a scoped key, and prove a real round trip before granting send access.
Accepted vs Delivered: What an Email Status Proves
Queued, accepted, delivered, bounced, unknown: what each email status actually proves, what it does not, and the correct next action for every one of them.