Inbox

REST endpoints for the Inbox: the people waiting for an answer, your team chats and what arrived for you, in one ordered list with Waiting, New and All tabs, the rail's count, marking a conversation as needing no reply, dismissing a row, and replying to someone new.

Overview

The Inbox is composed on the server for the person asking. Every row comes from a record that already exists (a message, a lead, a meeting answer, a brochure view, a request from another agency, a chat you are in); nothing is stored for the Inbox itself except two marks: that a conversation needs no reply, and that someone dismissed a row.

A row leaves because something happened on its record: you replied on any channel, called, booked, linked, marked it handled or dismissed it. Only two rows expire on their own: a new conversation after 14 days, and an opened brochure after 48 hours.

The endpoints on this page use the dashboard's Clerk bearer token and resolve the organization from the request. Reading is open to every member. Marking a row handled needs an active plan, exactly as on the CRM endpoints.

Endpoints

Method & pathWhoPurpose
GET /inboxAny memberOne page of the Inbox, in order
GET /inbox/countAny memberHow many people are waiting for you
POST /inbox/items/:ref/handledAnyone who can open the lead, or who has the rowMark a conversation as needing no reply, or dismiss a row
DELETE /inbox/items/:ref/handledThe sameUndo that mark or dismissal
GET /inbox/faces/:kind/:idAnyone who has the rowThe person's profile picture, the path in faceUrl
POST /new-conversations/:key/replyThe owner of the inbox it came in onReply to someone who is not a lead yet

What is in it

kindA row whenIt leaves when
awaiting_replyThe newest message on a lead, on any channel, is theirsAnyone replies on any channel, a call connects, or the row is marked handled
new_conversationA first message from someone who is not a lead yet reached one of your connected inboxesAdd as lead, link to a lead, Not a lead, or 14 days
new_leadA lead assigned to you nobody has called, emailed or messagedAny call, sent email or outbound message
unassigned_leadA new lead with nobody on it (admins, scope agency)Someone is assigned
invite_declinedA lead declined, or answered maybe to, an upcoming meeting you organisedThe meeting is moved (everyone is asked again), cancelled or accepted
brochure_openedA brochure you made for one lead was opened by someone outside your agencyAny contact with the lead after the view, or 48 hours
reconnectOne of your connected inboxes needs reconnectingIt is connected again
co_listing_invitationAnother agency invited you (or your agency, for admins) to co-list a homeAccepted or declined
partnership_requestAnother agency asked to partner (admins)Accepted or declined
open_house_tomorrowYour open house is tomorrow and agents said they are goingThe day passes
agent_requestAn agent at another agency asked to message youAccepted or declined
request_replySomeone answered your open buyer request in a chat and you have not repliedYou reply in that thread, or the request closes
post_draftA listing post on your own Instagram or LinkedIn waits for you: a draft (from a new listing, a price change, the Post action or Ask), a post that failed, or one whose account needs reconnecting. reason is { code: "post_draft", postId, channel, postKind, status }, ref the listing, automatic true when a trigger drafted itIt is published, scheduled or discarded

With a view (see below), your own chats join the list:

kindA row whenIt leaves when
team_dmA direct message with a colleague or an agent at another agency whose newest message is theirs and unread by youYou read it or reply
team_mentionAn unread message in a channel or group mentions youYou read it
team_channelA channel or group that does not wait on you (the all view only)Never waits
lead_threadA lead's conversation that waits on nobody (the all view only)Never waits

A chat is only ever yours: you see a direct message, a channel or a group only as a member of it, at every scope. An admin's members or agency scope changes which lead conversations show, never whose chats. Group chatter never waits; only a direct message to you or a mention of you does. Your own last message never waits.

Overdue tasks are not rows: they live on the calendar.

Order

Rows come back in one total order:

  1. reconnect, when a broken inbox blocks a reply on this page;
  2. the people waiting (awaiting_reply, new_conversation, new_lead, unassigned_lead) together, longest waiting first;
  3. the team (group: "team"): agent_request first, then team_dm, team_mention and request_reply, longest waiting first. A lead waiting always comes before your team;
  4. invite_declined (and a reconnect that blocks nothing);
  5. brochure_opened;
  6. the network rows;
  7. post_draft.

