Skip to content
View as Markdown

List inbound messages ​

GET /v1/inbox/inbound-messages

A polling feed of new inbound emails, each with its conversation's state.

Scope: inbox:read

Access: The key's member must be able to use the campaign's email inbox in DialEcho (its message and email permissions) and see the contact; other conversations are left out.

Poll it to act on replies as they arrive. Messages are ordered by when DialEcho stored them (created_at, then id), not by the provider's received_at, so a message that syncs late still comes through. Every inbound kind is included, auto-replies too: check kind. Marking a conversation read or archived does not take its messages out of the feed; archived threads still appear, with conversation_state.archived set.

next_cursor is always set, even on an empty page. While has_more is true, ask for the next page right away. When it is false the current window is walked: save next_cursor and poll with it again in about 10 seconds, with jitter. Store each page's messages before you save its cursor, so a crash only repeats a page.

Delivery is at least once. Each new window starts 5 minutes before the previous one ended (never before since), so recent messages come again, and a repeated page repeats its messages: deduplicate by message id, never by sender, subject or time.

The feed follows timestamps; it is not a change log, so a message stored late (a slow import, or access granted later) can land behind your position. Run a second, slower walk at least daily with its own cursor, starting from your original since (or 1970-01-01T00:00:00Z for all history), and deduplicate ids across both walks.

Only messages of conversations the key's member can see are returned. Each request looks at up to 1,000 messages, so a page can be empty while has_more is true.

Query parameters ​

NameTypeRequiredDescription
campaign_idUUIDNoOnly this campaign. It must be a campaign whose inbox the key can read, otherwise 404.
domainstringNoOnly this inbox. Leave it out for both. One of contact, realestate.
sincetimestampNoWhere the feed starts: messages stored at or after this time. Only for the first request; after that the cursor carries it. ISO 8601; a time without an offset is read as UTC (URL-encode + as %2B). Default: when the API key was created.
limitintegerNoMessages per page. 1 to 100. Default: 50.
cursorstringNoThe next_cursor you saved, unchanged. It carries campaign_id, domain and since: send it alone, or with exactly the same filters. Up to 4,096 characters.

Response ​

Status: 200 OK

NameTypeDescription
dataarray of objectsInbound messages, in the order DialEcho stored them.
data.messageobjectThe inbound message.
data.message.idUUIDThe message. Pass an inbound message's id as in_reply_to_message_id to answer it.
data.message.conversation_idUUIDIts conversation.
data.message.campaign_idUUIDThe conversation's campaign.
data.message.domainstringThe conversation's inbox. One of contact, realestate.
data.message.directionstringinbound from the contact, outbound to them. One of inbound, outbound.
data.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.
data.message.from_emailstring | nullThe sender, following the member's email visibility.
data.message.to_emailsarray of stringsThe recipients. Addresses the member's role hides are left out.
data.message.cc_emailsarray of stringsThe CC recipients, the same way.
data.message.subjectstring | nullThe subject.
data.message.textstring | nullThe plain-text body. Restricted when the member's role masks messages.
data.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.
data.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.
data.message.attachments.filenamestring | nullThe file name.
data.message.attachments.content_typestring | nullThe MIME type, for example application/pdf.
data.message.attachments.size_bytesinteger | nullThe size in bytes.
data.message.created_attimestamp | nullWhen DialEcho stored the message (RFC 3339, UTC). The inbound feed is ordered by it.
data.message.received_attimestamp | nullWhen the mail provider received an inbound message.
data.message.sent_attimestamp | nullWhen an outbound message was sent.
data.message.is_syntheticbooleantrue only for a conversation's origin_message, which is not a stored message.
data.message.external_replyobject | nullSet on replies sent through this API; null on every other message.
data.message.external_reply.api_key_idUUID | nullThe API key that sent it.
data.message.external_reply.organization_member_idUUID | nullThe key's team member.
data.message.external_reply.automatedbooleanAlways true: sent by an integration, not typed by a person.
data.message.external_reply.in_reply_to_message_idUUID | nullThe inbound message it answered.
data.conversation_stateobjectIts conversation as it is now. Read the whole conversation with GET /v1/inbox/conversations/{conversation_id}.
data.conversation_state.statusstringThe conversation status. One of open, needs_review, booked, not_interested, closed, suppressed.
data.conversation_state.archivedbooleantrue while it is archived.
data.conversation_state.readbooleantrue when it was read at or after its latest inbound message.
next_cursorstringAlways set: pass it back as cursor for the next page or the next poll.
has_morebooleantrue while the current window holds more messages: ask again right away. false: poll again later.
scan_throughtimestampThe end of the current window (RFC 3339, UTC).

Example request ​

curl

bash
curl "https://development.agencydevworks.ai/v1/inbox/inbound-messages?campaign_id=cccccccc-0000-4000-8000-000000000001&since=2026-11-02T00:00:00Z&limit=50" \
  -H "Authorization: Bearer dei_your_key"

JavaScript

js
const response = await fetch("https://development.agencydevworks.ai/v1/inbox/inbound-messages?campaign_id=cccccccc-0000-4000-8000-000000000001&since=2026-11-02T00:00:00Z&limit=50", {
  headers: {
    "Authorization": "Bearer dei_your_key",
  },
});
const data = await response.json();

Example response ​

Status: 200 OK

json
{
  "data": [
    {
      "message": {
        "id": "66666666-0000-4000-8000-000000000002",
        "conversation_id": "44444444-0000-4000-8000-000000000001",
        "campaign_id": "cccccccc-0000-4000-8000-000000000001",
        "domain": "contact",
        "direction": "inbound",
        "kind": "human_reply",
        "from_email": "pat@example.com",
        "to_emails": ["jordan@outreach.example.com"],
        "cc_emails": [],
        "subject": "Re: Your solar quote",
        "text": "Is Thursday afternoon available?",
        "html": "<p>Is Thursday afternoon available?</p>",
        "attachments": [{ "filename": "electric-bill.pdf", "content_type": "application/pdf", "size_bytes": 48213 }],
        "created_at": "2026-11-02T16:20:02Z",
        "received_at": "2026-11-02T16:20:00Z",
        "sent_at": null,
        "is_synthetic": false,
        "external_reply": null
      },
      "conversation_state": { "status": "open", "archived": false, "read": false }
    }
  ],
  "next_cursor": "v2.exampleOpaqueCursor",
  "has_more": false,
  "scan_through": "2026-11-02T16:21:00Z"
}

Errors ​

HTTPCodeWhen
403insufficient_scopeThe key does not have inbox:read.
404not_foundcampaign_id is not a campaign whose inbox the key can read, or domain names an inbox this DialEcho deployment does not run.
409resync_requiredThe cursor expired (after 30 days) or the key's or its member's access changed since it was issued. Start again without a cursor, since your last checkpoint, and restart the daily walk too.
422invalid_cursorThe cursor was not issued to this key for this feed, was changed, or was sent with different filters.
422invalid_requestA parameter is invalid. error.fields names the first invalid filter (campaign_id, domain, since), or query.limit or query.cursor.
503dependency_unavailableThe inbox or the access check is temporarily unavailable. Retry with backoff.

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