n8n · Webhook + HTTP Request
Receive and send agent email in n8n
Two workflows cover most email automation in n8n: one that starts when mail arrives and answers in the thread, and one that sends an email and waits for the reply. Both use n8n’s built-in nodes only.
- Written against
- n8n 2.40.7
- Checked
- 25 September 2026
- Runnable example
- examples/guides/n8n-email-automation
There is no Agentboxd node for n8n, and you don’t need one. Agentboxd calls a URL when mail arrives (a signed webhook), and everything else is a JSON API with a bearer key. n8n’s Webhook trigger and HTTP Request node handle both. This guide was checked end to end on n8n 2.40 against a Mailroom server: a test email came in, the signature checked out, and the acknowledgement landed in the sender’s inbox, in the same thread.
#Create the credential
In n8n, create a credential of type Header Auth. Name it Agentboxd API key, set Name to Authorization and Value to Bearer mr_… with your key from the API keys page. Every HTTP Request node below uses it: set Authentication to Generic Credential Type, Generic Auth Type to Header Auth, and pick the credential. (The Bearer Auth type works too.)
#Receive email with the Webhook node
Add a Webhook node: HTTP Method POST, Path agentboxd-inbound, Respond Immediately. Under Options, turn on Raw Body. Agentboxd signs the exact bytes it sends, so the signature can only be checked against the raw body, not against JSON that n8n parsed and serialised again. With Raw Body on, n8n keeps the body as binary data in the data property.
n8n gives the node two URLs. The test URL (/webhook-test/agentboxd-inbound) only listens while you click Listen for test event; the production URL (/webhook/agentboxd-inbound) works once the workflow is published. Register the production one with Agentboxd.
#Verify the signature and filter
Add a Code node (JavaScript, Run Once for All Items). It recomputes HMAC-SHA256(secret, "<timestamp>.<raw body>"), compares it to X-Mailroom-Signature in constant time, rejects timestamps more than five minutes old, and then decides whether the email deserves an automatic answer:
// n8n Code node ("Run Once for All Items"): verify the Agentboxd webhook signature and keep only
// email worth answering. Needs NODE_FUNCTION_ALLOW_BUILTIN=crypto on the n8n instance and the
// Webhook node's "Raw Body" option on. The same code is embedded in inbound-reply.workflow.json.
const crypto = require('crypto');
// The secret returned once by POST /v1/webhooks. Keep it out of shared workflow exports.
const SECRET = 'whsec_replace_me';
const item = $input.first();
const headers = item.json.headers;
const raw = await this.helpers.getBinaryDataBuffer(0, 'data');
const ts = String(headers['x-mailroom-timestamp'] || '');
const sig = String(headers['x-mailroom-signature'] || '');
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
throw new Error('missing or stale X-Mailroom-Timestamp');
}
const expected = crypto.createHmac('sha256', SECRET).update(`${ts}.`).update(raw).digest('hex');
if (!/^[0-9a-f]{64}$/i.test(sig) || !crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))) {
throw new Error('bad X-Mailroom-Signature');
}
const event = JSON.parse(raw.toString('utf8'));
if (event.type !== 'message.received') return [];
const m = event.data.message;
// Spoofed senders: never auto-answer them.
if (m.labels.includes('spf-fail') || m.labels.includes('dmarc-fail')) return [];
// Auto-replies, bulk mail and bounces: answering them starts mail loops.
const auto = String(m.headers['auto-submitted'] || 'no').toLowerCase();
const precedence = String(m.headers['precedence'] || '').toLowerCase();
if (auto !== 'no' || ['bulk', 'junk', 'list', 'auto_reply'].includes(precedence)) return [];
return [
{
json: {
inbox_id: event.data.inbox.id,
message_id: m.id,
from: m.from,
subject: m.subject || '(no subject)',
text: m.extracted_text || '',
},
},
];- `require('crypto')` is off by default in the Code node. Set
NODE_FUNCTION_ALLOW_BUILTIN=cryptoon the n8n instance. If you run task runners in external mode, set it in the runner’s environment overrides instead. - Returning `[]` stops the workflow for that event, so mail that failed SPF or DMARC, and anything with
Auto-SubmittedorPrecedence: bulkheaders, never gets an automatic answer. Answering auto-replies is how two robots end up emailing each other all night. - Headers are lower-case in the message JSON (
m.headers['auto-submitted']), andextracted_textis only the new part of the email. - Keep the secret out of workflow exports you share. Paste it into the node after importing, or load it from a place only your instance can read.
#Reply with the HTTP Request node
Add an HTTP Request node: Method POST, URL as an expression, the credential from the first step, Send Body on, Body Content Type JSON, Specify Body Using JSON:
POST https://api.agentboxd.com/v1/inboxes/{{ $json.inbox_id }}/messages/{{ $json.message_id }}/reply{{ JSON.stringify({
text: 'Hi,\n\nThanks for your email about "' + $json.subject + '". We have it and will answer within one business day.\n\nAcme Support'
}) }}The reply goes out from the agent’s address, DKIM-signed, with In-Reply-To and References set, so it lands in the sender’s thread. Add a header Idempotency-Key: ack-{{ $json.message_id }} (Send Headers): if n8n retries the node, Agentboxd returns the first reply instead of sending a second one.
This is where an AI step goes in a real workflow: put an AI Agent or LLM node between the Code node and the HTTP Request, give it $json.text as input, and send its answer. Keep the filter in front of it, so spoofed mail never reaches the model.
#Register the webhook with Agentboxd
Publish the workflow, then point Agentboxd at the production URL. You can do it in the dashboard under Webhooks, or with one call:
curl -s https://api.agentboxd.com/v1/webhooks \
-H "Authorization: Bearer $AGENTBOXD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://n8n.example.com/webhook/agentboxd-inbound",
"events": ["message.received"],
"inbox_ids": ["<inbox id>"]
}'
# → { "id": "…", "secret": "whsec_…", … } the secret is shown only in this responseCopy the secret into the Code node. Then use Send test in the dashboard (a webhook.test event, which the filter ignores) or simply email the inbox. Agentboxd retries failed deliveries 8 times over about a day, and lists every attempt under Recent deliveries.
#Send an email and wait for the answer
The second workflow starts manually (swap in a Schedule or any other trigger) and uses only HTTP Request nodes:
- Get or create inbox:
POST /v1/inboxeswith{ "client_id": "n8n-outreach", "display_name": "Acme Ops" }. Idempotent: every run gets the same inbox back. - Send email:
POST /v1/inboxes/{{ $json.id }}/messages/sendwithto,subjectandtext. The response is the queued message, including itsinbox_idandcreated_at. - Wait for the reply: a
GETlong-poll that returns as soon as the answer arrives, or{ "data": null }after 60 seconds. Set the node’s Timeout option above 60 seconds (the example uses 70,000 ms).
GET https://api.agentboxd.com/v1/inboxes/{{ $json.inbox_id }}/messages/wait
?timeout=60
&since={{ $json.created_at }}
&from=dana@example.comFor replies that take hours, don’t hold a workflow open: let the first workflow handle them when they arrive, and use the thread (thread_id) to connect the answer to what you sent.
#Import instead of building
Both workflows are in the Agentboxd repository under examples/guides/n8n-email-automation/ as inbound-reply.workflow.json and send-and-wait.workflow.json. Import them with Import from File, select your Header Auth credential in each HTTP Request node, paste the webhook secret, and publish. On a dedicated Enterprise deployment, rebuild them with your API URL (the folder’s README shows how).
Related: the Webhooks reference (events, payloads, retries), and how the support agent use case fits together.