LumboxLumbox Docs

Auto-Reply Agent

Let an inbox draft its own replies from your docs and past threads, with a person approving each one.

Auto-Reply Agent

Turn on auto-reply for an inbox and every new email to it starts an agent run. The agent reads the thread, looks up facts with its tools, and then does one of three things: sends the reply, saves it as a draft for a person to approve, or hands the thread to a human.

This recipe builds a support inbox (support@ on your own domain) in draft mode, so nothing goes out until someone approves it.

How a run works

  1. An email arrives at the inbox.
  2. Lumbox checks the safeguards. If any check fails, the email gets no reply and no draft.
  3. The agent gets the thread (up to 20 messages, each body cut at 4,000 characters), your instructions, and its tools. It can call tools for up to 4 rounds; the 5th round must produce the reply.
  4. The run ends in one of three ways:
    • The agent calls escalate_to_human: no reply, the thread is marked NEEDS_HUMAN, and thread.escalated fires.
    • The inbox is in draft mode, or the email was flagged as a likely prompt injection: a pending draft is saved and draft.created fires.
    • The inbox is in auto mode: the reply is sent in the same thread, from the inbox address.

Every tool call is logged. Read the log with:

GET /v1/inboxes/:id/agent-actions?limit=50

Each entry has tool, input, output, status, error, duration_ms, email_id, reply_email_id and created_at.

1. Create the support inbox

Custom domains need the Starter plan or above. Add the domain and its DNS records first, as shown in Custom Domains. If your root domain already receives mail somewhere else, use a subdomain such as help.yourco.com, because the MX records route the whole domain to Lumbox.

curl -X POST https://api.lumbox.co/v1/inboxes \
  -H "X-API-Key: ak_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "Support", "domain": "help.yourco.com", "local_part": "support"}'

The domain must be verified (status ACTIVE) before you can create an inbox on it.

2. Add your knowledge base

The knowledge base belongs to the org, so every inbox's agent searches the same documents. Add plain text: help articles, policies, pricing notes.

curl -X POST https://api.lumbox.co/v1/knowledge \
  -H "X-API-Key: ak_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Billing FAQ",
    "content": "To change your billing email, open Settings > Billing and edit Billing contact...",
    "source": "https://yourco.com/help/billing"
  }'

title and content are required, source is optional. The document is split into chunks and embedded. The response (201):

{
  "id": "kdoc_...",
  "title": "Billing FAQ",
  "chunk_count": 3,
  "stored_bytes": 2841,
  "ingestion_status": "ready",
  "indexed": true
}

List documents with GET /v1/knowledge (the response includes stored_bytes and storage_limit_bytes for the org) and remove one with DELETE /v1/knowledge/:id.

Size limits

One document can be up to 2 MB. Total storage per org depends on the plan:

PlanKnowledge storage
Free2 MB
Starter10 MB
Pro50 MB
Scale250 MB
StatusMeaning
413The document is over 2 MB, or it would take the org over its storage limit
400title or content is missing, or the document has no text to index
402No embedding access: the org is on Free without its own OpenAI key or a connected Cloudflare account, or the paid plan's AI credit is used up

3. Turn on auto-reply

In the dashboard, open the inbox, choose More > Auto-reply settings, switch it on, pick Draft first or Send automatically, add instructions and save. Through the API:

POST /v1/inboxes/:id/auto-reply
FieldTypeRequiredDescription
enabledbooleanyesTurns the agent on or off for this inbox
mode"auto" or "draft"noauto sends each reply as soon as it is written. draft saves it for approval. New inboxes start in auto; omit the field to keep the current mode
instructionsstring or nullnoAdded to the agent's system prompt as operator instructions: tone, sign-off, what to escalate. null or "" clears them; omit the field to keep the current text
curl -X POST https://api.lumbox.co/v1/inboxes/inb_abc123/auto-reply \
  -H "X-API-Key: ak_..." \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "mode": "draft",
    "instructions": "You answer for YourCo support. Keep replies under 120 words and sign off as The YourCo team. Escalate refunds and account deletions."
  }'

