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.

PathFrom lineInboundCeiling
Gmail / Outlook actionthe human who authorised OAuththat person’s real mailboxthat provider’s sending policy
Email by Zapiera zapiermail.com address, fixeda generated zapiermail.com address5 a day on Free or trial, 10 an hour on paid
Webhooks action to this APIan inbox the project ownsa signed message.received callbackthe 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:

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 keyResult
Identical payload, first completedthe stored 202 is replayed, no second email
Identical payload, first still in flightHTTP 503 temporarily_unavailable, “A matching send is already in progress.”
Different payloadHTTP 409 idempotency_conflict
Identical payload after 30 daysHTTP 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.

MechanismSame Zap run?What re-executesKey from trigger dataKey from zap_meta_timestamp
Autoreplayyesthe errored step onlysafeduplicate
Manual replay of errored stepsyesthe errored step onlysafeduplicate
Replay all steps, from the editorno, a new runevery step including the trigger and successful onessafeduplicate

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

All guides · Documentation · Integrations