Threads and messages
Create conversations, control message visibility, and collect customer feedback.
A thread holds one customer conversation. Messages record its replies and internal notes; statuses, labels, and assignments help your team manage it. This page explains how to create and read those records, including feedback and ratings.
The examples follow Alex Fern using your shop. Replace the customer values, product id, public key, URLs, and abbreviated record ids with your own.
Opening a thread
support_thread_create opens a conversation for an external customer:
{
"type": "ticket",
"title": "Cannot upload my avatar",
"authorExternalId": "user_8842",
"authorDisplay": "Alex Fern",
"metadata": { "plan": "team", "appVersion": "4.19.2" },
"labelIds": []
}type is either ticket, for a problem the customer wants fixed, or feedback, for unsolicited input. It lets you filter the queue but triggers no automation. It cannot change after creation.
title is required for both types and must contain 1–500 characters. It appears in the queue and in thread notifications.
metadata stores your JSON object, such as a plan, app version, device, or order id. Support returns it without interpreting its keys. Feedback widgets can use the four conventional keys.
rating is an optional integer from 0 to 10, returned as null when unset. It is a thread field you can filter on. See The rating field.
labelIds accepts up to 50 labels from the same product. An id from another product returns not found and prevents the thread from being created.
A new thread uses the non-archived open status with the lowest display order and starts at normal priority. Creation accepts neither a status nor a priority override. Change priority to low, high, or urgent afterward with a thread update.
The external author
authorExternalId is your existing identifier for the customer, such as a row id or customer number. Support stores it verbatim without resolving, normalizing, or enriching it. It provides no end-user directory or sign-in.
authorDisplay is an optional name, also stored verbatim. It appears at the top of the thread and after “From” in new-thread notifications. Support has no contact-address fields and does not contact customers; your product handles customer notifications.
Open customer threads through your backend, using the identity it has authenticated. Support has no unauthenticated thread-creation route. The authorExternalId queue filter matches the supplied value exactly.
Editing a thread
support_thread_update covers the thread’s own fields — title, priority, labelIds and metadata — and at least one of them must be present.
labelIds and metadata replace their previous values in full. To retain existing labels or metadata keys, include them in the replacement. Send labelIds: [] to clear all labels.
Use support_thread_status_set for status changes and support_thread_assign for assignment. These reject archived destinations and deactivated assignees, respectively.
Reading threads back
support_thread_get returns one thread with its status joined in. A thread that belongs to another product is reported as not found, never as forbidden.
support_thread_list filters the queue by type, statusId, category, assignedAgentId, authorExternalId, createdFrom / createdTo, ratingMin / ratingMax, and labelIds. Filters combine with AND. A thread must carry every requested label to match.
Results use offset pagination, with 25 threads by default and at most 100. sort: "created" returns newest-created threads first; sort: "updated" returns most recently modified threads first.
Each thread includes its status object, named labels, assignedAgentId, rating, metadata, and lastReplyAt. The assignee and rating are null when unset. lastReplyAt is the creation time of the newest undeleted public message, inbound or outbound, or null if none exists. Internal notes do not change it.
Messages
support_message_create posts one entry:
{
"threadId": "b50f26b4…",
"direction": "outbound",
"authorAgentId": "13c893aa…",
"body": "Thanks for the report. Which browser are you on?",
"visibility": "public"
}Direction, visibility, and author are independent fields:
Direction: inbound records input from the customer; outbound records a support reply.
Visibility: public is content your product may show to the customer. internal is a note for your support team. The default is public, so set internal explicitly for notes. Your product must apply this distinction when rendering a conversation.
Author: set exactly one of authorExternalId and authorAgentId. The former is your customer identifier; the latter must identify a Support agent in this product. An agent from another product returns not found. Input validation and a database constraint enforce the one-author rule.
For example, an inbound internal note can record “The customer called and said X”. Direction does not force an author type or visibility; Support accepts each combination.
body, when supplied at creation, must contain 1–50,000 characters and is stored without trimming or normalization. Omit it for an attachment-only message with at least one uploaded attachment. Sending an empty string is rejected. Editing an existing message has a different empty-body rule.
Posting a public message stamps the thread’s lastReplyAt, and a public inbound message can also move the thread’s status on its own — see Statuses. attachmentIds names files the message carries; see Labels and attachments.
Support separately records the Lepsto identity that created the message. That identity determines who may edit or delete it. The supplied author fields do not establish ownership. See Correct or remove a message.
Reading a conversation
support_message_list returns messages oldest first.
Pass threadId for one conversation. Use visibility: "public" when preparing a customer-facing view; omit it for a team view that includes internal notes. Add direction to narrow the results further.
support_message_get returns one message in the same shape as a list entry. It can read internal notes too; the list visibility filter does not restrict a direct read.
Each message includes direction, visibility, creation time, both author fields (one set, one null), and confirmed attachments with freshly signed download URLs. Unconfirmed uploads are excluded.
editedAt and deletedAt start as null. A deleted message remains in the list with body: "" and attachments: []. Show a “Message removed” placeholder to preserve its position; mark messages with editedAt as edited. The get and list tools share this record shape. Message webhooks report changes by id; they do not contain the body. See Correct or remove a message.
Catching up across the whole product
Omit threadId to read messages across the product. Use this after an integration outage to catch up on newly created messages.
Set since to read messages created strictly after a timestamp, then page through the result with nextCursor:
{ "since": "2026-08-19T17:11:32.344Z", "limit": 100 }Send each returned nextCursor as the next request’s cursor until it is null. The cursor includes timestamp and message position, so pagination distinguishes messages created at the same instant. Treat it as opaque; never construct it yourself. A since timestamp alone excludes all messages at that exact time, so use the returned cursor when continuing a partial page.
offset remains available, but new rows can shift later pages and cause skips. Prefer cursors for catch-up reads. Sending cursor and offset together is rejected.
The product boundary and visibility filters are the same for thread-scoped and product-wide reads.
Feedback threads
A feedback thread uses the same record and routes as a ticket. Use metadata to record the context of the feedback and the separate rating field for a score you need to filter.
Support stores metadata without interpreting it. It validates rating and supports rating bounds in queue filters.
Even a score-only widget must supply a title, such as “Rated the checkout step 8/10”. Feedback starts open, at normal priority, and unassigned. Your team can label, assign, and answer it like a ticket.
The four conventional keys
Key | What it holds |
|---|---|
pageUrl | The address the end user was on when they wrote. The whole URL, as the browser had it. |
elementRef | Your own stable name for the thing they were rating — a screen, a step, a control. Not a CSS selector or a DOM path, both of which change when the markup does. |
device | Whatever you already know about the client: model, operating system, browser. One human-readable string. |
locale | The language the end user was reading in, as a BCP 47 tag — en-GB, pt-BR. |
These keys are optional conventions. Support allows omitted keys and additional keys, and does not validate URLs or language tags, normalize, trim, or lowercase values. For example, pageUrl: "not a url" is stored unchanged.
Use consistent keys in the code that writes feedback and in dashboards, exports, or queries that read it. Support does not enforce that agreement.
The queue cannot filter or search metadata keys such as pageUrl or locale; there is no metadata query parameter or index. Use supported thread fields, including rating, for server-side filtering.
The rating field
rating is an optional integer from 0 to 10 inclusive. It is null when unset, and null is what a thread that was never scored reads back as.
It is accepted on thread create on both planes — support_thread_create on the authenticated side, and public POST /threads — and it reads back on every route that returns a thread.
Collect the score before opening the thread. rating is set only at creation: support_thread_update has no rating field, and no other operation changes it.
Send an integer from 0 to 10. Values such as 11, -1, 7.5, and "8" return a validation 400 with field errors and no top-level message. Numeric strings are not converted. See Public API errors.
0 is a valid score. Omit rating to leave it null; do not use a truthiness check to distinguish a missing score from zero.
Your product defines the scale. For example, you can map five emoji choices to 0, 2, 5, 8, and 10, or collect an eleven-point NPS answer. Support stores the number without the scale and does not compute thread-rating averages, classify promoters, or change priority or status from it. Keep the mapping stable so old and new ratings remain comparable.
Filtering by rating
The thread list takes two bounds, on both planes:
Parameter | Rules |
|---|---|
ratingMin | Integer 0–10. Only threads whose rating is at or above it. |
ratingMax | Integer 0–10. Only threads whose rating is at or below it. |
Bounds are inclusive and can be used separately or together. ratingMin=0&ratingMax=6 selects scores from zero through six. Combine them with type, assignee, and creation-window filters to narrow the queue.
Unrated threads match neither bound. ratingMin=0 selects all scored threads and excludes null. There is no unrated-only filter; compare the queue without rating bounds with its scored records when you need an unrated count.
A frontend files an emoji rating
In this example, Alex rates your shop’s checkout. The backend creates a feedback thread with the score in rating and page context in metadata.
The browser sends the rating to your backend:
await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
score: 8,
step: 'checkout.payment.submit',
pageUrl: window.location.href,
locale: navigator.language,
}),
})Your backend holds the product public key and calls Support:
curl -X POST "https://public.lepsto.com/{productId}/support/threads" \
-H "Authorization: Bearer lpk_…" \
-H "Content-Type: application/json" \
-d '{
"type": "feedback",
"title": "Rated the checkout step 8/10",
"authorExternalId": "user_8842",
"authorDisplay": "Alex Fern",
"rating": 8,
"metadata": {
"pageUrl": "https://shop.example.com/checkout/payment",
"elementRef": "checkout.payment.submit",
"device": "iPhone 15 Pro / iOS 18.2 / Safari",
"locale": "en-GB"
}
}'Keep the key on your backend. Depending on its scope, it can open threads and read the product’s queue, including internal notes. Customer application origins are excluded by the public API’s CORS rules.
Derive authorExternalId from the session your backend authenticated. Support stores the supplied identifier without proving who it belongs to; accepting it directly from the browser would let a customer claim another identity.
The response preserves your metadata values. The new feedback thread appears in the queue, where your team can label it, assign it, and reply. Having a rating does not make it read-only.
Where to go next
- Correct or remove a message: editing a body, soft deletion, and the edit history.
- Statuses: the statuses a thread moves through.
- Agents: the agents who write outbound messages.
- Surveys: asking for feedback rather than waiting for it.
- Tools, SDK and errors: every tool named here, and how it is called.