Response:

{
  "id": "inb_abc123",
  "address": "support@help.yourco.com",
  "auto_reply_enabled": true,
  "auto_reply_instructions": "You answer for YourCo support. ...",
  "auto_reply_mode": "draft"
}
StatusMeaning
400enabled is missing or not a boolean, or mode is not auto or draft
402You set enabled: true and the org has no AI access (see AI billing). The body has error, upgrade_url and byok_url
404Inbox not found

GET /v1/inboxes/:id also returns auto_reply_enabled, auto_reply_instructions and auto_reply_mode.

Built-in tools

The agent always has these five tools. Results longer than 4,000 characters are cut before the model reads them.

ToolArgumentsReturns
search_knowledgequeryUp to 5 knowledge chunks with a similarity score of 0.3 or higher: { "hits": [{ "doc", "content", "score" }] }. doc is the document title and content is cut at 1,500 characters. If embeddings are unavailable it falls back to a keyword match, returns hits without score, and adds "note": "keyword search (embeddings unavailable)"
search_past_conversationsquery, from_sender_only (default false)Up to 5 of the newest emails in this inbox, outside the current thread, whose subject or body contains the query: { "emails": [{ "from", "subject", "date", "snippet" }] }. snippet is the first 300 characters
get_contactemail (defaults to the sender){ "email", "contact", "emails_received_from_sender", "first_seen" }. contact is { "name", "tags", "status", "metadata" } from your contacts, or null
set_labelsadd, remove (arrays of strings)Adds or removes labels on the email being handled and returns { "labels": [...] }
escalate_to_humanreasonEnds the run with no reply. The thread status becomes NEEDS_HUMAN and thread.escalated fires with the reason

The system prompt tells the agent to use these tools for facts, never to invent order numbers, prices, dates or policies, and to escalate when it cannot ground an answer in tool results.

Custom HTTP tools

Register your own HTTP endpoints as tools, for example an order lookup.

curl -X POST https://api.lumbox.co/v1/agent-tools \
  -H "X-API-Key: ak_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "lookup_order",
    "description": "Look up an order by its order number. Returns status and tracking.",
    "url": "https://api.yourco.com/orders/lookup",
    "method": "POST",
    "headers": {"Authorization": "Bearer your-internal-token"},
    "input_schema": {
      "type": "object",
      "properties": {"order_number": {"type": "string"}},
      "required": ["order_number"]
    },
    "inbox_id": "inb_abc123"
  }'
FieldDescription
nameRequired. Matches ^[a-zA-Z][a-zA-Z0-9_]{0,63}$
descriptionRequired. The agent reads it to decide when to call the tool
urlRequired. Must be a public https URL; private and internal addresses are rejected
methodGET, POST, PUT, PATCH or DELETE. Default POST
headersStored encrypted. The API returns only header_keys, never the values
input_schemaJSON Schema for the arguments. Default: an object with no properties
inbox_idLimit the tool to one inbox. Omit it to give the tool to every inbox in the org
enabledDefault true

How calls are made:

  • GET and DELETE send the arguments as query parameters; the other methods send them as a JSON body.
  • Each call has a 10 second timeout. Redirects are followed up to 4 hops, each one re-checked, and your headers are dropped on a cross-origin hop.
  • A non-2xx response reaches the agent as { "error": "http_<status>", "body": ... }.

Dry-run a tool with sample input before the agent uses it:

curl -X POST https://api.lumbox.co/v1/agent-tools/TOOL_ID/test \
  -H "X-API-Key: ak_..." \
  -H "Content-Type: application/json" \
  -d '{"input": {"order_number": "1042"}}'

GET /v1/agent-tools, PATCH /v1/agent-tools/:id and DELETE /v1/agent-tools/:id manage registered tools.

