Conversation endpoints: list, clear, and share
Conversations are addressable objects with list, get, clear, and share endpoints. Audit agent history in code, or build your own review view over it.
The thread in the sidebar is not UI state. It is a row keyed by conversation_id, owned by a user, with a title, a message list, a generation history, and (optionally) a share token. Closing the tab does not delete it. Refreshing the app reloads it from the same endpoints the sidebar uses. If you want an audit log, a review queue, or a transcript you can hand a client, you do not scrape the chat. You call the conversation API.
All of the routes below sit under /api/v1/agentic, behind the same auth as the rest of the agent: a Supabase JWT or a vsk_ API key, sent as Bearer. Every mutating call is scoped to user_id matching the caller. There is no public list, and there is no webhook that will push a transcript to you when a turn finishes. You poll.
Conversations are rows, not tabs
GET /api/v1/agentic/conversations?user_id= returns the caller's threads, newest updated_at first. Each item is small on purpose:
| Field | What it is |
|---|---|
conversation_id |
The handle you pass to every other call |
title |
Display name |
message_count |
How long the thread is |
created_at / updated_at |
Unix ms |
There is no cursor and no documented page size. Treat the list as "all of this user's threads," not as a feed you paginate. If you are building a review view, this is the index: store the ids you care about, then fetch bodies on demand.
GET /api/v1/agentic/conversation/:conversation_id?user_id= is the body. It 404s for a missing id or when the user_id you pass does not own the row. What comes back:
messagesis the plain role/content list, flattened from the model-facing format. Fine for a grep, bad for a review UI, because images and tool cards are gone.ui_messagesis the rich transcript the app actually renders: text, generated media, tool calls. Prefer this whenever a human will look at the result. On GET, pending items in the server-side dispatch log are reconciled against the media tables and folded into this array, so a clip that finished after the stream closed shows a URL in the GET response. That is why a review view should read this endpoint rather than trusting the last SSE payload you cached. The dispatch log itself is not a response field.mediaLibrary(capped at 40) lists attachments and generated files that belong to the thread, which is what find something I made is doing from the chat side.
GET /api/v1/agentic/conversation/:conversation_id/tasks is the other half of a live thread. Each time the agent dispatches a generation, a server-owned conversation_tasks row is written. The client can read those rows. It cannot write them. Completions that land after the stream has closed still appear here, which is the whole reason the table exists: ui_messages is a client-saved array and would otherwise lose a finish that arrived late. A review view that only reads ui_messages will look empty on a job that is still rendering. Merge tasks by created_at.
List, then get, then decide
A reviewer who does not want to live in the agent chat can still do the job:
TOKEN="vsk_…"
USER_ID="…"
# Index
curl -s "https://api.versely.studio/api/v1/agentic/conversations?user_id=$USER_ID" \
-H "Authorization: Bearer $TOKEN"
# One thread, rich messages
curl -s "https://api.versely.studio/api/v1/agentic/conversation/$CONVO_ID?user_id=$USER_ID" \
-H "Authorization: Bearer $TOKEN"
# In-flight and finished tasks for that thread
curl -s "https://api.versely.studio/api/v1/agentic/conversation/$CONVO_ID/tasks" \
-H "Authorization: Bearer $TOKEN"
From there the decisions are boring, which is the point. Approve the outputs in your own tool. Ask the agent for a remake in the same conversation_id so the history stays in one place. Or export the ui_messages media URLs into whatever you already use for client review. The share-a-generation flow is the per-asset version of this idea. The conversation endpoints are the per-thread version.
Do not build a second source of truth. The title, the messages, and the media are already on the row. Your view should be a reader.
Clear is a delete
POST /api/v1/agentic/conversation/clear takes conversation_id and user_id. It drops the cached copy and deletes the Postgres row for that owner. It does not tombstone. A subsequent GET 404s. The gallery of generations is a different table; clearing a conversation does not walk through user_*_urls and delete the files. If you needed the files gone too, that is a media delete, not a conversation clear.
Use clear when the thread itself is the thing you do not want around: a prompt you should not have pasted, a client brief that does not belong in a shared seat, a debug session full of bad model names. Do not use it as "hide from the sidebar but keep the audit trail." There is no hide. If you need an audit trail, do not clear; mint a share link or copy the GET payload into your own store first.
The sidebar in the app calls this same endpoint. There is no softer path in the product.
Share is a live, revocable transcript
POST /api/v1/agentic/conversation/share takes conversation_id and user_id. On first call it mints a UUID share_token, stores it on the row, and returns share_url. Calling share again on the same thread reuses that token; the URL does not rotate just because you clicked twice. Pass action: "revoke" to null the token. After revoke, the public page 404s (the CDN may hold a hit for up to a minute; misses are no-store).
The page is a read-only, noindex transcript of up to 200 user and assistant ui_messages, with text clipped per message and media URLs kept only if they are http(s). It is live: it reads the stored ui_messages column, so a GET after a new saved turn shows the new turn. That is the feature and the caution. If you share a thread and then keep generating into it, the recipient sees the new work once those messages are saved. For a frozen handoff, share, then stop writing to that conversation_id. Start a new thread for the next round.
Revoke when the review is over. The token is the only secret on the URL; anyone with the link can read whatever is currently on the row. Do not put secrets in the chat if you plan to share the chat. A lasting preference and a brand kit belong in saved context, not in a thread you are about to publish as a transcript.
This is also the right shape for the client review loop when the artefact is the conversation itself (prompts, rejected takes, the take you kept) rather than one file. Send the share URL. Revoke it when the round closes.
FAQ
Can I list another user's conversations with my key?
List and get are filtered by the user_id you pass. A GET on a conversation_id that does not belong to that user_id 404s the same way a missing id does. Mutating calls (clear, share) reject a body user_id that is not the authenticated caller. Share is the public surface, and it is opt-in per thread.
Does clearing a conversation cancel in-flight generations?
No. Clear deletes the conversation row and its cached copy. It does not walk user_*_urls, and it does not cancel work already sent to a provider. That work still finishes, and the media still lands in the library. conversation_tasks rows are not foreign-keyed to the conversation, so they are not cascaded away. If the work was a background task, cancel it on POST /api/v1/agentic/tasks/:taskId/cancel. Then clear, if you also want the thread gone.
Why is the share page missing messages I can see in the app?
The public renderer keeps user and assistant ui_messages only, caps the list at 200, and drops entries with no text and no media. Tool-only cards, system notes, and anything that never made it into stored ui_messages will not show. The share page reads that column as saved, not the GET-time enrichment. If a generation finished after the last client save, reload the thread in the app so the client writes the rich messages, then treat the share URL as complete; the token is already minted.
Is there a webhook when a conversation updates?
No. Versely does not take a customer webhook_url on these routes. The status of a generation is polled; the status of a thread is GET. If you need a review queue, poll GET /conversations for updated_at and fetch the ones that moved. The developer surface is MCP, CLI, and skills on top of the same API, not a push bus.