Docs
guides · receiving

Reply in-thread

Answering a message is one call. POST /v1/emails/{id}/reply takes the id of the message you're replying to and derives everything an in-thread reply needs — the From and To addresses, the Re: subject, and the In-Reply-To / References headers — so the recipient's mail client threads it correctly.

A normal send makes you assemble the envelope yourself: pick a verified from, set the recipient, re-type the subject with a Re: prefix, and hand-write the In-Reply-To and References headers from the parent's message id. Forget any of those and the reply lands as a fresh thread. The reply endpoint does all of it from one input: the id of the message you are answering.

POST/v1/emails/{id}/reply

The {id} in the path is the parent message — typically the inbound message you got from a thread or the inbound_received webhook. The only required content is a body: pass html, text, or both.

curl https://api.drin.run/v1/emails/msg_in_01/reply \
  -H "Authorization: Bearer $DRIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Thanks for reaching out — order #4182 ships tomorrow.",
    "html": "<p>Thanks for reaching out — order #4182 ships <b>tomorrow</b>.</p>"
  }'

The reply is queued like any send and returns its new message id. It becomes the next outbound message in the same thread.

202 Accepted
{
  "id": "msg_out_02",
  "status": "queued"
}

01 What Drin sets for you

Send nothing but a body and Drin fills the rest in from the parent message:

  • From defaults to the parent message's inbox address — the address it was sent to. Your reply comes from the same mailbox the conversation is already on.
  • To defaults to the parent message's sender, so the reply goes back to whoever wrote in.
  • Subject defaults to the parent subject with a Re: prefix — and is not doubled when the subject already starts with Re:.
  • In-Reply-To and References are set from the parent's RFC 822 message id and its existing reference chain, so Gmail, Outlook, and Apple Mail collapse the reply into the original conversation instead of starting a new one.
A body is requiredAt least one of html or text must be present. Everything else is optional — omit it and the derived default applies.

02 Request body

Every field overrides the value Drin would otherwise derive. Supply only what you want to change.

htmlstringOptional
HTML body of the reply. Required unless text is given.
textstringOptional
Plain-text body. Required unless html is given. Provide both for the best deliverability.
subjectstringOptional
Override the auto-generated Re: subject. Omit to keep the threaded default.
fromobjectOptional
{ email, name? }. Override the sending address — useful to hand a conversation to a different inbox (e.g. escalate from support@ to billing@). Must be an address on a domain you own.
toarrayOptional
{ email, name? }[]. Override the recipients. Defaults to the parent message's sender.
ccarrayOptional
{ email, name? }[]. Additional copy recipients.
attachmentsarrayOptional
{ filename, content, contentType? }[] with content base64-encoded.

03 Overriding the defaults

The defaults cover the common case; pass any field to change it. Here a reply is escalated to a different inbox and CC'd to a supervisor — the threading headers are still derived from the parent, so it stays in the same conversation.

Override From + CC
await drin.emails.reply("msg_in_01", {
  text: "Routing this to our billing team.",
  // Override any default Drin would have derived from the parent:
  from: { email: "billing@yourdomain.com", name: "Acme Billing" },
  cc: [{ email: "supervisor@yourdomain.com" }],
});

04 The agent inbox loop

Reply is the closing move of the receive → read → respond loop. Read a thread, let an agent draft an answer from the conversation, and post it — without ever touching a mail header.

Receive → reply
import { DrinClient } from "@drin00/sdk";

const drin = new DrinClient({ apiKey: process.env.DRIN_API_KEY });

// 1. A webhook handed you an inbound messageId. Read the full thread.
const thread = await drin.threads.get(threadId);
const last = thread.messages.at(-1)!;

// 2. Let an agent draft a reply from the conversation...
const draft = await agent.answer(thread.messages);

// 3. ...and post it. Threading is handled for you — no headers to set.
await drin.emails.reply(last.id, { text: draft });
Test it without DNSUse POST /v1/inbound/simulate to inject a synthetic inbound message, then call reply on the returned messageId to exercise the full loop locally. See Inbound & threads.
WebhooksGet pushed every inbound_received event so an agent can reply the moment a message lands.