REST API
The Support area publishes 44 REST endpoints. Paths are relative to https://api.lessly.com; see Authentication for the required header.
GET/support/agents
List this product support agents by display name. Deactivated agents are hidden unless asked for: they keep every message they wrote but cannot take new threads. Through the public edge the profiles come back without lesslyUserId — the binding never leaves the authenticated plane
MCP tool: support_agent_list
Parameters
| Name | In | Type | Required | Description |
|---|
| includeInactive | query | boolean | — | Also return deactivated agents (default false) |
Response
| Field | Type | Always | Description |
|---|
| agents | array | • | The product support agents, by display name |
POST/support/agents
Create a support agent profile for this product. Binding a Lessly identity is optional and the value is stored verbatim — nothing resolves it or checks that the user exists. An unbound profile is a bot or the client's own backend, and it replies exactly like a human agent does
MCP tool: support_agent_create
Parameters
| Name | In | Type | Required | Description |
|---|
| displayName | body | string | • | Name the end user sees next to this agent replies |
| title | body | string | — | Job title shown under the name, e.g. "Support engineer" |
| avatarUrl | body | string | — | Absolute URL of the agent avatar image |
| lesslyUserId | body | string | — | Lessly identity to bind this profile to, stored verbatim as an opaque string. It is never resolved, validated against the platform or enriched. Omit it for a bot or a backend profile. At most one agent per product may claim a given Lessly user |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Agent id |
| title | string | • | Job title shown under the name, or null if unset |
| isActive | boolean | • | A deactivated agent keeps its history but can no longer be put on a thread |
| avatarUrl | string | • | Avatar image URL, or null if unset |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| displayName | string | • | Name shown next to the agent replies |
| lesslyUserId | string | • | Bound Lessly identity, stored verbatim and resolved by nobody. Null for an unbound profile — a bot or the client's own backend, which replies just like anyone else |
GET/support/agents/:id
Get one support agent by id, deactivated ones included. An agent belonging to another product is reported as not found. Through the public edge the profile comes back without lesslyUserId — the binding never leaves the authenticated plane
MCP tool: support_agent_get
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Agent id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Agent id |
| title | string | • | Job title shown under the name, or null if unset |
| isActive | boolean | • | A deactivated agent keeps its history but can no longer be put on a thread |
| avatarUrl | string | • | Avatar image URL, or null if unset |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| displayName | string | • | Name shown next to the agent replies |
| lesslyUserId | string | • | Bound Lessly identity, stored verbatim and resolved by nobody. Null for an unbound profile — a bot or the client's own backend, which replies just like anyone else |
PATCH/support/agents/:id
Edit an agent profile. Pass null to clear the title, the avatar or the Lessly binding; omit a field to leave it alone. Setting isActive to false deactivates the agent: it keeps every message it wrote and every thread it already sits on, but cannot be assigned to another one
MCP tool: support_agent_update
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Agent id |
| displayName | body | string | — | Name the end user sees next to this agent replies |
| title | body | string | — | New job title, or null to clear it |
| avatarUrl | body | string | — | New avatar URL, or null to clear it |
| lesslyUserId | body | string | — | Lessly identity to bind, or null to unbind. Stored verbatim, resolved by nobody |
| isActive | body | boolean | — | Deactivate or reactivate. A deactivated agent keeps every message it wrote and every thread it already sits on, but cannot be assigned to another one |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Agent id |
| title | string | • | Job title shown under the name, or null if unset |
| isActive | boolean | • | A deactivated agent keeps its history but can no longer be put on a thread |
| avatarUrl | string | • | Avatar image URL, or null if unset |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| displayName | string | • | Name shown next to the agent replies |
| lesslyUserId | string | • | Bound Lessly identity, stored verbatim and resolved by nobody. Null for an unbound profile — a bot or the client's own backend, which replies just like anyone else |
POST/support/attachments
Reserve a file on a support thread and get a short-lived signed URL to upload it to. The bytes never pass through this API: PUT them to the returned uploadUrl with the same Content-Type, then call support_attachment_finalize. Until finalized the attachment is pending — it cannot be attached to a message and is invisible in message output. A thread belonging to another product is reported as not found
MCP tool: support_attachment_create
Parameters
| Name | In | Type | Required | Description |
|---|
| threadId | body | string | • | Thread the file belongs to. An attachment is created against a thread first and claimed by one of its messages later, when that message is posted |
| fileName | body | string | • | Display name of the file, e.g. "invoice.pdf". Only the base name is used for the storage key; any directory part is discarded |
| contentType | body | string | • | MIME type of the file. The upload URL is signed for exactly this type, so the PUT must send the same Content-Type header or storage rejects it |
| sizeBytes | body | integer | • | Size of the file in bytes, at most 26214400. Declared up front so an oversized upload is refused before a URL is issued; finalize checks the real size again |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Attachment id |
| status | string | • | pending: the row exists and an upload URL was issued, but no bytes have been confirmed. uploaded: finalize saw the object in storage. Only uploaded attachments can be attached to a message or downloaded |
| fileName | string | • | Display name of the file |
| threadId | string | • | Thread this attachment belongs to |
| createdAt | string | • | ISO-8601 creation timestamp |
| messageId | string | • | The message that carries this attachment, or null while nothing claims it yet |
| sizeBytes | integer | • | Size in bytes: what the caller declared while the attachment is pending, what storage reported once it is uploaded |
| uploadUrl | string | • | Short-lived signed URL. PUT the file bytes here with the same Content-Type, then call support_attachment_finalize. The URL is a bearer credential for this one object |
| uploadedAt | string | • | ISO-8601 timestamp of the confirmed upload, or null while pending |
| contentType | string | • | MIME type declared when the attachment was created |
| uploadExpiresAt | string | • | ISO-8601 moment the upload URL stops working |
POST/support/attachments/finalize
Confirm that an upload landed. Storage is asked whether the object exists and how large it is; if it is not there the call is refused and the attachment stays pending. On success the attachment becomes uploaded — it can now be named in support_message_create and carries a download URL. Calling it again on an uploaded attachment is safe and simply returns a fresh download URL
MCP tool: support_attachment_finalize
Parameters
| Name | In | Type | Required | Description |
|---|
| attachmentId | body | string | • | The attachment whose upload should be confirmed, as returned by the create call |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Attachment id |
| status | string | • | pending: the row exists and an upload URL was issued, but no bytes have been confirmed. uploaded: finalize saw the object in storage. Only uploaded attachments can be attached to a message or downloaded |
| fileName | string | • | Display name of the file |
| threadId | string | • | Thread this attachment belongs to |
| createdAt | string | • | ISO-8601 creation timestamp |
| messageId | string | • | The message that carries this attachment, or null while nothing claims it yet |
| sizeBytes | integer | • | Size in bytes: what the caller declared while the attachment is pending, what storage reported once it is uploaded |
| uploadedAt | string | • | ISO-8601 timestamp of the confirmed upload, or null while pending |
| contentType | string | • | MIME type declared when the attachment was created |
| downloadUrl | string | • | Short-lived signed URL to read the uploaded file |
GET/support/labels
List this product label dictionary by name. A product that has invented no tags yet gets an empty list — unlike statuses, there is nothing to seed
MCP tool: support_label_list
Parameters
No parameters.
Response
| Field | Type | Always | Description |
|---|
| labels | array | • | The product label dictionary, by name |
POST/support/labels
Add a label to this product label dictionary. A label is a free-form tag: nothing automates off it and a thread may carry any number of them. Names are unique per product
MCP tool: support_label_create
Parameters
| Name | In | Type | Required | Description |
|---|
| name | body | string | • | Label name as the customer wants to see it. Unique within the product |
| color | body | string | — | Optional display colour as a six-digit hex string, e.g. #2563eb |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Label id |
| name | string | • | Label name, unique within the product |
| color | string | • | Display colour as a hex string, or null if uncoloured |
| createdAt | string | • | ISO-8601 creation timestamp |
GET/support/messages
List messages oldest first — the conversation reads forward. Name a threadId to read one thread, or omit it to read every thread of this product, which is how a client catches up on what it missed: pass since with the timestamp of the last message it has, then follow nextCursor until it comes back null. Paging is stable across messages that share a timestamp, so a walk neither skips nor repeats one. Filter by direction and by visibility; omit visibility to get public messages and internal notes together. A thread belonging to another product is reported as not found
MCP tool: support_message_list
Parameters
| Name | In | Type | Required | Description |
|---|
| threadId | query | string | — | Thread whose messages to list. Omit to list the messages of every thread of this product — the catch-up read |
| direction | query | string | — | Only messages in this direction |
| visibility | query | string | — | Only messages with this visibility. Omit to get both public and internal |
| since | query | string | — | ISO-8601 timestamp. Only messages created strictly after it — pass the createdAt of the last message you already have |
| cursor | query | string | — | Opaque position from a previous nextCursor. Resumes exactly where that page ended, with no gap and no repeat even when messages share a timestamp. Never construct one, and do not combine it with offset |
| limit | query | integer | — | Page size, 1..100 (default 50) |
| offset | query | integer | — | Number of messages to skip. The older pagination, kept for callers that already use it; prefer cursor, which cannot drift as messages arrive |
Response
| Field | Type | Always | Description |
|---|
| limit | integer | • | Page size that was applied |
| offset | integer | • | Offset that was applied |
| messages | array | • | The requested page of messages, oldest first — the conversation reads forward |
| nextCursor | string | • | Position to resume from: pass it back as cursor for the next page. Null when this page is the last one |
POST/support/messages
Post a message into a support thread. A message has exactly one author: either an opaque external identifier from the client's own user base, or a support agent — never both and never neither. Direction, authorship and visibility are independent: an internal note may be inbound or outbound. Posting a message moves the parent thread's last reply timestamp. A message may carry files: create and finalize them first, then name them in attachmentIds. Through the public edge an agent author is accepted only if that agent carries no Lessly binding — a bot or the calling backend; an agent bound to a Lessly identity may author only from a signed-in session
MCP tool: support_message_create
Parameters
| Name | In | Type | Required | Description |
|---|
| threadId | body | string | • | Id of the thread this message belongs to |
| body | body | string | • | The message text, stored verbatim |
| direction | body | string | • | inbound: written by the external end user. outbound: written by support, going out to them |
| authorExternalId | body | string | — | Opaque identifier of the external end user in the client's own user base, stored verbatim. Set this exactly when the message comes from outside; leave authorAgentId empty |
| authorAgentId | body | string | — | The support agent of this product who wrote the message. Set this exactly when the message comes from inside the support contour; leave authorExternalId empty. An agent of another product is reported as not found |
| visibility | body | string | — | Defaults to public when omitted. Internal notes are never shown to the end user |
| attachmentIds | body | array | — | Files this message carries, created with support_attachment_create and confirmed with support_attachment_finalize beforehand. Each must be an uploaded attachment of the same thread that no other message already carries; otherwise the whole message is refused. At most 10 per message |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Message id |
| body | string | • | The message text, byte-identical to what was stored |
| threadId | string | • | Thread this message belongs to |
| createdAt | string | • | ISO-8601 creation timestamp |
| direction | string | • | inbound: written by the external end user. outbound: written by support, going out to them |
| visibility | string | • | public: part of the conversation the external end user sees. internal: a note between agents that never leaves the support contour |
| attachments | array | • | Files this message carries, each with a freshly signed download URL. Empty when there are none. Attachments whose upload was never confirmed are never listed here |
| authorAgentId | string | • | The agent author, or null when the external end user wrote the message |
| authorExternalId | string | • | The external author, or null when an agent wrote the message |
GET/support/responses
List the responses of ONE survey, newest first — surveyId is required, because a page mixing two surveys would carry answers keyed by two different question sets. Filters: score range (scoreMin/scoreMax, inclusive — a response with no score matches neither bound), a creation window (from/to), completed, plus pagination
MCP tool: support_response_list
Parameters
| Name | In | Type | Required | Description |
|---|
| surveyId | query | string | • | Required: responses are always read one survey at a time |
| scoreMin | query | integer | — | Only responses scored at or above this value (inclusive). A response with no score matches neither bound — not even at the bottom of the scale |
| scoreMax | query | integer | — | Only responses scored at or below this value (inclusive). A response with no score matches neither bound — not even at the bottom of the scale |
| from | query | string | — | Only responses created at or after this ISO-8601 timestamp |
| to | query | string | — | Only responses created at or before this ISO-8601 timestamp |
| completed | query | boolean | — | true: only responses that reached the end. false: only partial ones |
| limit | query | integer | — | Page size, 1..100 (default 25) |
| offset | query | integer | — | Number of responses to skip |
Response
| Field | Type | Always | Description |
|---|
| limit | integer | • | Page size that was applied |
| offset | integer | • | Offset that was applied |
| responses | array | • | The requested page of responses, newest first |
GET/support/responses/:id
Get one survey response by id, with its answers exactly as they were submitted. A response belonging to another product is reported as not found
MCP tool: support_response_get
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Response id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Response id |
| score | integer | • | The answer of the first nps or rating question, raw and never rescaled, or null when neither was answered. Null is not zero — 0 is a real score at the bottom of an NPS scale |
| answers | object | • | Answers keyed by question id, exactly as stored |
| metadata | object | • | Free-form client-owned JSON |
| surveyId | string | • | The survey that was answered |
| completed | boolean | • | Whether the respondent reached the end |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| authorExternalId | string | • | The respondent, byte-identical to what was stored |
| escalatedThreadId | string | • | The feedback thread this response was escalated into, or null while it has not been |
POST/support/responses/:id/escalate
Turn a survey response into a feedback thread so a person can follow up. The thread opens on the product’s default open status, titled with the survey’s name, authored by the same external respondent, carrying the response metadata plus surveyResponseId, and rated with the response score when it fits 0..10. No message is written — the answers stay on the response, and neither the end user nor an agent typed anything. A response that was already escalated is refused with a conflict naming the thread it went to
MCP tool: support_response_escalate
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Response id |
Response
| Field | Type | Always | Description |
|---|
| thread | object | • | The feedback thread that was opened |
| response | object | • | The response, with escalatedThreadId now set |
GET/support/rules
List this product’s delivery rules, newest first, optionally narrowed to one survey, to the live or switched-off ones, or to the rules that apply to a given page URL and device. This is the same answer a client’s runtime fetches through the public edge, where a URL is required and only live rules on published surveys are visible
MCP tool: support_rule_list
Parameters
| Name | In | Type | Required | Description |
|---|
| surveyId | query | string | — | Only rules delivering this survey |
| enabled | query | boolean | — | true: only live rules. false: only switched-off ones |
| url | query | string | — | Only rules whose URL match applies to this page URL |
| device | query | string | — | The device the page is rendered on. Without it only rules targeting any device match |
| limit | query | integer | — | Page size, 1..100 (default 25) |
| offset | query | integer | — | Number of rules to skip |
Response
| Field | Type | Always | Description |
|---|
| limit | integer | • | Page size that was applied |
| rules | array | • | The requested page of rules, newest first |
| offset | integer | • | Offset that was applied |
POST/support/rules
Write a delivery rule: where a survey should be shown (a URL match), on which devices, when (immediately, after a delay, or on the client’s own event) and how often. Nothing here runs on this side — the rule is configuration a client’s own runtime reads back through the public edge and acts on. A rule reaches that edge only while its survey is published, so a rule may be written against a draft and simply waits
MCP tool: support_rule_create
Parameters
| Name | In | Type | Required | Description |
|---|
| surveyId | body | string | • | The survey this rule delivers. It must belong to this product |
| urlMatch | body | object | • | Which pages this rule applies to |
| trigger | body | object | • | When to show the survey once the page matches |
| frequency | body | object | • | How often the same person may see this survey |
| device | body | string | — | Which devices this rule applies to (default any) |
| enabled | body | boolean | — | Whether the rule is live. A rule is created enabled unless this says otherwise |
| attributeConditions | body | array | — | Conditions on the visitor’s attributes. Omit for a rule that applies to everyone |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Rule id |
| device | string | • | Which devices the rule applies to. any is the rule that does not care and is the default |
| enabled | boolean | • | Whether the rule is live |
| trigger | object | • | — |
| surveyId | string | • | The survey this rule delivers |
| urlMatch | object | • | Which pages match |
| createdAt | string | • | ISO-8601 creation timestamp |
| frequency | object | • | — |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| attributeConditions | array | • | Conditions on the visitor’s attributes, ALL of which must hold |
DELETE/support/rules/:id
Delete a delivery rule for good. Unlike a survey, a rule is not archived — it has no responses behind it and nothing outlives it, so the row is removed and cannot be recovered. The survey it targeted is untouched. To stop delivery reversibly, set enabled to false instead
MCP tool: support_rule_delete
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Rule id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Id of the rule that was deleted |
| deleted | boolean | • | Always true — the rule is gone |
GET/support/rules/:id
Get one delivery rule by id, with its whole configuration. A rule belonging to another product is reported as not found
MCP tool: support_rule_get
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Rule id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Rule id |
| device | string | • | Which devices the rule applies to. any is the rule that does not care and is the default |
| enabled | boolean | • | Whether the rule is live |
| trigger | object | • | — |
| surveyId | string | • | The survey this rule delivers |
| urlMatch | object | • | Which pages match |
| createdAt | string | • | ISO-8601 creation timestamp |
| frequency | object | • | — |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| attributeConditions | array | • | Conditions on the visitor’s attributes, ALL of which must hold |
PATCH/support/rules/:id
Edit a delivery rule: its URL match, device, trigger, frequency, conditions, or whether it is live at all. Every value replaces the old one whole rather than merging into it. The survey is not editable — a rule is the targeting OF one survey, and retargeting it silently would change what every matching page shows; delete it and write the one you meant. Switching a rule off is the quiet way to stop delivering without touching the survey
MCP tool: support_rule_update
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Rule id |
| urlMatch | body | object | — | The new URL match, replacing the old one |
| trigger | body | object | — | The new trigger, replacing the old one |
| frequency | body | object | — | The new frequency, replacing the old one |
| device | body | string | — | The new device targeting |
| enabled | body | boolean | — | Turn the rule on or off |
| attributeConditions | body | array | — | The complete new condition set, replacing the old one. Pass [] to clear it |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Rule id |
| device | string | • | Which devices the rule applies to. any is the rule that does not care and is the default |
| enabled | boolean | • | Whether the rule is live |
| trigger | object | • | — |
| surveyId | string | • | The survey this rule delivers |
| urlMatch | object | • | Which pages match |
| createdAt | string | • | ISO-8601 creation timestamp |
| frequency | object | • | — |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| attributeConditions | array | • | Conditions on the visitor’s attributes, ALL of which must hold |
GET/support/statuses
List this product status dictionary in display order. A product that never opened settings gets the four defaults seeded on the first call. Archived statuses are hidden unless asked for
MCP tool: support_status_list
Parameters
| Name | In | Type | Required | Description |
|---|
| includeArchived | query | boolean | — | Also return archived statuses (default false) |
Response
| Field | Type | Always | Description |
|---|
| statuses | array | • | The product status dictionary, in display order |
POST/support/statuses
Add a status to this product status dictionary. The name is what people see; the category is what automation and reporting read, and it cannot change once threads sit on the status
MCP tool: support_status_create
Parameters
| Name | In | Type | Required | Description |
|---|
| name | body | string | • | Status name as the customer wants to see it. Unique within the product |
| color | body | string | • | Display colour as a six-digit hex string, e.g. #2563eb |
| order | body | integer | • | Sort order within the status dictionary, lowest first |
| category | body | string | • | Lifecycle bucket the status belongs to. Automation and reporting read the category, never the name — so it cannot be changed once threads sit on the status |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Status id |
| name | string | • | Status name, unique within the product |
| color | string | • | Display colour as a hex string |
| order | integer | • | Sort order within the status dictionary |
| archived | boolean | • | Archived statuses stay readable but are not offered as targets |
| category | string | • | Lifecycle bucket the status belongs to |
PATCH/support/statuses/:id
Rename, recolour or reorder a status. The category may only change while no thread references the status: changing it later would rewrite the history of every metric derived from it. A status belonging to another product is reported as not found
MCP tool: support_status_update
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Status id |
| name | body | string | — | Status name as the customer wants to see it. Unique within the product |
| color | body | string | — | Display colour as a six-digit hex string, e.g. #2563eb |
| order | body | integer | — | Sort order within the status dictionary, lowest first |
| category | body | string | — | New lifecycle bucket. Refused while any thread references the status |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Status id |
| name | string | • | Status name, unique within the product |
| color | string | • | Display colour as a hex string |
| order | integer | • | Sort order within the status dictionary |
| archived | boolean | • | Archived statuses stay readable but are not offered as targets |
| category | string | • | Lifecycle bucket the status belongs to |
POST/support/statuses/:id/archive
Retire a status. There is no way to delete one: every thread on it moves to the target status in the same transaction, so no thread is ever left on an archived status. The target must be a different, live status of this product
MCP tool: support_status_archive
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Status id |
| targetStatusId | body | string | • | Live status every thread on the archived status moves to, in the same transaction |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Status id |
| name | string | • | Status name, unique within the product |
| color | string | • | Display colour as a hex string |
| order | integer | • | Sort order within the status dictionary |
| archived | boolean | • | Archived statuses stay readable but are not offered as targets |
| category | string | • | Lifecycle bucket the status belongs to |
GET/support/surveys
List this product’s surveys, newest first, optionally narrowed to one lifecycle state (draft, published or archived), with pagination
MCP tool: support_survey_list
Parameters
| Name | In | Type | Required | Description |
|---|
| status | query | string | — | Only surveys in this lifecycle state |
| limit | query | integer | — | Page size, 1..100 (default 25) |
| offset | query | integer | — | Number of surveys to skip |
Response
| Field | Type | Always | Description |
|---|
| limit | integer | • | Page size that was applied |
| offset | integer | • | Offset that was applied |
| surveys | array | • | The requested page of surveys, newest first |
POST/support/surveys
Open a new survey. It starts as a draft — editable and not yet answerable — whatever questions travel with it. There is no way to create one already published: publishing runs the structure validator, and a create that skipped it would be a second way in
MCP tool: support_survey_create
Parameters
| Name | In | Type | Required | Description |
|---|
| name | body | string | • | What this survey is called. It becomes the title of an escalated feedback thread |
| questions | body | array | — | The initial question set. Omit it to open an empty draft and fill it in later |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Survey id |
| name | string | • | What this survey is called |
| status | string | • | draft: editable, not answerable. published: answerable, frozen. archived: terminal, neither editable nor answerable, and there is no way back |
| createdAt | string | • | ISO-8601 creation timestamp |
| questions | array | • | The ordered question set, exactly as stored |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
GET/support/surveys/:id
Get one survey by id, with its whole question set. A survey belonging to another product is reported as not found. Through the public edge only a PUBLISHED survey is readable — a draft or an archived one is reported as not found there
MCP tool: support_survey_get
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Survey id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Survey id |
| name | string | • | What this survey is called |
| status | string | • | draft: editable, not answerable. published: answerable, frozen. archived: terminal, neither editable nor answerable, and there is no way back |
| createdAt | string | • | ISO-8601 creation timestamp |
| questions | array | • | The ordered question set, exactly as stored |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
PATCH/support/surveys/:id
Edit a survey’s name or question set. Only a draft is editable — publishing freezes the structure, because answers are keyed by question id and rewriting the questions under them would change what every collected response was answering. To edit a live survey, unpublish it first. questions replaces the whole set; pass an empty array to clear it
MCP tool: support_survey_update
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Survey id |
| name | body | string | — | New name |
| questions | body | array | — | The complete new question set, replacing the old one. Pass [] to empty the draft |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Survey id |
| name | string | • | What this survey is called |
| status | string | • | draft: editable, not answerable. published: answerable, frozen. archived: terminal, neither editable nor answerable, and there is no way back |
| createdAt | string | • | ISO-8601 creation timestamp |
| questions | array | • | The ordered question set, exactly as stored |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
POST/support/surveys/:id/archive
Switch a survey off for good, from either draft or published. Archiving is terminal — there is no way back in v2 — and it deletes nothing: every response already collected stays readable
MCP tool: support_survey_archive
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Survey id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Survey id |
| name | string | • | What this survey is called |
| status | string | • | draft: editable, not answerable. published: answerable, frozen. archived: terminal, neither editable nor answerable, and there is no way back |
| createdAt | string | • | ISO-8601 creation timestamp |
| questions | array | • | The ordered question set, exactly as stored |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
POST/support/surveys/:id/publish
Publish a draft survey, which is what makes it answerable and what freezes its structure. Publishing validates the whole question set: it must be non-empty, question ids must be unique, radio and checkbox questions must offer options, a statement may not be required, and every skip must target a later question or the literal "end". A survey that is already published or archived is refused
MCP tool: support_survey_publish
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Survey id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Survey id |
| name | string | • | What this survey is called |
| status | string | • | draft: editable, not answerable. published: answerable, frozen. archived: terminal, neither editable nor answerable, and there is no way back |
| createdAt | string | • | ISO-8601 creation timestamp |
| questions | array | • | The ordered question set, exactly as stored |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
GET/support/surveys/:id/stats
Aggregate one survey’s responses over a time window: how many came in and how many reached the end, then per question — option counts for radio and checkbox, mean and distribution for rating and nps, an answered count for the rest. For the FIRST nps question it also returns promoters (9-10), passives (7-8), detractors (0-6) and the NPS score: percentage of promoters minus percentage of detractors over everyone who answered, rounded once. The window defaults to the last 30 days — omitting from does not mean "since the beginning"
MCP tool: support_survey_stats
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Survey id |
| from | query | string | — | Start of the window, ISO-8601, inclusive. Defaults to 30 days ago — omitting it means "recently", never "since the beginning" |
| to | query | string | — | End of the window, ISO-8601, inclusive. Defaults to now |
Response
| Field | Type | Always | Description |
|---|
| to | string | • | ISO-8601 end of the window that was actually applied |
| nps | object | • | The NPS block, or null when the survey asks no nps question |
| from | string | • | ISO-8601 start of the window that was actually applied |
| surveyId | string | • | The survey these numbers describe |
| questions | array | • | Every question of the survey, answered or not |
| responses | object | • | The response counts over the window |
POST/support/surveys/:id/unpublish
Return a published survey to draft so it can be edited. It stops accepting responses immediately; the responses already collected are untouched. A survey that is not published is refused
MCP tool: support_survey_unpublish
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Survey id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Survey id |
| name | string | • | What this survey is called |
| status | string | • | draft: editable, not answerable. published: answerable, frozen. archived: terminal, neither editable nor answerable, and there is no way back |
| createdAt | string | • | ISO-8601 creation timestamp |
| questions | array | • | The ordered question set, exactly as stored |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
GET/support/threads
List support threads for this product, newest first — by creation time by default, or by last modification with sort='updated'. Every filter lives here: type, status, status category, assignee, external author, creation window, rating range (ratingMin/ratingMax, inclusive — a thread nobody rated matches neither bound), plus pagination
MCP tool: support_thread_list
Parameters
| Name | In | Type | Required | Description |
|---|
| type | query | string | — | Only threads of this type |
| statusId | query | string | — | Only threads sitting on this exact status |
| category | query | string | — | Only threads whose status belongs to this category. Resolved through a join on the status dictionary — the category is never stored on the thread |
| assignedAgentId | query | string | — | Only threads assigned to this agent |
| authorExternalId | query | string | — | Only threads opened by this external author, matched verbatim |
| createdFrom | query | string | — | Only threads created at or after this ISO-8601 timestamp |
| createdTo | query | string | — | Only threads created at or before this ISO-8601 timestamp |
| ratingMin | query | integer | — | Only threads rated at or above this score (0..10, inclusive). An unrated thread never matches a rating filter — not even at the bottom of the scale |
| ratingMax | query | integer | — | Only threads rated at or below this score (0..10, inclusive). An unrated thread never matches a rating filter — not even at the bottom of the scale |
| labelIds | query | string | — | Only threads carrying ALL of these labels. Every other filter here narrows the result and so does this one: each additional label id makes the answer smaller, never bigger |
| sort | query | string | — | Ordering of the page, newest first either way (default 'created') |
| limit | query | integer | — | Page size, 1..100 (default 25) |
| offset | query | integer | — | Number of threads to skip |
Response
| Field | Type | Always | Description |
|---|
| limit | integer | • | Page size that was applied |
| offset | integer | • | Offset that was applied |
| threads | array | • | The requested page of threads, newest first by the requested sort |
POST/support/threads
Open a support thread on behalf of an external end user. The author is an opaque identifier from the client's own user base and is stored verbatim. The thread opens on the product's default open status; there is no status argument. An optional rating — an integer 0..10, both ends inclusive — may travel with it and is stored verbatim too; omit it for an unrated thread, because 0 is a real score at the bottom of the scale
MCP tool: support_thread_create
Parameters
| Name | In | Type | Required | Description |
|---|
| type | body | string | • | ticket: the end user wants something fixed. feedback: unsolicited input |
| title | body | string | • | Short human-readable subject of the thread |
| authorExternalId | body | string | • | Opaque identifier of the external end user in the client's own user base. Stored verbatim: never resolved, normalised or enriched, and it is not a Lessly identity |
| authorDisplay | body | string | — | Optional display name for the external author, as the client already knows it |
| metadata | body | object | — | Free-form client-owned JSON attached to the thread |
| labelIds | body | array | — | Labels to put on the new thread |
| rating | body | integer | — | Optional score the client attached when the thread was opened: an integer 0..10, both ends inclusive, stored verbatim and never recomputed. Omit it for an unrated thread — 0 is a real score at the bottom of the scale, not a way of saying "no rating" |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Thread id |
| type | string | • | ticket: the end user wants something fixed. feedback: unsolicited input |
| title | string | • | Thread subject |
| labels | array | • | Every label on the thread, by name |
| rating | integer | • | The score the client supplied at creation, or null while the thread is unrated |
| status | object | • | The status itself, joined in at read time |
| metadata | object | • | Free-form client-owned JSON |
| priority | string | • | Thread priority |
| statusId | string | • | Id of the status the thread sits on |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| lastReplyAt | string | • | ISO-8601 timestamp of the last reply, or null while there are none |
| authorDisplay | string | • | Display name for the external author, if given |
| assignedAgentId | string | • | Assigned agent, or null while unassigned |
| authorExternalId | string | • | The external author, byte-identical to what was stored |
GET/support/threads/:id
Get one support thread by id, with its status joined in. A thread belonging to another product is reported as not found
MCP tool: support_thread_get
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Thread id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Thread id |
| type | string | • | ticket: the end user wants something fixed. feedback: unsolicited input |
| title | string | • | Thread subject |
| labels | array | • | Every label on the thread, by name |
| rating | integer | • | The score the client supplied at creation, or null while the thread is unrated |
| status | object | • | The status itself, joined in at read time |
| metadata | object | • | Free-form client-owned JSON |
| priority | string | • | Thread priority |
| statusId | string | • | Id of the status the thread sits on |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| lastReplyAt | string | • | ISO-8601 timestamp of the last reply, or null while there are none |
| authorDisplay | string | • | Display name for the external author, if given |
| assignedAgentId | string | • | Assigned agent, or null while unassigned |
| authorExternalId | string | • | The external author, byte-identical to what was stored |
PATCH/support/threads/:id
Edit a thread: its title, its priority, its labels and its client-owned metadata. Moving a thread between statuses and putting an agent on it have their own tools and are not possible here. labelIds replaces the entire label set — pass an empty array to clear it, and a label belonging to another product is reported as not found
MCP tool: support_thread_update
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Thread id |
| title | body | string | — | New subject of the thread |
| priority | body | string | — | New priority |
| labelIds | body | array | — | The complete new label set. Pass an empty array to remove every label |
| metadata | body | object | — | Replaces the whole client-owned metadata object |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Thread id |
| type | string | • | ticket: the end user wants something fixed. feedback: unsolicited input |
| title | string | • | Thread subject |
| labels | array | • | Every label on the thread, by name |
| rating | integer | • | The score the client supplied at creation, or null while the thread is unrated |
| status | object | • | The status itself, joined in at read time |
| metadata | object | • | Free-form client-owned JSON |
| priority | string | • | Thread priority |
| statusId | string | • | Id of the status the thread sits on |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| lastReplyAt | string | • | ISO-8601 timestamp of the last reply, or null while there are none |
| authorDisplay | string | • | Display name for the external author, if given |
| assignedAgentId | string | • | Assigned agent, or null while unassigned |
| authorExternalId | string | • | The external author, byte-identical to what was stored |
POST/support/threads/:id/assignment
Put a support agent on a thread, or pass null to return it to the unassigned queue — clearing an assignment is an ordinary operation, not an error. An agent belonging to another product is reported as not found
MCP tool: support_thread_assign
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Thread id |
| agentId | body | string | • | Support agent of this product to put on the thread, or null to return it to the unassigned queue. An agent of another product is reported as not found |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Thread id |
| type | string | • | ticket: the end user wants something fixed. feedback: unsolicited input |
| title | string | • | Thread subject |
| labels | array | • | Every label on the thread, by name |
| rating | integer | • | The score the client supplied at creation, or null while the thread is unrated |
| status | object | • | The status itself, joined in at read time |
| metadata | object | • | Free-form client-owned JSON |
| priority | string | • | Thread priority |
| statusId | string | • | Id of the status the thread sits on |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| lastReplyAt | string | • | ISO-8601 timestamp of the last reply, or null while there are none |
| authorDisplay | string | • | Display name for the external author, if given |
| assignedAgentId | string | • | Assigned agent, or null while unassigned |
| authorExternalId | string | • | The external author, byte-identical to what was stored |
POST/support/threads/:id/status
Move a thread to a status. Any status may follow any other — there are no transition rules. The category is never a parameter: it comes from the status the thread lands on. A status belonging to another product is reported as not found, and an archived status is refused
MCP tool: support_thread_status_set
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Thread id |
| statusId | body | string | • | Destination status of this product. Any status may follow any other — v1 has no transition rules. An archived status is refused |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Thread id |
| type | string | • | ticket: the end user wants something fixed. feedback: unsolicited input |
| title | string | • | Thread subject |
| labels | array | • | Every label on the thread, by name |
| rating | integer | • | The score the client supplied at creation, or null while the thread is unrated |
| status | object | • | The status itself, joined in at read time |
| metadata | object | • | Free-form client-owned JSON |
| priority | string | • | Thread priority |
| statusId | string | • | Id of the status the thread sits on |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-modification timestamp |
| lastReplyAt | string | • | ISO-8601 timestamp of the last reply, or null while there are none |
| authorDisplay | string | • | Display name for the external author, if given |
| assignedAgentId | string | • | Assigned agent, or null while unassigned |
| authorExternalId | string | • | The external author, byte-identical to what was stored |
GET/support/webhook-deliveries
List webhook delivery attempts for this product, newest first. Filter by endpoint, by state (pending, delivered, failed), by event type or by the time the delivery was opened. Each row carries the exact JSON body that was sent, the last HTTP status and, while attempts remain, when the next one is due
MCP tool: support_webhook_deliveries_list
Parameters
| Name | In | Type | Required | Description |
|---|
| webhookId | query | string | — | Only deliveries for this endpoint |
| state | query | string | — | Only deliveries in this state: pending, delivered or failed |
| eventType | query | string | — | Only deliveries of this event type |
| createdFrom | query | string | — | Only deliveries opened at or after this ISO-8601 time |
| createdTo | query | string | — | Only deliveries opened at or before this ISO-8601 time |
| limit | query | integer | — | Page size, 1..200 (default 50) |
| offset | query | integer | — | Number of deliveries to skip |
Response
| Field | Type | Always | Description |
|---|
| limit | integer | • | Page size that was applied |
| offset | integer | • | Offset that was applied |
| deliveries | array | • | The requested page, newest first |
POST/support/webhook-deliveries/redeliver
Send a past delivery to its endpoint again, with the original payload and the original idempotencyKey — a receiver that already processed it can recognise and ignore the repeat. The original delivery row is kept as history and a new one is opened for this attempt. If the endpoint has since been disabled or unsubscribed from the event, nothing is delivered
MCP tool: support_webhook_redeliver
Parameters
| Name | In | Type | Required | Description |
|---|
| id | body | string | • | Id of the delivery to send again |
Response
| Field | Type | Always | Description |
|---|
| delivery | object | • | The new delivery, opened in pending state. It carries the original idempotencyKey, so a receiver that already processed the first one can ignore it |
GET/support/webhooks
List the endpoints this product has registered, oldest first. Signing secrets are never included — only the short prefix that tells two endpoints apart
MCP tool: support_webhook_list
Parameters
No parameters.
Response
| Field | Type | Always | Description |
|---|
| webhooks | array | • | The endpoints this product has registered, oldest first |
POST/support/webhooks
Register an HTTPS endpoint to receive Support events for this product, and mint its signing secret. The secret is returned here in clear and NOWHERE ELSE — exactly once, so store it now: it cannot be read back and can only be replaced by rotating it. Plain http:// endpoints are rejected
MCP tool: support_webhook_create
Parameters
| Name | In | Type | Required | Description |
|---|
| url | body | string | • | HTTPS endpoint the event is POSTed to. Plain http:// is rejected |
| eventTypes | body | array | • | Event types this endpoint subscribes to. One or more of support.message.created, support.thread.status-changed, support.thread.assigned, support.survey.published, support.response.created |
| description | body | string | — | Free-form note about what this endpoint is for, up to 1000 characters |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Webhook id |
| url | string | • | HTTPS endpoint events are POSTed to |
| secret | string | • | The signing secret in clear. Shown exactly once, here — store it now; it cannot be read back and can only be replaced by rotating it |
| status | string | • | active or disabled |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-update timestamp |
| eventTypes | array | • | Event types this endpoint subscribes to |
| description | string | • | Free-form note, or null |
| secretPrefix | string | • | First characters of the signing secret, to tell endpoints apart. Never the whole secret |
DELETE/support/webhooks/:id
Remove a registered endpoint and the delivery history that points at it. To stop delivery without losing the registration, set its status to disabled instead
MCP tool: support_webhook_delete
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Webhook id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Id of the endpoint that was removed |
| deleted | boolean | • | Always true — the endpoint is gone |
GET/support/webhooks/:id
Get one registered endpoint by id. The signing secret is not part of the answer. An endpoint belonging to another product is reported as not found
MCP tool: support_webhook_get
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Webhook id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Webhook id |
| url | string | • | HTTPS endpoint events are POSTed to |
| status | string | • | active or disabled |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-update timestamp |
| eventTypes | array | • | Event types this endpoint subscribes to |
| description | string | • | Free-form note, or null |
| secretPrefix | string | • | First characters of the signing secret, to tell endpoints apart. Never the whole secret |
PATCH/support/webhooks/:id
Edit a registered endpoint: its url, the event types it subscribes to, its description or its status. Setting status to disabled keeps the registration and stops delivery. Omit a field to leave it alone; pass null to clear the description. The signing secret is not editable here — rotate it instead
MCP tool: support_webhook_update
Parameters
| Name | In | Type | Required | Description |
|---|
| url | body | string | — | HTTPS endpoint the event is POSTed to. Plain http:// is rejected |
| eventTypes | body | array | — | Event types this endpoint subscribes to. One or more of support.message.created, support.thread.status-changed, support.thread.assigned, support.survey.published, support.response.created |
| description | body | string | — | Free-form note about what this endpoint is for, up to 1000 characters |
| status | body | string | — | active delivers events; disabled keeps the registration and stops delivery |
| id | path | string | • | Webhook id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Webhook id |
| url | string | • | HTTPS endpoint events are POSTed to |
| status | string | • | active or disabled |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-update timestamp |
| eventTypes | array | • | Event types this endpoint subscribes to |
| description | string | • | Free-form note, or null |
| secretPrefix | string | • | First characters of the signing secret, to tell endpoints apart. Never the whole secret |
POST/support/webhooks/:id/rotate-secret
Replace the signing secret of an endpoint and return the new one in clear, exactly once. The old secret stops verifying immediately — there is no grace window, so update the receiver before rotating
MCP tool: support_webhook_rotate_secret
Parameters
| Name | In | Type | Required | Description |
|---|
| id | path | string | • | Webhook id |
Response
| Field | Type | Always | Description |
|---|
| id | string | • | Webhook id |
| url | string | • | HTTPS endpoint events are POSTed to |
| secret | string | • | The signing secret in clear. Shown exactly once, here — store it now; it cannot be read back and can only be replaced by rotating it |
| status | string | • | active or disabled |
| createdAt | string | • | ISO-8601 creation timestamp |
| updatedAt | string | • | ISO-8601 last-update timestamp |
| eventTypes | array | • | Event types this endpoint subscribes to |
| description | string | • | Free-form note, or null |
| secretPrefix | string | • | First characters of the signing secret, to tell endpoints apart. Never the whole secret |
Was this page helpful?Thanks for your feedback!