Skip to content
View as Markdown

Reply to a conversation ​

POST /v1/inbox/conversations/{conversation_id}/replies

Emails a reply to the conversation's latest inbound message, from the campaign mailbox or the reply address.

Scope: inbox:reply

Access: The key's member must be able to see the conversation in the inbox.

DialEcho sends the reply the way your team's replies from the inbox go out: the recipient, subject and threading come from the conversation, and without html the HTML part is made from text. Recipients, sender, subject, CC, BCC and attachments cannot be set; any field not listed here is rejected. 200 means the mail provider accepted the reply and DialEcho stored it, not that it reached the inbox. It shows in the thread as a manual_reply with external_reply set.

in_reply_to_message_id must be the conversation's latest inbound message, from the feed or the conversation. If a newer one has arrived the reply is refused with 409 conversation_changed: read the conversation again and answer the latest. Each inbound message gets at most one reply through the API, across all your keys; a message your team or the AI agent already answered is 409 already_replied too. A pending AI reply or draft for the message is cancelled when yours is sent; on real-estate conversations a draft waiting for review is refused with 409 draft_requires_review instead.

send_as picks the sender; check the conversation's reply_identities first. campaign_mailbox sends from the mailbox the thread came from. reply_address sends from the campaign's connected reply address; when it is not set up, disconnected or out of capacity the reply is refused with 409 sender_unavailable and never sent from another address.

Idempotency-Key is required and belongs to your API key and this conversation. The same key with the same body returns 200 with the original message and replayed: true and sends nothing, even after the conversation was archived or answered. The same key with a different body is 409 idempotency_conflict. Bodies are compared after text is trimmed and a blank html dropped.

After a timeout or 409 delivery_unknown, retry only with the same Idempotency-Key and body: it reports the outcome and never sends a second email. delivery_unknown means the reply may have gone out, so never send it again under a new key. To change the conversation's state after replying, use PATCH; never resend an email to fix state.

Replies have their own limit on top of the request limit: 20 a minute per API key, in bursts of up to 5. Requests rejected as invalid (422) do not count; every other reply request does, replays included.

Headers ​

NameTypeRequiredDescription
Idempotency-KeystringYesIdentifies this reply for your API key and this conversation. Reusing it with a different body is 409 idempotency_conflict. 16 to 128 printable ASCII characters, no spaces (a UUID works).

Path parameters ​

NameTypeRequiredDescription
conversation_idUUIDYesThe conversation.

Body ​

Content type: application/json

NameTypeRequiredDescription
textstringYesThe reply as plain text. Leading and trailing whitespace is trimmed, and it must contain more than whitespace. Up to 50,000 characters, counted before trimming.
htmlstringNoAn HTML version of the same reply. Blank is the same as leaving it out. Up to 200,000 characters.
send_asstringYesWhich sender to reply from. One of campaign_mailbox, reply_address.
in_reply_to_message_idUUIDYesThe inbound message you are answering: the conversation's latest inbound message.

Response ​

Status: 200 OK

The same for a new reply and a replay; replayed tells them apart.

NameTypeDescription
statusstringAlways sent. One of sent.
replayedbooleantrue when this answers an earlier request with the same Idempotency-Key; nothing was sent this time.
messageobjectThe reply as stored: direction outbound, kind manual_reply, external_reply set.
message.idUUIDThe message. Pass an inbound message's id as in_reply_to_message_id to answer it.
message.conversation_idUUIDIts conversation.
message.campaign_idUUIDThe conversation's campaign.
message.domainstringThe conversation's inbox. One of contact, realestate.
message.directionstringinbound from the contact, outbound to them. One of inbound, outbound.
message.kindstring | nullhuman_reply: a person wrote back. auto_reply: an auto-responder, such as an out-of-office. manual_reply: sent by your team or through this API. ai_reply: sent by the AI email agent. null when the message was never classified, as with older messages and origin_message. One of human_reply, auto_reply, manual_reply, ai_reply.
message.from_emailstring | nullThe sender, following the member's email visibility.
message.to_emailsarray of stringsThe recipients. Addresses the member's role hides are left out.
message.cc_emailsarray of stringsThe CC recipients, the same way.
message.subjectstring | nullThe subject.
message.textstring | nullThe plain-text body. Restricted when the member's role masks messages.
message.htmlstring | nullThe HTML body exactly as stored, or null (also when the role masks messages). Sanitize it before you display it and never execute it.
message.attachmentsarray of objectsWhat was attached; there are no download links. Empty when the role masks messages. A detail that is not known is null.
message.attachments.filenamestring | nullThe file name.
message.attachments.content_typestring | nullThe MIME type, for example application/pdf.
message.attachments.size_bytesinteger | nullThe size in bytes.
message.created_attimestamp | nullWhen DialEcho stored the message (RFC 3339, UTC). The inbound feed is ordered by it.
message.received_attimestamp | nullWhen the mail provider received an inbound message.
message.sent_attimestamp | nullWhen an outbound message was sent.
message.is_syntheticbooleantrue only for a conversation's origin_message, which is not a stored message.
message.external_replyobject | nullSet on replies sent through this API; null on every other message.
message.external_reply.api_key_idUUID | nullThe API key that sent it.
message.external_reply.organization_member_idUUID | nullThe key's team member.
message.external_reply.automatedbooleanAlways true: sent by an integration, not typed by a person.
message.external_reply.in_reply_to_message_idUUID | nullThe inbound message it answered.