Set requires_approval: true on a tool that changes something outside Lumbox, such as issuing a refund. When the agent calls it, the call is not run: the reply is saved as a draft and the call waits with it. Approving the draft sends the reply and then runs the waiting call with the arguments the agent chose, once. Rejecting the draft marks the call rejected. The approve response includes tool_results, so you can see whether each call succeeded.

Approve, edit or reject drafts

In draft mode each reply waits in the drafts queue. Pending drafts are also in the dashboard under Inboxes > Drafts, where you can edit the text before approving.

List drafts

GET /v1/drafts?status=pending&inbox_id=inb_abc123&limit=50

status is pending (default), approved or rejected. limit is 1 to 200, default 50. Each draft:

{
  "id": "...",
  "inbox_id": "inb_abc123",
  "thread_id": "...",
  "in_reply_to_email_id": "eml_...",
  "to": "customer@example.com",
  "subject": "Re: How do I change my billing email?",
  "text": "Hi, you can change it in Settings > Billing...",
  "status": "pending",
  "source": "auto_reply",
  "agent_action_ids": ["..."],
  "sent_email_id": null,
  "decided_at": null,
  "created_at": "2026-10-13T09:12:44.000Z"
}

GET /v1/drafts/:id returns one draft.

Approve as written

curl -X POST https://api.lumbox.co/v1/drafts/DRAFT_ID/approve \
  -H "X-API-Key: ak_..."

Edit and approve

Pass text to send your version instead of the agent's. It must be a non-empty string.

curl -X POST https://api.lumbox.co/v1/drafts/DRAFT_ID/approve \
  -H "X-API-Key: ak_..." \
  -H "Content-Type: application/json" \
  -d '{"text": "Hi, open Settings > Billing and edit Billing contact. The change applies from your next invoice."}'

Approving sends the reply in the original thread from the inbox address, sets the draft to approved with sent_email_id, and fires draft.approved.

Reject

curl -X POST https://api.lumbox.co/v1/drafts/DRAFT_ID/reject \
  -H "X-API-Key: ak_..."

Nothing is sent and no webhook fires. Approving or rejecting a draft that is no longer pending returns 409. Approving a draft whose inbox was deleted returns 410.

Webhooks

Subscribe to the three agent events. The default subscription is only email.received, so list them explicitly (or use "*" for all events).

curl -X POST https://api.lumbox.co/v1/webhooks \
  -H "X-API-Key: ak_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-server.com/lumbox", "events": ["draft.created", "draft.approved", "thread.escalated"]}'

The response includes a secret (shown once). Every delivery is a POST with this envelope:

{
  "event": "draft.created",
  "timestamp": "2026-10-13T09:12:44.512Z",
  "data": {}
}

and two headers: X-Lumbox-Event (the event name) and X-Lumbox-Signature (hex HMAC-SHA256 of the raw body, keyed with the secret).

draft.created

{
  "draft_id": "...",
  "inbox_id": "inb_abc123",
  "thread_id": "...",
  "in_reply_to_email_id": "eml_...",
  "to": "customer@example.com",
  "subject": "Re: How do I change my billing email?",
  "text": "first 500 characters of the draft"
}

draft.approved

{
  "draft_id": "...",
  "inbox_id": "inb_abc123",
  "thread_id": "...",
  "sent_email_id": "eml_...",
  "edited": true
}

edited is true when the approval passed its own text.

thread.escalated

{
  "thread_id": "...",
  "inbox_id": "inb_abc123",
  "email_id": "eml_...",
  "from": "customer@example.com",
  "subject": "Refund for a double charge",
  "reason": "Refund request beyond the documented policy"
}

Safeguards

