FR
Copied
API

Feedback API

In-app chat with the platform admin and entry point for on-demand module activation requests.

Feedback API

The Feedback API powers the in-app chat between an authenticated user and the platform admin. It also doubles as the entry point for on-demand module activation requests: clicking "Request" on a stub module (email, SMS, WhatsApp, phone carrier) opens a feedback thread with a dedicated topic, which surfaces in the admin dashboard's "On demand" inbox.

A thread is a stable conversation pinned to a topic. Every reply is a feedback_message row scoped to that thread. Read state is tracked per role (user, admin) so each side sees only its own unread badge.

All endpoints require an authenticated caller. Generic errors: 401 (not authenticated), 404 (thread does not exist). Endpoint-specific causes are listed inline.

Topic conventions

The topic field on a thread is a free-form string capped at 64 chars, but the product follows a small set of conventions:

Topic value Meaning
general Default. Catch-all chat.
feedback Generic product feedback.
bug Bug report.
feature Feature request.
on_demand_email Activation request for the email-campaign stub.
on_demand_sms Activation request for the SMS-campaign stub.
on_demand_whatsapp Activation request for the WhatsApp stub.
on_demand_phone_carrier Activation request for the phone-carrier stub.

Any topic matching on_demand_* is picked up by the admin endpoint GET /api/admin/feedback/on-demand, which groups threads by topic and exposes open counts. The on-demand stubs are listed in the module registry under on_demand; a client can read the registry and build topic = "on_demand_" + slug.

The shorter type field (bug, feature, other) is independent of topic and only carries the coarse intent for sorting.


POST /api/feedback/threads

Create a new thread together with its first message. Rate limit: 20 threads per user per hour.

Field Type Notes
type string bug, feature, or other. Defaults to other.
message string 3 to 5000 chars. The first message body.
topic string Optional. Defaults to general. Max 64 chars.

Request

POST /api/feedback/threads
{
  "type": "feature",
  "topic": "on_demand_whatsapp",
  "message": "Sending WhatsApp follow-ups to scraped leads would be useful."
}

Response — 201 Created

{
  "id": 142,
  "user_id": 7,
  "user_email": "user@example.com",
  "type": "feature",
  "status": "open",
  "created_at": "2026-05-27 10:11:12",
  "last_read_user": "2026-05-27 10:11:12",
  "last_read_admin": null,
  "messages": [
    {
      "id": 991,
      "author_role": "user",
      "author_user_id": 7,
      "message": "Sending WhatsApp follow-ups to scraped leads would be useful.",
      "created_at": "2026-05-27 10:11:12"
    }
  ],
  "preview": "Sending WhatsApp follow-ups to scraped leads would be useful.",
  "last_message_at": "2026-05-27 10:11:12",
  "unread_for_me": 0
}

Specific causes: 400 type not in {bug, feature, other}; 422 message shorter than 3 or longer than 5000; 429 more than 20 threads in the last hour.


POST /api/feedback/threads/{thread_id}/messages

Append a reply to an existing thread. The caller must own the thread, and the thread must not be closed. Posting a message also marks the thread as read for the user side.

Request

POST /api/feedback/threads/142/messages
{
  "message": "Adding more context: opt-out tracking would also be required."
}

Response — 201 Created

Returns the full serialized thread, identical in shape to the POST /threads response, with the appended message included.

Specific causes: 400 thread is closed; 403 caller does not own the thread; 422 message empty or longer than 5000.


GET /api/feedback/threads

List the caller's threads, newest first. Capped at 100 rows. Each entry embeds the full message list so the client can render previews and unread counts without a second round trip.

Response — 200 OK

[
  {
    "id": 142,
    "user_id": 7,
    "user_email": "user@example.com",
    "type": "feature",
    "status": "open",
    "created_at": "2026-05-27 10:11:12",
    "last_read_user": "2026-05-27 10:11:12",
    "last_read_admin": null,
    "messages": [ /* ... */ ],
    "preview": "Sending WhatsApp follow-ups...",
    "last_message_at": "2026-05-27 10:11:12",
    "unread_for_me": 0
  }
]

The unread_for_me counter reflects admin replies not yet seen, computed from last_read_user. The companion endpoint GET /api/feedback/unread returns the same number aggregated across every thread, ready to bind to a header badge.


When the admin replies

Posting an admin message (POST /api/admin/feedback/threads/{id}/messages) does three things:

  1. appends the message to the thread and reopens it if it was closed;
  2. bumps the thread's unread_for_me for the user, which drives the in-app badge;
  3. sends the user a notification e-mail containing the reply, a reminder of their original message, and a button to /settings?feedback={thread_id} — a deep link that opens Settings › Support inbox on that exact conversation. That page is the only place a conversation is read or answered; the in-app « Report a bug » bubble only opens new ones. Delivery is best-effort: the reply is always stored, a mail failure is never propagated.

Timestamps are stored to the millisecond. Second precision was not enough: a reply landing in the same second as the reader's last_read_* marker failed the strict created_at > last_read comparison and stayed invisible permanently, since neither value ever changes afterwards.

Admin scope

GET /api/admin/feedback/threads and GET /api/admin/feedback/unread both exclude on-demand activation requests (topic LIKE 'on_demand_*') unless include_on_demand=true is passed. Those requests have their own admin surface (GET /api/admin/feedback/on-demand), so the reports inbox and its unread badge cover exactly the same set of threads. Threads are ordered by last activity, not creation date, so a month-old thread the user just replied to comes back to the top.