Example request ​

curl

bash
curl -X POST "https://development.agencydevworks.ai/v1/inbox/conversations/44444444-0000-4000-8000-000000000001/replies" \
  -H "Authorization: Bearer dei_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0f0f0f0f-0000-4000-8000-00000000abcd" \
  -d '{
  "text": "Thursday at 2 pm works. Talk then!",
  "html": "<p>Thursday at 2 pm works. Talk then!</p>",
  "send_as": "reply_address",
  "in_reply_to_message_id": "66666666-0000-4000-8000-000000000002"
}'

JavaScript

js
const response = await fetch("https://development.agencydevworks.ai/v1/inbox/conversations/44444444-0000-4000-8000-000000000001/replies", {
  method: "POST",
  headers: {
    "Authorization": "Bearer dei_your_key",
    "Content-Type": "application/json",
    "Idempotency-Key": "0f0f0f0f-0000-4000-8000-00000000abcd",
  },
  body: JSON.stringify({
    "text": "Thursday at 2 pm works. Talk then!",
    "html": "<p>Thursday at 2 pm works. Talk then!</p>",
    "send_as": "reply_address",
    "in_reply_to_message_id": "66666666-0000-4000-8000-000000000002"
  }),
});
const data = await response.json();

Example response ​

Status: 200 OK

json
{
  "status": "sent",
  "replayed": false,
  "message": {
    "id": "88888888-0000-4000-8000-000000000001",
    "conversation_id": "44444444-0000-4000-8000-000000000001",
    "campaign_id": "cccccccc-0000-4000-8000-000000000001",
    "domain": "contact",
    "direction": "outbound",
    "kind": "manual_reply",
    "from_email": "jordan@example.com",
    "to_emails": ["pat@example.com"],
    "cc_emails": [],
    "subject": "Re: Your solar quote",
    "text": "Thursday at 2 pm works. Talk then!",
    "html": "<p>Thursday at 2 pm works. Talk then!</p>",
    "attachments": [],
    "created_at": "2026-11-02T16:22:04Z",
    "received_at": null,
    "sent_at": "2026-11-02T16:22:03Z",
    "is_synthetic": false,
    "external_reply": {
      "api_key_id": "abababab-0000-4000-8000-000000000001",
      "organization_member_id": "ffffffff-0000-4000-8000-000000000001",
      "automated": true,
      "in_reply_to_message_id": "66666666-0000-4000-8000-000000000002"
    }
  }
}

Errors ​

HTTPCodeWhen
403insufficient_scopeThe key does not have inbox:reply. Checked before the body is validated.
404not_foundThe conversation does not exist, was deleted, or the key's member cannot see it; or in_reply_to_message_id is not a message of this conversation.
409idempotency_conflictThe Idempotency-Key was already used on this conversation with a different body. Nothing was sent.
409conversation_changedin_reply_to_message_id is not the latest inbound message any more, or the conversation changed while the reply was prepared. Read it again and answer the latest.
409already_repliedThe message was already answered, by your team, the AI agent or another API reply. Do not resend.
409reply_in_progressAnother reply to the conversation is being written or sent right now. retryable is true: retry shortly with the same Idempotency-Key.
409delivery_unknownThe reply may have been sent but the provider did not confirm it. Retry only with the same Idempotency-Key and body, never as a new request.
409conversation_archivedThe conversation is archived. Restore it with PATCH and {"archived": false} first.
409recipient_suppressedThe recipient unsubscribed or is suppressed. Do not retry.
409non_actionable_inboundThe message cannot be answered automatically: it is an auto-reply or an opt-out, or the conversation has no recipient address.
409sender_unavailableThe send_as sender is not set up, disconnected or out of capacity, or an earlier API reply to this message failed before it was sent and must be reviewed in DialEcho. Nothing was sent.
409draft_requires_reviewReal-estate conversations: an AI draft for this message is waiting for review in DialEcho.
413payload_too_largeThe body is larger than 1 MB. It was not read and nothing was sent.
422invalid_requestconversation_id is not a UUID, the Idempotency-Key is missing or malformed, the body is invalid (error.fields lists every problem), or in_reply_to_message_id is not an inbound message.
429rate_limitedOver the reply limit (20 a minute per key, bursts of 5) or the request limit. Wait the Retry-After seconds, then retry with the same Idempotency-Key.
503dependency_unavailableThe suppression check, the inbox or the access check was unavailable before anything was sent. Retry with backoff and the same Idempotency-Key.

Every endpoint can also answer 401 invalid_api_key, 429 rate_limited and 503 dependency_unavailable. See Errors in the overview.