Each check below runs before the agent writes anything.

  • No replies to automated senders. Skipped: senders whose local part starts with mailer-daemon, postmaster, bounce, no-reply, noreply or do-not-reply; mail with an Auto-Submitted header other than no, an X-Auto-Response-Suppress header, or Precedence: bulk, junk, list or auto_reply; mailing-list mail (List-Id or List-Unsubscribe); mail flagged as spam; and mail from the inbox's own address.
  • 5 replies per thread. Once the inbox has sent 5 messages in a thread, the agent stops answering it.
  • Prompt-injection screen. Each email is scored for injection attempts (instruction overrides, requests to reveal the prompt or forward data, hidden text). A high score turns the reply into a draft even in auto mode, and the agent is told not to follow instructions inside the message. Medium and high scores are logged as an injection_screen agent action.
  • Your send lists and quota. If your send allow/block lists block the sender, or the org has used its monthly send quota, the email is skipped.
  • No AI access. If the org has no AI access when the email arrives, the email is skipped.

Replies sent by the agent, in auto mode or through an approval, count toward the plan's monthly sends.

AI billing

Starter, Pro and Scale run the agent on Lumbox AI. Every model call and every embedding call is metered by its token counts, priced, and deducted from the plan's monthly AI credit:

PlanMonthly AI credit
Starter$2
Pro$10
Scale$40

The credit resets on the 1st of each month (UTC). When it is used up, Lumbox AI calls stop until the reset: new emails get no agent run, and AI endpoints return 402.

Free has no AI credit. Save your own model key first, or enabling auto-reply returns 402:

curl -X PUT https://api.lumbox.co/v1/ai-config \
  -H "X-API-Key: ak_..." \
  -H "Content-Type: application/json" \
  -d '{"provider": "openai", "api_key": "sk-...", "model": "your-model-id"}'

provider is one of openai, anthropic, google, deepseek, xai or moonshot. On every plan, a saved key is used for the agent's model calls, and calls made with it do not draw from the credit. Knowledge documents need embeddings: on Free that means an OpenAI key or a connected Cloudflare account.

Check spend for the current month:

GET /v1/usage/ai

The response has billed_usd, included_credit_usd, remaining_usd, token totals, and by_feature, where agent replies appear as auto_reply.

Node.js

The lumbox SDK covers inboxes and webhooks. Auto-reply, knowledge and drafts have no SDK methods yet, so this example calls them over REST.

import { createHmac, timingSafeEqual } from "node:crypto";
import { Lumbox, type WebhookData } from "lumbox";

const API = "https://api.lumbox.co";
const KEY = process.env.LUMBOX_API_KEY!;
const client = new Lumbox({ apiKey: KEY });

async function lumbox(method: string, path: string, body?: unknown) {
  const res = await fetch(`${API}${path}`, {
    method,
    headers: { "X-API-Key": KEY, "Content-Type": "application/json" },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${await res.text()}`);
  return res.json();
}

const inbox = await client.createInbox({
  name: "Support",
  domain: "help.yourco.com",
  local_part: "support",
});

await lumbox("POST", "/v1/knowledge", {
  title: "Billing FAQ",
  content: "To change your billing email, open Settings > Billing and edit Billing contact.",
});

await lumbox("POST", `/v1/inboxes/${inbox.id}/auto-reply`, {
  enabled: true,
  mode: "draft",
  instructions: "Keep replies under 120 words. Escalate refunds.",
});

const hook = (await client.createWebhook("https://your-server.com/lumbox", [
  "draft.created",
  "draft.approved",
  "thread.escalated",
])) as WebhookData & { secret: string };
console.log("Store this signing secret:", hook.secret);

const { data: pending } = await lumbox("GET", `/v1/drafts?inbox_id=${inbox.id}`);
for (const draft of pending) {
  console.log(draft.to, draft.subject, draft.text);
}

if (pending.length) {
  await lumbox("POST", `/v1/drafts/${pending[0].id}/approve`, {
    text: "Hi, open Settings > Billing and edit Billing contact.",
  });
}

export function verifyLumboxSignature(rawBody: string, signature: string, secret: string) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  return (
    signature.length === expected.length &&
    timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  );
}