Lessly Support
A support inbox for your product: threads your end users open, answered by your own people.
Lessly Support runs a support inbox for your product. Someone using your application opens a thread, your team answers it in messages, and the thread moves through the statuses your product defines until it is resolved. Labels group threads however you like, agents are the people and bots that answer them, and every change worth reacting to is emitted as an event.
You work with your own concepts: threads, messages, statuses, labels, agents, attachments. Keeping a conversation in order, deciding which reply the end user may see, and holding the files a message carries are Support’s job.
Support is headless. It is a backend and a set of MCP tools with no screens of its own, so it has no entry in the Product App navigation. The specialists’ workplace is a separate Lessly App that runs in the workspace and talks to this API — see Where Support sits.
Two contours that never mix
Support holds two kinds of “who”, and it never confuses them.
Ours is the caller. Lessly Identity answers one question — is this call legitimate, and which product does it belong to — and the answer is always someone on your side: a specialist in the Workspace app, your own backend, or a bot. It never touches the content of a thread.
Yours is the end user. The people who write to your support are not Lessly accounts and Support knows nothing about them. They arrive as an authorExternalId, an opaque identifier from your own user base, optionally with a display name you already have. Support stores both verbatim: it resolves nothing, enriches nothing, and calls nobody to look anything up. There is no end-user directory here, and Support implements no login, signup or session of its own.
That is why a message’s author is either an external identifier or one of your agents, and never a Lessly account on either side.
Product, thread, message, status, agent
Product is the boundary of everything Support does for you. Every thread, message, status, label, agent and attachment belongs to exactly one product, and nothing you create in one product is visible from another. A thread, status or agent owned by another product is reported as not found, never as forbidden. The product itself is a platform concept — you manage it there, not here.
Thread is one conversation. It is either a ticket — the end user wants something fixed — or feedback, which is unsolicited input. A thread carries a title, a priority (low, normal, high, urgent), any number of labels, a free-form metadata object that is yours to fill, an optional rating from 0 to 10, and at most one assigned agent. It is opened on behalf of an external end user: authorExternalId is an opaque identifier from your own user base, stored verbatim and never resolved, normalised or enriched. It is not a Lessly identity.
Message is one entry in a thread. Messages exist only inside a thread and read forward, oldest first. A message has exactly one author — either an external end user or one of your agents, never both and never neither — and three independent axes: direction (inbound from the end user, outbound from support), visibility (public, or an internal note the end user never sees), and which of the two author fields is set. Every combination is legal: an internal note may be inbound as well as outbound.
Status is where a thread sits, and it comes from a dictionary your product owns. Label is a free-form tag. Agent is a profile that writes outbound messages.
- Threads and messages — opening, editing and filtering threads, and what a message carries
- Statuses — the dictionary, its four categories and archiving
- Agents — profiles, bots, assignment and deactivation
- Labels and attachments — tags, and sending a file with a message
The status dictionary
A status has a name your customers see and a category that automation and reporting read: open, pending, resolved or closed. That split is the point of the dictionary — you may rename pending to “waiting on customer” without breaking anything, because nothing inside Support reads the name.
A product that has never opened its settings gets four statuses seeded the first time the dictionary is listed: open, pending, resolved and closed, one per category, in that display order. You can add your own, recolour them, and reorder them.
A category is fixed once threads sit on the status. Changing it later would rewrite the history of every metric derived from it, so it is refused while any thread references the status.
Statuses are never deleted, only retired. Archiving one names a live status every thread on it moves to, in the same transaction, so no thread is ever left on an archived status — and an archived status cannot receive threads again.
There are no transition rules: any status may follow any other. A new thread opens on the product’s default open status — the non-archived open-category status with the lowest display order — and there is no way to ask for a different one at creation.
One transition happens on its own. A public inbound message posted to a thread parked in the pending category pulls that thread back to open. An internal note cannot trigger it, because the end user never sees it and so it is not evidence that they replied; an outbound message cannot either, because that is the support side talking. The rule reads the category, not the name.
Labels and agents
A label is a free-form tag: nothing automates off it, names are unique within the product, and a thread may carry any number of them. Editing a thread’s labels replaces the whole set rather than adding to it. Filtering by labels narrows the queue — asking for two labels returns threads that carry both.
An agent is the profile an end user sees next to a reply: a display name, optionally a job title and an avatar. Binding a Lessly identity is optional and the value is stored verbatim — nothing resolves it or checks that the user exists, and at most one agent per product may claim a given Lessly user. An unbound profile is a bot or your own backend, and it replies exactly like a human agent does.
Agents are deactivated rather than removed. A deactivated agent keeps every message it wrote and every thread it already sits on, but cannot be assigned to another one.
Assignment itself is one agent at a time, and clearing it is an ordinary operation: passing no agent returns the thread to the unassigned queue.
Attachments
The bytes of a file never pass through the API. A file is reserved against a thread first, which answers with a short-lived signed URL; you PUT the file to that URL with the same Content-Type the reservation declared, then confirm the upload. Storage is asked whether the object is really there and how large it is, and only then does the attachment become uploaded.
Until it is confirmed the attachment is pending: it cannot be named by a message and does not appear in message output. A file is at most 25 MiB, the size is declared up front so an oversized upload is refused before a URL is issued, and the real size is checked again on confirmation.
A message may carry up to ten attachments. Each must belong to the same thread, be uploaded, and be unclaimed — a file another message already carries is refused, and one bad id refuses the whole message.
Events and notifications
Six events are emitted as things happen, whichever contour caused them: support/thread.created, support/thread.assigned, support/thread.status-changed, support/message.created, support/survey.published and support/response.created. Each carries the product it happened in and an idempotency key, so the same action delivered twice is deduplicated. A status change carries both the destination’s name and its category, so a consumer can act on the move without reading the thread back.
Those six are what Support emits internally. An outbound webhook endpoint subscribes to a list of its own — five of the six, written with dots rather than slashes, since a thread being opened is not delivered back to the backend that opened it. See Receive Support events.
Support also has its own section in the workspace notification settings, called Support: new threads, customer replies and assignments in this product’s inbox. A person tunes the section; which event it was travels inside the notification itself. See Notifications.
Where Support sits
Support is headless, so its screens live in a separate Lessly App that talks to this API. In the workspace the App appears as Support, and its own menu has four entries. Overview is the dashboard. Queue is the inbox: every thread of the product, narrowed by the filters above. Statuses is the status dictionary and where you add, recolour, reorder and archive one. Agents is the list of agent profiles.
One screen sits a level down and has no menu entry of its own: a single thread, reached from the queue, with its messages in order.
The App is not the only way in. The same operations are exposed as support_* MCP tools and over the REST API under /support, and @lessly/sdk-app generates a support namespace from the published operation catalogue — there is no hand-written Support SDK package to install. See Tools, SDK and errors.
Support publishes a public perimeter as well. Twelve keyed HTTP routes let your own backend open threads, read the queue, post and read messages, read the status, label and agent dictionaries, read and answer a published survey, and ask which delivery rules apply to a page — all without a platform login. See Public HTTP API. Everything a specialist does stays on the authenticated, product-scoped surface: assignment, status moves, editing agent profiles, attachments and the webhook registry are not published there at all.
Either way a thread is opened by your backend, not by an end user’s browser. The public routes are keyed with a product public key that must never reach a browser, and there is still no unauthenticated route anywhere in Support and no login for an end user to have.
Where to go next
- Answer your first thread: seven steps from an empty product to an answered ticket.
- Threads and messages: the conversation, its author, and the product-wide catch-up read.
- Surveys: asking for feedback rather than waiting for it, and where a survey is shown.
- Public HTTP API: what your backend calls without a platform login.