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:

Code node · Verify and filter
// 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=crypto on 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-Submitted or Precedence: bulk headers, 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']), and extracted_text is 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:

URL (expression)
POST https://api.agentboxd.com/v1/inboxes/{{ $json.inbox_id }}/messages/{{ $json.message_id }}/reply
JSON body (expression)
{{ 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:

shell
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 response

Copy 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/inboxes with { "client_id": "n8n-outreach", "display_name": "Acme Ops" }. Idempotent: every run gets the same inbox back.
  • Send email: POST /v1/inboxes/{{ $json.id }}/messages/send with to, subject and text. The response is the queued message, including its inbox_id and created_at.
  • Wait for the reply: a GET long-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).
Wait for the reply · URL and query
GET https://api.agentboxd.com/v1/inboxes/{{ $json.inbox_id }}/messages/wait
  ?timeout=60
  &since={{ $json.created_at }}
  &from=dana@example.com

For 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.