Inside a group, Ask may lift a row (someone asking to be left alone first, then a specific request such as a viewing) or sink it (an automatic reply). Ask never removes a row: with Ask's reading missing or not yet available in the message's language, every message still appears with the reason wrote.

A lead appears once, with its strongest reason. additionalCount says how many other reasons it has.

GET /inbox

QueryTypeNotes
scopeme | members | agencyDefault me. Members always get me.
assigneeIdsstring, repeatableUser ids for scope=members (admins).
channelemail | whatsapp | linkedin | instagram | telegramOnly rows on this channel.
qstringOnly rows whose person or agency name contains it.
cursorstringnextCursor from the previous page.
limit1 to 50Default 30.
timeZoneIANA nameFor "tomorrow" and the note's "overnight". Default UTC.
viewwaiting | new | allThe view (the dashboard's filter). Without it you get the list as it was before the tabs, with no chats, no views and no note.
viaa connected inbox id | team | agenciesOnly rows from that inbox, your colleagues' chats, or other agencies (their chats and their requests).
curl 'https://api.fondaro.com/inbox?view=waiting&scope=me&limit=30&timeZone=Europe/Madrid' \
  -H 'Authorization: Bearer <clerk-session-token>'
{
  "asOf": "2026-09-25T10:00:00.000Z",
  "scope": "me",
  "items": [
    {
      "id": "message:6b0c9d0e-5d7a-4e0a-9d59-0f3c1e2a7b11",
      "kind": "awaiting_reply",
      "group": "people",
      "ref": { "kind": "lead", "id": "1042" },
      "conversationKey": null,
      "messageRef": { "kind": "message", "id": "6b0c9d0e-5d7a-4e0a-9d59-0f3c1e2a7b11" },
      "connectedAccountId": "3f0c6f8e-1c1a-4a7e-9e7b-2d0c7f6a1b11",
      "occurredAt": "2026-09-25T08:58:12.000Z",
      "lead": {
        "id": 1042,
        "firstName": "Lead",
        "lastName": "Example",
        "language": "en",
        "assigneeIds": ["user_2abc"]
      },
      "title": "Lead Example",
      "channel": "whatsapp",
      "reason": { "code": "wrote", "channel": "whatsapp" },
      "replyOwed": true,
      "automatic": false,
      "fromAnAgency": false,
      "handleable": true,
      "additionalCount": 0,
      "offers": [],
      "preview": "Hi! Is Saturday 11 still ok?",
      "chat": null,
      "faceUrl": "/inbox/faces/message/6b0c9d0e-5d7a-4e0a-9d59-0f3c1e2a7b11"
    },
    {
      "id": "chat:0d1e2f3a-4b5c-4d6e-8f90-a1b2c3d4e5f6",
      "kind": "team_dm",
      "group": "team",
      "ref": { "kind": "conversation", "id": "0d1e2f3a-4b5c-4d6e-8f90-a1b2c3d4e5f6" },
      "conversationKey": null,
      "messageRef": null,
      "connectedAccountId": null,
      "occurredAt": "2026-09-25T09:40:00.000Z",
      "lead": null,
      "title": "Ana Soler",
      "channel": null,
      "reason": { "code": "team", "waits": "dm" },
      "replyOwed": true,
      "automatic": false,
      "fromAnAgency": false,
      "handleable": false,
      "additionalCount": 0,
      "offers": [],
      "preview": "Can you take the viewing on Saturday?",
      "chat": {
        "conversationId": "0d1e2f3a-4b5c-4d6e-8f90-a1b2c3d4e5f6",
        "type": "dm",
        "scope": "internal",
        "status": "active",
        "waits": "dm",
        "unreadCount": 1,
        "mentionCount": 0,
        "person": { "userId": "user_2ana", "avatarUrl": null },
        "last": { "fromViewer": false, "senderFirstName": "Ana", "attachment": null },
        "mentionedBy": null
      },
      "faceUrl": null
    }
  ],
  "nextCursor": null,
  "groups": { "reconnect": 0, "people": 1, "team": 1, "invites": 0, "brochures": 0, "network": 0, "posts": 0 },
  "counts": { "awaiting_reply": 1, "team_dm": 1, "new_conversation": 0, "...": 0 },
  "waitingCount": 2,
  "hasConnectedAccount": true,
  "view": "waiting",
  "views": { "waiting": 2, "new": 0 },
  "note": {
    "sentences": [
      {
        "shape": "wrote",
        "people": [{ "rowId": "message:6b0c9d0e-5d7a-4e0a-9d59-0f3c1e2a7b11", "ref": { "kind": "lead", "id": "1042" }, "name": "Lead Example" }],
        "others": 0,
        "questionId": null
      },
      {
        "shape": "team",
        "person": { "rowId": "chat:0d1e2f3a-4b5c-4d6e-8f90-a1b2c3d4e5f6", "ref": { "kind": "conversation", "id": "0d1e2f3a-4b5c-4d6e-8f90-a1b2c3d4e5f6" }, "name": "Ana Soler" },
        "mention": false,
        "channelName": null
      }
    ]
  }
}
  • ref is what the row opens: the lead, a calendar_event, a brochure, a listing, an organization, an open_house, or a chat conversation (agent_request, request_reply and the team rows). It is null for new_conversation (open it by conversationKey with the new conversation endpoints) and for reconnect (use connectedAccountId).
  • reason is a code with parameters, for the client to put into words: ask (questionId, Ask's reading), wrote (channel), new_conversation (with askReason, why they wrote, when Ask read it), enquiry (why a new lead enquired, when Ask read it), origin (where a new lead came from), invite_declined, brochure_opened, reconnect, co_listing_invitation, partnership_request, open_house_tomorrow, agent_request, request_reply, team (a chat; waits is dm, mention or null) and thread (a lead's conversation in all: the channel of the newest message and whether it was yours).
  • preview is the person's own words, at most about 140 characters: a message's first line, an email's subject, a new conversation's newest message, a chat's last message. It is null when there are no words (a new lead, a voice note: chat.last.attachment then says voice, gif or card).
  • chat carries a team row's conversation: direct message or channel, internal (colleagues) or external (another agency), why it waits, the unread and mention counts, the other person of a direct message, and who sent the last message. A person is always a name, never an email.
  • faceUrl is the path of the person's own profile picture on WhatsApp, Instagram, LinkedIn or Telegram, for a lead's or a new conversation's row on those channels, and null otherwise. Fetch it with your bearer token: it answers the image (cached privately for a day, with an ETag), or 404 when they have none or you cannot see the row.
  • connectedAccountId is the broken inbox on reconnect, and the inbox a message arrived on for a message row (what via filters on).
  • offers are Ask's suggestions for the message, the same ones the lead page shows. Accepting one always opens a proposal for you to approve.
  • groups, counts, waitingCount and views cover the whole Inbox, not the page and not the q, channel or via filter.
  • Paging is by position in the order, so a row that clears between two pages never shifts the next one.

Tabs

viewWhat it holds
waitingEvery row above that waits, your waiting chats included, in the order above
newnew_lead, new_conversation, unassigned_lead and agent_request, in the same order
allEvery conversation, newest activity first: one row per lead with a message or email, per new conversation, and per chat you are in. Requests to you are pinned above the first page. A lead or chat that waits keeps its waiting row.

views counts the rows of waiting and new (all has no count). With a view, q also matches the last words (preview), not only the name. Each view has its own cursor; a cursor from another view starts from the top.

The note

note is on the first page of every view (null on later pages, and when there is nothing to say): one or two sentences chosen by the server, as parts your client puts into words, so every client says the same thing. The first shape that fits wins, in this order: wrote (people who wrote), new_people (new leads from one source, with overnight before noon for leads since 18:00 the day before in timeZone), invite, brochure, then team (a direct message or a mention, never ahead of a lead sentence). A sentence names at most two people; others counts only the people beyond them. It never carries a total. Each name's rowId is the row it focuses. The dashboard no longer draws the note (2026-09-27); it stays on the wire for other clients.

GET /inbox/count

The rail's number: people waiting for you (awaiting_reply, new_conversation, new_lead), one per lead, plus the chats that owe you an answer (team_dm, team_mention) and requests to you (agent_request), always in your own scope.

curl 'https://api.fondaro.com/inbox/count' \
  -H 'Authorization: Bearer <clerk-session-token>'
{ "waitingCount": 3 }

Handled

"No reply needed" is kept on the message it clears, so it holds on the web and the phone alike. The next message from the lead is a new row, so the lead comes back by itself.

:ref is the row's messageRef as a key (email:<uuid> or message:<uuid>, URL-encoded), or lead:<id> for the lead's newest message, which must be theirs. Only a message on a lead you can open can be marked. A row with no message is dismissed instead (below). New conversations are decided with the new conversation endpoints, and chats clear when you read them.

curl -X POST 'https://api.fondaro.com/inbox/items/message%3A6b0c9d0e-5d7a-4e0a-9d59-0f3c1e2a7b11/handled' \
  -H 'Authorization: Bearer <clerk-session-token>'
{
  "messageRef": "message:6b0c9d0e-5d7a-4e0a-9d59-0f3c1e2a7b11",
  "handled": true,
  "handledAt": "2026-09-25T10:02:41.000Z"
}

Marking a message that is already marked keeps the first mark. DELETE on the same path removes it and returns "handled": false.

StatusWhen
400:ref is not an email, a message, a lead or a row that can be dismissed
403Your organization has no active plan
404No inbound message on a lead you can open, or no such row in your Inbox

Dismiss

A row with no message of its own (new_lead, unassigned_lead, invite_declined, brochure_opened, reconnect, co_listing_invitation, partnership_request, open_house_tomorrow, agent_request, request_reply, post_draft) is dismissed on the same path, with the row's id as :ref. With a view, these rows come back with handleable: true; without one, handleable still means a message to mark.

The dismissal holds for everyone in your organization, on the web and the phone, until something newer happens on the row: another view of the brochure, a new answer to the invite, a newer reply to your buyer request, the inbox breaking again. Then the row comes back and can be dismissed again. You can dismiss only a row your own Inbox shows you right now. timeZone (an IANA name, default UTC) finds an open house "tomorrow" where you are.

curl -X POST 'https://api.fondaro.com/inbox/items/new_lead%3Alead%3A1042/handled?timeZone=Europe/Madrid' \
  -H 'Authorization: Bearer <clerk-session-token>'
{
  "messageRef": "new_lead:lead:1042",
  "handled": true,
  "handledAt": "2026-09-25T10:02:41.000Z"
}

DELETE on the same path is Undo: the row is back on the next read.

Read

Every row carries unread, the dot: something new since you last opened this conversation. It is yours alone, on the web and the phone; a colleague opening the same lead does not clear it for you. It says nothing about owing an answer: a row you have opened still waits in Waiting and still counts in waitingCount until you answer or mark it handled.

Rowunread is true while
A lead (awaiting_reply, lead_thread, new_lead, unassigned_lead)Their newest message, or the enquiry of a new lead, is newer than your last open. A conversation whose newest message is yours is read.
new_conversationTheir newest message is newer than your last open, until you reply
A chat (team_dm, team_mention, team_channel)It has messages you have not read. Chats are read by reading them, not here.
Anything elseYou have not opened it since it happened (another view of the brochure, a new answer to the invite)

POST /inbox/read marks a conversation opened now. key names it: lead:<id> for any row of a lead, conversation:<conversationKey> for a new conversation, the row's id for anything else. You can read only what your own Inbox could show you: a lead you can open, a new conversation in one of your inboxes, a row your Inbox holds right now. timeZone (an IANA name, default UTC) finds an open house "tomorrow" where you are. Reading needs no active plan.

curl -X POST 'https://api.fondaro.com/inbox/read' \
  -H 'Authorization: Bearer <clerk-session-token>' \
  -H 'Content-Type: application/json' \
  -d '{ "key": "lead:1042" }'
{ "key": "lead:1042", "readAt": "2026-09-25T10:02:41.000Z" }

Reading again moves readAt. Afterwards inbox:changed goes to your own sockets only, so your other tabs and devices refetch.

StatusWhen
400key is not a lead, a new conversation or a row that reads by its id (a chat, or a lead's row by its own id)
404No such lead you can open, conversation in your inboxes, or row in your Inbox

Reply to someone new

POST /new-conversations/:key/reply answers a new_conversation row from the inbox they wrote to, before you decide whether they are a lead. :key is the row's conversationKey, URL-encoded. Only the owner of that inbox can reply, exactly as only they can open the conversation.

BodyTypeNotes
bodystring, 1 to 20,000 charactersPlain text. A WhatsApp, Instagram, LinkedIn or Telegram message is at most 4,000.
subjectstringEmail only. Default: Re: and their newest subject.
ccstring[]Email only. Up to 10 addresses; one already in To is dropped, and each is kept once.
bccstring[]Email only. Up to 10 addresses; one already in To or Cc is dropped.
bodyHtmlstring, at most 100,000 charactersEmail only. The formatted body, sent instead of body's paragraphs after it is cleaned to p, br, strong, b, em, i, u, s, ul, ol, li, blockquote and a (http, https or mailto links only). body stays the plain version.

A chat reply ignores cc, bcc and bodyHtml. The From is always the inbox they wrote to, so the reply stays in their thread.

An email goes out from that mailbox as a reply to their newest message, so it lands in the same thread for them. A chat message goes into the chat they opened, a few seconds after your last message from that account, like every message you send. The reply stays with the conversation: it moves to the lead when you add them as a lead or link them, and it goes with the conversation on Not a lead or after 14 days. The row stays in your Inbox until you decide, without its dot (replyOwed: false), and the conversation's newest message is yours (answered: true).

curl -X POST 'https://api.fondaro.com/new-conversations/email.3f0c6f8e-1c1a-4a7e-9e7b-2d0c7f6a1b11.YnV5ZXJAZXhhbXBsZS5jb20/reply' \
  -H 'Authorization: Bearer <clerk-session-token>' \
  -H 'Content-Type: application/json' \
  -d '{ "body": "Hello, the villa is free to view on Saturday at 11." }'

With formatting and a copy:

curl -X POST 'https://api.fondaro.com/new-conversations/email.3f0c6f8e-1c1a-4a7e-9e7b-2d0c7f6a1b11.YnV5ZXJAZXhhbXBsZS5jb20/reply' \
  -H 'Authorization: Bearer <clerk-session-token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "body": "Hello, the villa is free to view on Saturday at 11.",
    "bodyHtml": "<p>Hello, the villa is free to view on <strong>Saturday at 11</strong>.</p>",
    "cc": ["partner@example.com"]
  }'
{
  "message": {
    "id": "9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
    "kind": "email",
    "direction": "outbound",
    "subject": "Re: Villa in Mijas",
    "body": "Hello, the villa is free to view on Saturday at 11.",
    "messageAt": "2026-09-25T10:04:00.000Z",
    "attachments": []
  }
}

The conversation's messages (GET /new-conversations/:key) now carry direction, inbound or outbound.

StatusWhen
400An empty body, or a chat message over 4,000 characters
403A sandbox organization (nothing is sent from one), or no active plan
404No such conversation in your inboxes
409The inbox cannot send: code is mailbox_needs_reconnect or message_needs_reconnect (reconnect it), message_account_restricted, or mailbox_not_connected / message_not_connected (connect it again). integration names the connection page
429Sending is paused: message_paced, message_rate_limited or mailbox_rate_limited
502The provider did not take it; try again

Live updates

After a message reaches a lead, a new conversation arrives, a reply goes to one, or a row is marked, dismissed or brought back, the dashboard's chat socket sends inbox:changed with an empty payload to your organization. Refetch GET /inbox. A client should also refetch every 60 seconds, which covers a dropped socket.

MCP

list_inbox on the MCP server (scope crm:read) returns the list as it was before the tabs, without offers, with preview on lead rows. It never returns a chat: team rows and chat words stay out of the assistant and MCP. There is no tool to mark a row handled.