Sending is the half of agent email that can do real damage. A bad read wastes a model call. A bad send lands in a stranger's inbox and cannot be pulled back. This guide covers the three single-message calls (send, reply, forward) and the parts that matter when an agent rather than a person is pressing the button: previewing, retrying without duplicates, and reading the errors.

In Lumbox the sender is always an inbox. Every outbound call goes through /v1/inboxes/{id}/..., and the From address is that inbox's address, such as support-bot@trylumbox.com or an address on a domain you have verified. For the default domain there is no SMTP host or password to configure. You send with an HTTP request and an API key.

Send a new message

curl -X POST https://api.lumbox.co/v1/inboxes/inb_XXXXXXXXXXXXXXXXXXXX/send \
  -H "X-API-Key: ak_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "dana@example.com",
    "subject": "The report you asked for",
    "text": "Hi Dana, the summary is below.",
    "html": "<p>Hi Dana, the summary is below.</p>"
  }'

to and subject are required. to takes a string or an array, cc works the same way, and the body can be text, html, or both. This endpoint has no bcc, reply_to or attachment field. If a workflow depends on one of those, find out before the agent is built around it.

A successful send returns 201:

{
  "ok": true,
  "email_id": "eml_...",
  "thread_id": "eml_...",
  "message_id": "<msg_...@trylumbox.com>",
  "provider_message_id": "...",
  "provider": "ses"
}

Store email_id and thread_id with whatever task produced the message. A new send starts its own thread, which is why the two IDs match here. message_id is the Message-ID header Lumbox generated on the inbox's own domain, and provider names the service that handed the message on, as a lowercase string.

Preview with dry_run

Add "dry_run": true to the same body and nothing is sent. The API validates the addresses, applies your allow and block lists, and returns what it would have sent: from, to, cc, subject, the first 500 characters of the body, the body length, whether HTML is present, and the Message-ID it would use.

This is the cheapest human-in-the-loop step you can add. The agent drafts, calls with dry_run, shows the preview to a person or a second model, and only then repeats the call without the flag. One detail to plan for: the per-minute send limiter runs before the dry-run check, so a preview uses a slot in that window even though it does not count toward your monthly quota. On the Free plan, where the window is three sends a minute, a preview plus a send uses two of the three.

Reply inside the thread

async function replyTo(inboxId, emailId, text, taskKey) {
  const res = await fetch(`https://api.lumbox.co/v1/inboxes/${inboxId}/reply`, {
    method: "POST",
    headers: {
      "X-API-Key": process.env.LUMBOX_API_KEY,
      "Content-Type": "application/json",
      "Idempotency-Key": taskKey,
    },
    body: JSON.stringify({ email_id: emailId, text }),
  });
  if (!res.ok) throw new Error(`reply failed: ${res.status} ${await res.text()}`);
  return res.json(); // { ok, email_id, thread_id, message_id }
}

The only required field is email_id, the message being answered, and it has to belong to the same inbox or you get a 404. The recipient is the original sender. With "reply_all": true, the original To and Cc addresses are added as Cc, minus the inbox itself and the original sender. The subject gets a single Re: prefix, never Re: Re:, and the API sets In-Reply-To to the original Message-ID and appends it to References. Those two headers are what mail clients use to group the reply with the earlier messages, and the reply is stored under the same thread_id as the original.

Letting the API build these headers matters. An agent that "replies" through a plain send call has to produce the subject and threading headers itself, and a send call has no field for In-Reply-To, so the recipient's client shows it as a new conversation.

Forward to a person

POST /v1/inboxes/{inbox_id}/forward
{
  "email_id": "eml_...",
  "to": "ops@yourcompany.com",
  "comment": "This arrived overnight and needs a human decision."
}

The optional note field is comment. Some older examples call it note, which the API ignores. The subject becomes Fwd: ..., and the body gets your comment, a "Forwarded message" block with the original sender, date and subject, then the original text. A forward starts a new thread rather than joining the old one, because the conversation with the new recipient is a different conversation. Forwarding is the natural escalation path for an agent: when it hits something it should not decide alone, it sends the message to a person with one line of context.

Retry without sending twice

Agents retry. A network timeout on a send leaves the agent unsure whether the message went out, and the usual reaction is to call the tool again. With email, that can mean the recipient gets two copies.

Send an Idempotency-Key header on send, reply and forward calls. Lumbox stores the first successful response for 24 hours. A repeat with the same key and the same body returns that stored response with an Idempotent-Replayed: true header instead of sending again. The same key with a different body returns 409, and so does a repeat that arrives while the first request is still running. A failed attempt releases the key, so the retry can go through. Build the key from something stable in your own system, such as a task ID plus the step name, not a random value generated on each attempt.

Errors the agent will see

StatusMeaningWhat the agent should do
400Missing to or subject, or a malformed addressFix the input; do not retry as is
403 INVALID_RECIPIENTFree-tier check found a recipient domain that cannot receive mail, a likely typo, or a disposable address; invalid_recipients lists themDrop or correct those addresses
403 SEND_BLOCKEDEvery recipient is on the suppression list, or the sender looks like brand impersonation on a shared domainStop; retrying will not help
429Per-minute send limit for the plan (3, 30, 60 or 120), with retry_after_secondsWait that long, then retry with the same idempotency key
429 SEND_RAMP_LIMITNew Free account without a verified domain: 5 sends in the first hour, 25 in the first dayWait, verify a domain, or upgrade
402 PLAN_LIMIT_EXCEEDEDMonthly send quota used upStop and tell a human

Suppression deserves a note. Addresses that hard-bounced, complained, or used a one-click unsubscribe are dropped from future sends automatically. If some recipients are suppressed and others are not, the message goes to the rest. If all of them are, the call fails with SEND_BLOCKED rather than quietly sending to nobody. The suppression list post covers how addresses get there.

What happens after the call

The provider depends on the sending address. Mail from @trylumbox.com inboxes goes out through Amazon SES. Mail from an inbox on your own domain goes through the default outbound configuration you set up for your organization, if you have one, which is how you bring your own SES, Postmark, SendGrid or SMTP server. The provider field in the response tells you which one handled a given message.

Every sent message is stored against the inbox with the category outbound, so the agent can list what it sent with GET /v1/inboxes/{id}/emails?category=outbound. When IMAP is set up for the inbox, a copy is also appended to its Sent folder, so a person checking the mailbox in an ordinary mail client sees both sides of the conversation. Click tracking is opt-in: pass "track_clicks": true with an HTML body and links are rewritten so that a click fires an email.clicked webhook. There is no open tracking.

More than one recipient at a time

These three endpoints are for individual messages. For up to 100 separate messages in one request, each with its own recipient and content, there is POST /v1/send/bulk, which has different rules: it needs an organization-level key and always goes out through Resend. The bulk sending guide covers it, including when not to use it. For how replies group on the receiving side, see email threading for AI conversations.

The full request and response reference for all three calls is on the sending docs page, and a free account includes 100 sends a month to try them with.