Conversations and messages

Start chats, send follow-ups, stream replies over a WebSocket, compare models, approve MCP tool calls and share conversations through the REST API.

20 min read · Updated 28 September 2026

A conversation is a chat thread: the same thing your team sees under Chat History in the app. You start one with a message, the model answers, and you keep sending follow-ups. Every request here runs as the user who owns the API key, with the same access, quotas and workspace rules as that user in the browser.

All paths are relative to https://api.stickyprompts.com/api/v1. Request bodies are JSON. Files are uploaded through the file endpoints first and then referenced by ID.

How chat works

  1. Start with POST /conversation (waits for the full reply) or POST /conversation/stream (returns at once; the reply arrives over the WebSocket). Both create the conversation and send the first message in one call.
  2. Follow up with POST /conversation/:id or POST /conversation/:id/stream.
  3. Change the model, style or reasoning effort for later turns with PATCH /conversation/:id/model. Follow-up bodies do not accept a model: the conversation remembers its “next message” settings.
  4. Read history with GET /conversation/:id (paginated, newest first) and list conversations with GET /conversation.
  5. Watch progress live with the WebSocket GET /conversation/:id/ws, or poll GET /conversation/:id and read the AI message status.

Identifiers. Most endpoints accept either the numeric conversation ID or the 8-character slug as :id. The share endpoints, delete, approve and the WebSocket and SSE streams accept only the numeric ID; each is called out below.

The Message object

Returned by the non-stream send endpoints, inside GET /conversation/:id, and as the data of WebSocket message events.

{
  "ID": 90412,
  "CreatedAt": "2026-09-28T09:14:03.512Z",
  "UpdatedAt": "2026-09-28T09:14:09.871Z",
  "DeletedAt": null,
  "conversation_context_id": 5120,
  "ai_model_id": 12,
  "ai_model": { "ID": 12, "name": "Example Model", "stream_support": true, "...": "..." },
  "agent_id": null,
  "is_auto_model": false,
  "text": "Here are three subject lines for the Northwind Q4 newsletter: ...",
  "raw_text": "Here are three subject lines for the Northwind Q4 newsletter: ...",
  "parts": [
    { "id": "p1", "type": "text", "static": true, "text_part": { "text": "Here are three ...", "raw_text": "Here are three ..." } }
  ],
  "thinking": null,
  "isUser": false,
  "error_text": null,
  "error_id": null,
  "error_code": null,
  "mcp_approval_requests": null,
  "ai_style": "prompty",
  "status": "finished",
  "citations": null,
  "response_duration_ms": 6210,
  "type": "normal",
  "attachments": []
}
FieldTypeMeaning
conversation_context_iduint or nullThe conversation this message belongs to.
ai_model_id, ai_modeluint, objectThe model that produced (AI) or was selected for (user) this message.
agent_id, agentuint, objectThe agent that answered, if any.
is_auto_modelbooltrue when auto-routing picked the model.
prompt_iduint or nullSaved prompt used for this user message.
textstringMessage text. For AI messages, citation markers are already rendered.
raw_textstringThe unprocessed AI text (read and stream paths only).
partsarrayStructured blocks. type is text, thinking, web_search, web_fetch, code_execution, image_generation or mcp_call, with the matching *_part object. static is true when there is nothing expandable.
thinkingstring or nullReasoning text for thinking models.
isUserbooltrue for user messages, false for AI messages.
statusstringthinking, generating or finished.
error_text, error_id, error_codestring or nullSet when generation failed. error_id is a 10-character support reference; error_code is a machine code for some failure classes, such as sensitive-data blocks and MCP errors.
mcp_approval_requestsarrayPending or decided MCP tool approvals. See MCP tool approvals.
toolsarrayTools enabled for this turn.
citationsarrayWeb and knowledge-base citations: url, file_path, title, info (kb_id, file_id, page and row ranges), citation_index and more.
knowledge_base_ids, world_knowledgeuint[], boolKnowledge bases searched for this turn, and whether the model could go beyond them.
response_duration_msint or nullGeneration time.
typestringnormal or sub_conversation (a model comparison wrapper).
attachmentsarrayAttached files and transcriptions, each with a leading "type": "file" or "type": "transcription" key. Always an array.

Status. thinking means the AI message row exists but nothing is generated yet; generating means partial output is streaming in; finished means done. A failed AI message is also finished, with error_text and error_id set, so check those fields, not just status. User messages are always finished.

Credits. Every credits_used field is a decimal serialised as a JSON string, for example "0.4213".

Sending messages

Request body

POST /conversation and POST /conversation/stream accept every field below. The follow-up endpoints accept only the fields marked “follow-up”; the rest are silently ignored. No field is strictly required by the binder, but always send a message and a model.

Body
message string optional
The user message. Mention @<agent-tag> to route the turn to one of your agents. Start and follow-up.
attachments array optional
Items { "id": uint, "type": "file" | "transcription", "temp": bool }. Uploaded files or finished transcriptions. temp: true uses the attachment for this turn only, without recording it on the message. Start and follow-up.
knowledge_base_ids uint[] optional
Knowledge bases to retrieve context from for this turn. Start and follow-up.
knowledge_base_limit_node_ids uint[] optional
Restrict retrieval to these knowledge-base folders or files. Start and follow-up.
world_knowledge bool optional default false
false: answer from the knowledge base only. true: the knowledge base is primary, but the model may also use web search and its own knowledge. Start and follow-up.
flash_mode bool optional
Request the provider’s priority service tier. Only has an effect on models that support it. Start and follow-up.
tools array optional
Items { "name", "use", "mcp_tool_id"? }. See Tools. Start and follow-up.
ai_model_id uint optional
Model to use, from GET /ai_model. Ignored if auto_model_type is set. Start only.
auto_model_type string optional
best, balanced or fastest. Auto-routing picks the best model for each message and takes precedence over ai_model_id. Start only.
ai_style string optional default prompty
Response style: the tone value of an AI style. Start only.
reasoning_effort string optional
Reasoning effort for thinking models. An unsupported value falls back to the model’s default; non-thinking models store null. Start only.
agent_id uint optional
Start an agent chat: the agent’s model, system prompt, tools, MCP servers, knowledge bases and files are used on every turn. The agent must be yours. Start only.
invited_agent_ids uint[] optional
Your agents that can be @mentioned in this conversation. Ignored when agent_id is set. Start only.
team_id uint optional
Create a team conversation (the team gets chat_contribute access). See When the AI answers. Start only.
X-Project-ID integer header optional
Start only. Files the conversation under this project. Without a model in the body, the project’s default model is used. On every turn, the project’s files are added to the context. The project_id body field exists but is not read: use this header.

Model selection. auto_model_type wins, then ai_model_id, then the project default (with X-Project-ID). If none applies, the conversation is still created but sending fails with 503 {"error":"no AI model is selected, try reselecting model"}, so always send a model or an auto type when you start. There is no literal "auto" model ID. Agent chats and @agent mentions use the agent’s model (style prompty, no reasoning effort). The model must be allowed by your workspace’s model access and data residency rules, otherwise the send fails with 503.

Tools

Each entry is { "name": ..., "use": true | false }. Names: web_search, image_generation, video_generation, speech_generation, music_generation, remote_mcp.

  • A tool only reaches the model if the model lists it in its tools capabilities. Otherwise it is silently ignored.
  • remote_mcp also needs "mcp_tool_id": <id>, the ID of one of your configured remote MCP servers. Add one entry per server.
  • Automatic tool selection. On models that support tools, a small classifier may switch on web_search and image_generation (and code execution) based on the message, even if you did not ask. Sending an entry for a tool, even {"name":"web_search","use":false}, stops the classifier from adding it. Code execution is decided only by the classifier.

POST /conversation

POST /api/v1/conversation

Creates a conversation, sends the first message and waits for the complete AI reply, including any tool calls. The response is the finished AI Message; the new conversation’s ID is its conversation_context_id. The conversation gets an automatic name after the first exchange. Takes the request body above, plus the optional X-Project-ID header.

curl -X POST https://api.stickyprompts.com/api/v1/conversation \
  -H "Authorization: Bearer $STICKY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Draft three subject lines for the Northwind Q4 newsletter.",
    "ai_model_id": 12,
    "ai_style": "prompty",
    "tools": [{ "name": "web_search", "use": false }]
  }'
import os, requests

r = requests.post(
    "https://api.stickyprompts.com/api/v1/conversation",
    headers={"Authorization": f"Bearer {os.environ['STICKY_API_KEY']}"},
    json={
        "message": "Draft three subject lines for the Northwind Q4 newsletter.",
        "ai_model_id": 12,
        "ai_style": "prompty",
        "tools": [{"name": "web_search", "use": False}],
    },
    timeout=300,
)
r.raise_for_status()
reply = r.json()
if reply.get("error_id"):
    print("Failed:", reply["error_text"], reply["error_id"])
else:
    print(reply["conversation_context_id"], reply["text"])
const res = await fetch("https://api.stickyprompts.com/api/v1/conversation", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.STICKY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    message: "Draft three subject lines for the Northwind Q4 newsletter.",
    ai_model_id: 12,
    ai_style: "prompty",
    tools: [{ name: "web_search", use: false }],
  }),
});
const reply = await res.json();
console.log(reply.conversation_context_id, reply.error_id ?? reply.text);

Response. 200 with the AI Message (see the Message object). When the AI is not supposed to answer (a team conversation with several members and no @ai or agent mention), the body is 200 {"conversation_context_id": 5120} instead.

Errors

StatusBodyWhen
400{"error": "..."}Malformed JSON.
400{"error": "...", "error_code": "..."}Blocked by your workspace’s sensitive-data rules, or an MCP server error. When the AI is not answering, a sensitive-data block uses the key code instead of error_code.
400{"error": "Knowledge base not found"}Unknown knowledge base.
429{"error": "Quota exceeded", "reason": "..."}Checked before anything is created. reason is credit_count, user_count, subscription, trial_expired or unknown.
503{"error": "could not create conversation"}For example an agent_id you do not own or an unknown team_id (a failing team lookup can also surface as 500).
503{"error": "<message>"}Any other failure: no model selected, model restricted, provider error.

If the provider fails mid-generation, the AI message is still saved with error_text and error_id, and POST /conversation/:id/retry can re-run it.

POST /conversation/stream

POST /api/v1/conversation/stream

Same headers and body as POST /conversation, but it returns as soon as the user message is stored. Generation continues in the background and the reply arrives over the WebSocket, or by polling.

{ "conversation_context_id": 5120 }

Errors found before the user message is stored (quota 429, invalid agent, no model selected, restricted model, sensitive-data block) still come back synchronously, with the same shapes as above. Errors after the 200 (model or tool selection, knowledge-base fetch, provider error) are reported only as a WebSocket error_message event and persisted on an AI message with error_text and error_id.

Getting the reply

  • WebSocket (recommended). Connect to GET /conversation/{conversation_context_id}/ws. Events emitted before you connect are buffered and replayed to the first client, so connecting right after the 200 loses nothing.
  • Polling. Call GET /conversation/:id?page=1&size=2 and read the newest AI message (isUser: false, first in the list, since order is newest first). The AI message is created with status: "thinking" and saved on every chunk, so you see generating with growing text, then finished. Stop at finished and check error_id. Right after the 200 the AI message may not exist yet; if the newest message is still your own, generation has not started.

Streaming is real-time only for models with stream_support: true. For other models, the reply appears in one piece when it is finished.

POST /conversation/:id

POST /api/v1/conversation/:id

Sends a follow-up and waits for the AI reply. The model, style and reasoning effort come from the conversation’s next_message_* settings; change them first with PATCH /conversation/:id/model.

Path parameters
id string path required
Conversation ID or slug.
Body
message string optional
The follow-up text.
attachments array optional
As in the request body.
knowledge_base_ids uint[] optional
Knowledge bases for this turn.
knowledge_base_limit_node_ids uint[] optional
Restrict retrieval to these nodes.
world_knowledge bool optional
Allow knowledge beyond the knowledge base.
flash_mode bool optional
Priority service tier, where supported.
tools array optional
Tools for this turn.
curl -X POST https://api.stickyprompts.com/api/v1/conversation/5120 \
  -H "Authorization: Bearer $STICKY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "message": "Make the second one shorter." }'

Response. Same as POST /conversation: 200 with the AI Message, or 200 {"conversation_context_id": ...} when the AI does not answer. If the conversation has an active model comparison, the message goes to every thread in parallel and the response is a JSON array of AI Messages, one per thread.

Errors. As for POST /conversation, plus 404 {"error":"Conversation not found"}. Only the owner and users or teams with chat_contribute can send; anyone else gets 503 {"error":"you are not allowed to send messages in this conversation"}.

POST /conversation/:id/stream

POST /api/v1/conversation/:id/stream

Same as POST /conversation/:id, but returns 200 {"conversation_context_id": <id>} immediately. Follow progress over the WebSocket or by polling, as for POST /conversation/stream. With an active model comparison, every thread streams onto the parent conversation’s WebSocket.

Path parameters
id string path required
Conversation ID or slug.

When the AI answers

The AI answers a message when any of these is true:

  • only one user has contribute access to the conversation (the normal case, including every conversation created with an API key and no team_id);
  • the text contains @ai (case-sensitive);
  • it is an agent chat;
  • the text @mentions the tag of an agent invited to the conversation.

Otherwise the message is stored as a plain user message, visible to the other participants, and the endpoint returns 200 {"conversation_context_id": <id>} without calling a model, even on the non-stream endpoints.

Agents in a conversation

  • agent_id at creation makes an agent chat. Its model cannot be changed later.
  • In a single-user conversation, @tag routes that turn to your own agent with that tag, or to an agent shared with you (your own agents win a tag clash). In a multi-user conversation, only agents invited with invited_agent_ids or POST /conversation/:id/agents can be mentioned.
  • The answering agent is recorded on the AI message (agent_id, agent).

Streaming

GET /conversation/:id/ws

GET /api/v1/conversation/:id/ws

A WebSocket that pushes every update of the conversation. Connect to wss://api.stickyprompts.com/api/v1/conversation/5120/ws and send the Authorization: Bearer STICKY-API-... header on the upgrade request. Browser WebSocket objects cannot set headers, so this is for server-side clients. The server ignores anything you send; it only reads to detect the close.

Path parameters
id integer path required
The numeric conversation ID. A slug connects to a different, empty stream.

Every frame is a JSON text frame {"type": <string>, "data": <payload>}:

typedataWhen
messageMessage (full snapshot)A new user message from another participant, and every update of an AI message during generation. text, parts and thinking hold the accumulated content so far, not a delta: replace your copy by data.ID. status goes thinking, generating, finished.
ai_message_endnone (key absent)Generation finished, successfully or not. Terminal for the turn.
error_messageMessage with error_text, error_id, optional error_code, status: "finished"Generation or setup failed. Treat as terminal: an ai_message_end usually follows setup failures, but is not guaranteed after mid-generation provider errors.
ai_model_change{ "ai_model_id", "ai_style", "auto_model_type", "reasoning_effort" }Someone called PATCH /conversation/:id/model.
run_dataRunHistory objectOnly for turns driven by a saved prompt run, alongside message events. See Prompts and runs.
{"type":"message","data":{"ID":90412,"isUser":false,"status":"generating","text":"Here are","...":"..."}}
{"type":"message","data":{"ID":90412,"isUser":false,"status":"generating","text":"Here are three options","...":"..."}}
{"type":"message","data":{"ID":90412,"isUser":false,"status":"finished","text":"Here are three options for ...","response_duration_ms":6210,"...":"..."}}
{"type":"ai_message_end"}
  • Your own user messages are not echoed to your connection. The key acts as its owner, so a key-driven client sees only AI messages and messages from other people.
  • While no client is connected, events are buffered and replayed to the next connection, then cleared. A client that connects long after a turn may first receive stale events from it: ignore messages whose ID you already hold in a finished state.
  • Streams live in the API server’s memory. If you connect and receive nothing for a turn you know is running, fall back to polling GET /conversation/:id.
# 1. Start the turn without waiting
curl -X POST https://api.stickyprompts.com/api/v1/conversation/stream \
  -H "Authorization: Bearer $STICKY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "message": "Summarise last week for the Northwind team.", "auto_model_type": "balanced" }'
# -> {"conversation_context_id":5120}

# 2. cURL cannot hold a WebSocket open; poll the newest messages instead
curl "https://api.stickyprompts.com/api/v1/conversation/5120?page=1&size=2" \
  -H "Authorization: Bearer $STICKY_API_KEY"
import asyncio, json, os, requests, websockets

KEY = os.environ["STICKY_API_KEY"]
BASE = "https://api.stickyprompts.com/api/v1"

start = requests.post(
    f"{BASE}/conversation/stream",
    headers={"Authorization": f"Bearer {KEY}"},
    json={"message": "Summarise last week for the Northwind team.", "auto_model_type": "balanced"},
).json()
cid = start["conversation_context_id"]

async def listen():
    url = f"wss://api.stickyprompts.com/api/v1/conversation/{cid}/ws"
    async with websockets.connect(url, additional_headers={"Authorization": f"Bearer {KEY}"}) as ws:
        async for frame in ws:
            event = json.loads(frame)
            if event["type"] == "message" and not event["data"]["isUser"]:
                print(event["data"]["status"], len(event["data"]["text"]))
            elif event["type"] == "error_message":
                print("Failed:", event["data"]["error_text"], event["data"]["error_id"])
                break
            elif event["type"] == "ai_message_end":
                break

asyncio.run(listen())
import WebSocket from "ws"; // Node: browsers cannot send the Authorization header

const KEY = process.env.STICKY_API_KEY;
const BASE = "https://api.stickyprompts.com/api/v1";

const { conversation_context_id: cid } = await fetch(`${BASE}/conversation/stream`, {
  method: "POST",
  headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ message: "Summarise last week for the Northwind team.", auto_model_type: "balanced" }),
}).then((r) => r.json());

const ws = new WebSocket(`wss://api.stickyprompts.com/api/v1/conversation/${cid}/ws`, {
  headers: { Authorization: `Bearer ${KEY}` },
});
ws.on("message", (raw) => {
  const { type, data } = JSON.parse(raw);
  if (type === "message" && !data.isUser) console.log(data.status, data.text.length);
  if (type === "error_message") { console.error(data.error_text, data.error_id); ws.close(); }
  if (type === "ai_message_end") ws.close();
});

GET /conversation/:id/stream

GET /api/v1/conversation/:id/stream

A legacy Server-Sent Events endpoint. Use the WebSocket or polling instead. In the current code, the conversation send path writes only to the WebSocket, so this stream surfaces only error_message and run_data events for prompt-run errors and otherwise stays open without sending message content.

Path parameters
id integer path required
Numeric conversation ID.

It returns 404 {"error":"Stream not found"} when the server holds no in-memory stream for the conversation. Otherwise it responds with Content-Type: text/event-stream and frames event:<name>\ndata:<json>\n\n, where <name> is message, error_message (array of {run_id, error_id, error_text, model_name}), run_data (array of RunHistory) or close (data: Stream closed).

Reading conversations

GET /conversation/:id

GET /api/v1/conversation/:id

Returns one conversation with a page of its messages, newest first. Pagination metadata is in the X-Total-Count (total messages), X-Page and X-Page-Size response headers.

Path parameters
id string path required
Conversation ID, slug, or the conversation’s public share slug.
Query parameters
page integer query optional default 1
Page number.
size integer query optional default 10
Messages per page.

Access. The owner; users with chat_view or chat_contribute through sharing; or, when :id is the public slug of a shared conversation, anyone the share type allows (public: anyone, internal: any signed-in user, workspace: members of the owner’s workspace).

{
  "credits_used": "0.4213",
  "conversation_context_id": 5120,
  "conversation_context_name": "Q4 newsletter subject lines",
  "conversation_context_slug": "aB3dE5gH",
  "expensive_warning_threshold": 1000,
  "public_slug": null,
  "share_type": "",
  "is_public": false,
  "user_name": "John Smith",
  "messages": [
    { "ID": 90412, "isUser": false, "status": "finished", "text": "Here are three options ...", "...": "..." },
    { "ID": 90411, "isUser": true, "status": "finished", "text": "Draft three subject lines ...", "...": "..." }
  ],
  "next_message_ai_model_id": 12,
  "next_message_ai_model_style": "prompty",
  "next_message_auto_model_type": null,
  "next_message_reasoning_effort": null,
  "has_sub_conversations": false,
  "unsupported_files": null,
  "current_user_permission": "chat_contribute",
  "owner_id": 77,
  "user_count": 1,
  "team_shares": null,
  "agent_id": null,
  "invited_agents": [],
  "is_agent_chat": false
}
FieldMeaning
credits_usedCredits for the whole conversation, including comparison threads (decimal string).
messagesThis page of messages, newest first. AI messages have citations rendered in text, original in raw_text.
next_message_ai_model_id, next_message_ai_modelThe model the next turn will use. Falls back to the last AI message’s model.
next_message_ai_model_styleNext turn’s style. Note the key differs from the conversation object’s next_message_ai_style.
next_message_auto_model_typebest, balanced or fastest when auto-routed.
forked_from_conversation_id, _slug, _nameSet on forks.
has_sub_conversationsA model comparison exists.
unsupported_filesFiles attached earlier that the next model cannot read.
current_user_permissionchat_contribute for the owner, otherwise your highest access: chat_view, chat_contribute or none.
team_shares{team_id, team, access_type} for each team the conversation is shared with.

Errors. 403 {"error":"You are not allowed to access this conversation"}, 404 {"error":"Conversation not found"}.

GET /conversation

GET /api/v1/conversation

Lists your chat conversations, most recently active first. Only interactive chat conversations with at least one message are listed.

Query parameters
page integer query optional default 1
Page number. Totals are in the X-Total-Count, X-Page and X-Page-Size headers.
size integer query optional default 10
Items per page.
search string query optional
Substring match on any message text or on the conversation name.
agent_id uint query optional
List the chats of this agent. Without it, agent chats are excluded.
team_id uint query optional
List conversations shared with this team with chat_contribute. Without it, you get conversations you own that are not team conversations.
X-Project-ID integer header optional
Only conversations in this project.

Response. 200 with an array of conversation objects (empty: []). Each has user and project preloaded, message_count, credits_used, and in messages only the latest message (with attachments emptied). Note the camelCase keys publicSlug, isPublic, userId, projectId, runId, isInteractive and forkedFromConversationId next to snake_case ones.

[
  {
    "ID": 5120, "CreatedAt": "2026-09-28T09:13:58Z", "UpdatedAt": "2026-09-28T09:14:10Z", "DeletedAt": null,
    "slug": "aB3dE5gH", "publicSlug": null, "isPublic": false, "share_type": "",
    "userId": 77, "user": { "ID": 77, "first_name": "John", "last_name": "Smith", "...": "..." },
    "next_message_ai_model_id": 12, "next_message_ai_style": "prompty",
    "next_message_auto_model_type": null, "next_message_reasoning_effort": null,
    "projectId": null, "project": null, "agent_id": null,
    "name": "Q4 newsletter subject lines", "source": "chat",
    "messages": [ { "ID": 90412, "isUser": false, "text": "Here are three options ...", "...": "..." } ],
    "isInteractive": true, "expensive_warning_threshold": 1000,
    "forkedFromConversationId": null,
    "message_count": 2, "credits_used": "0.4213"
  }
]

GET /conversation/:id/used_prompts

GET /api/v1/conversation/:id/used_prompts

Returns the distinct saved prompts run inside this conversation, including comparison threads: an array of { "id", "name", "slug", "ai_model_id", "user_id" }, or []. Order is not defined.

Path parameters
id string path required
Conversation ID or slug.

Managing conversations

PATCH /conversation/:id

PATCH /api/v1/conversation/:id

Renames a conversation and moves it to the top of the list. Owner only.

Path parameters
id string path required
Conversation ID or slug.
Body
name string required
The new name. Empty or missing gives 400.

Response. 200 with the conversation object. Errors. 403 {"error":"You are not allowed to edit this conversation"}.

PATCH /conversation/:id/model

PATCH /api/v1/conversation/:id/model

Sets the model, style and reasoning effort for the next turns. Allowed for the owner and chat_contribute users. Also broadcasts an ai_model_change WebSocket event.

Path parameters
id string path required
Conversation ID or slug.
Body
ai_model_id uint optional
Model for the next turns. 400 {"error":"AI model not found"} if unknown.
ai_style string optional
Style for the next turns.
auto_model_type string optional
best, balanced, fastest or null. Always written: omitting it or sending null turns auto-routing off. Send it on every call to stay auto-routed.
reasoning_effort string optional
Normalised against the new or current model’s supported efforts.

Response. 200 {"message":"Model changed successfully"}. Errors. 400 {"error":"Cannot change model for agent conversations"}, 403 when you lack access.

POST /conversation/:id/fork

POST /api/v1/conversation/:id/fork

Creates a new conversation named "Forked from <name>" with copies of every message up to and including message_id. Attachments, tools and MCP approval records are copied, and the model, style and reasoning settings are kept. You must own the source conversation or it must be shared, otherwise 403.

Path parameters
id string path required
ID or slug of the source conversation.
Body
message_id uint required
The last message to copy. Must be a normal message of this conversation or of one of its comparison threads (404 or 400 otherwise).

Response. 200 with a reduced conversation response: conversation_context_id, conversation_context_name, conversation_context_slug, public_slug, is_public, user_name, messages (oldest first here) and credits_used: "0". The other keys have zero values.

DELETE /conversation/:id

DELETE /api/v1/conversation/:id

Soft-deletes the conversation and its messages and hides its prompt runs from run history. Owner only.

Path parameters
id integer path required
Numeric conversation ID.
curl -X DELETE https://api.stickyprompts.com/api/v1/conversation/5120 \
  -H "Authorization: Bearer $STICKY_API_KEY"

Response. 200 {"message":"Conversation deleted successfully"}. Errors. 404 {"error":"Conversation not found or you do not have permission to delete it"}.

POST /conversation/:id/retry

POST /api/v1/conversation/:id/retry

Re-runs the last turn when the latest AI message ended in an error. The failed message is regenerated in place (same ID) with the conversation’s current model and style, or the agent’s. Owner only. No body.

Path parameters
id string path required
Conversation ID or slug.

Response. 200 with:

  • for a plain chat turn, a Message. On models with stream_support: true the call returns at once with the message in status: "thinking" and the content arrives over the WebSocket or polling; on other models it returns the finished message;
  • for a turn that came from a saved prompt, an array of RunHistory objects.

Errors. 400 {"error":"Last AI message does not have an error"}, 400 {"error":"No user message found in this conversation"}, 403, 500 {"error":"..."}.

POST /conversation/:id/snooze-expensive-warning

POST /api/v1/conversation/:id/snooze-expensive-warning

When a conversation’s credits_used passes its expensive_warning_threshold (default 1000), the web app shows a usage warning. This doubles the threshold until it is above current usage. Owner only.

Path parameters
id string path required
Conversation ID or slug.

Response. 200 with the conversation object and its new expensive_warning_threshold. Errors. 400 if usage is still below the threshold.

POST /conversation/:id/agents

POST /api/v1/conversation/:id/agents

Invites one of your agents into the conversation so it can be @mentioned. Owner only; not allowed on agent chats (400).

Path parameters
id string path required
Conversation ID or slug.
Body
agent_id uint required
An agent you own.

Response. 200 {"message":"Agent invited successfully"}. Errors. 404 {"error":"Agent not found or not owned by user"}, 403.

DELETE /conversation/:id/agents/:agentId

DELETE /api/v1/conversation/:id/agents/:agentId

Removes an invited agent. Owner only.

Path parameters
id string path required
Conversation ID or slug.
agentId integer path required
Numeric agent ID.

Response. 200 {"message":"Agent removed successfully"}. Errors. 400 {"error":"Invalid agent ID"}, 404.

MCP tool approvals

This applies when a turn uses a remote MCP server (a remote_mcp tool, or an agent with MCP servers) whose configuration has require_approval: true, and the model runs on the OpenAI Responses protocol, the only path that produces approvals. Workflow runs always auto-approve.

  1. The model asks to call an MCP tool. Generation stops and the AI message gets one entry per requested call in mcp_approval_requests. On the streaming path, the text Approval required for action: <name> with the server label and arguments is also appended to the message text.
  2. Each entry has Go field names as keys (no JSON tags). Approved: null means pending; true or false means decided.
{
  "ID": 311, "CreatedAt": "2026-09-28T09:20:01Z", "UpdatedAt": "2026-09-28T09:20:01Z", "DeletedAt": null,
  "ApprovalRequestID": "mcpr_abc123",
  "Approved": null,
  "ServerLabel": "crm",
  "Name": "create_contact",
  "Arguments": "{\"email\":\"jane@northwind.example\"}",
  "MCPToolID": 0,
  "MessageID": 90415
}
  1. Decide with POST /conversation/:id/approve, using the entry’s numeric ID, not ApprovalRequestID.
  2. The server records the decision and runs a new AI turn that passes it back to the model. The continuation is a new AI message.

POST /conversation/:id/approve

POST /api/v1/conversation/:id/approve

Approves or rejects a pending MCP tool call.

Path parameters
id integer path required
Numeric conversation ID.
Body
approval_request_id uint required
The ID of the entry in mcp_approval_requests.
is_approved bool required
true to approve, false to reject.
use_stream bool optional
true: return immediately; the continuation arrives over the WebSocket or polling. false: wait for the continuation to finish.

Response. 200 with the JSON string "Successfully handled approval", not an object. The continuation message is not returned: read it with GET /conversation/:id or the WebSocket.

Errors. 404 {"error":"Conversation not found"}, 404 {"error":"MCP Approval request not found"}, 400 {"error":"Approval request already processed"}, 400 {"error":"No messages in conversation"}, 500. The continuation uses the conversation’s fixed next-message model; on an auto-routed conversation this is not supported.

Model comparison

A sub-conversation runs the same message against two or more models side by side, one “thread” per model. While it is active, follow-ups to the parent go to every thread. Closing it picks a winning thread whose history continues in the parent. This is the API side of Compare models.

POST /conversation/:id/sub-conversation

POST /api/v1/conversation/:id/sub-conversation

Starts a comparison and waits for every thread.

Path parameters
id string path required
ID or slug of your conversation (owner only). The literal values null or undefined create a new empty conversation first.
Body
ai_model_ids uint[] required
At least 2 model IDs.
message string optional
The message sent to every model.
prompt_instance_id uint optional
Instead of message, run a saved prompt instance in each thread.
attachments array optional
Only used with prompt_instance_id.
flash_mode bool optional
Only used with prompt_instance_id.
reasoning_effort string optional
Only used with prompt_instance_id.

The style is the conversation’s style, else your preferred style, else prompty.

Response. 200 with the wrapper Message (type: "sub_conversation"). Its sub_conversation holds parent_conversation_context_id, threads, selected_thread_id and is_active: true. Each thread has ai_model_id, ai_model, thread_conversation_context_id, thread_conversation_context (with messages), unsupported_files and credits_used.

Errors. 400 with fewer than 2 models, 400 {"error":"AI model not found: <id>"}, 400 {"error":"An active sub-conversation already exists. Close it before creating a new one."}.

POST /conversation/:id/sub-conversation/stream

POST /api/v1/conversation/:id/sub-conversation/stream

The streaming variant: same path parameters and body, but it returns once the user messages exist. Thread replies then stream onto the parent conversation’s WebSocket, and each thread’s messages in the response holds only the user message.

POST /sub-conversation/:id/close

POST /api/v1/sub-conversation/:id/close

Closes an active comparison in a conversation you own.

Path parameters
id integer path required
Numeric sub-conversation ID (sub_conversation_id or sub_conversation.ID).
Body
ai_model_id uint required
Becomes the parent’s next-message model. Must be greater than 0.
ai_style string optional
Becomes the parent’s style.
thread_id uint optional
The winning thread. 0 means none.

Response. 204 No Content. Errors. 404 if not found or not active.

POST /sub-conversation/:id/select-thread

POST /api/v1/sub-conversation/:id/select-thread

Changes which thread’s messages count as the conversation history. The sub-conversation must be closed; owner only.

Path parameters
id integer path required
Numeric sub-conversation ID.
Body
thread_id uint required
Greater than 0.

Response. 204 No Content.

Sharing

All four sharing endpoints take the numeric conversation ID and are owner only (404 {"error":"Conversation not found"} otherwise).

POST /conversation/:id/share

POST /api/v1/conversation/:id/share

Shares the conversation by link. Generates a 32-character public slug if there is none and sets isPublic: true. Read the shared conversation with GET /conversation/{public_slug}.

Path parameters
id integer path required
Numeric conversation ID.
Body
share_type string required
workspace (members of your workspace), internal (any signed-in StickyPrompts user with the link) or public (anyone with the link).

Response. 200 {"message":"Conversation shared successfully","publicSlug":"<32 chars>"}. Errors. 400 {"error":"Invalid share type"}.

POST /conversation/:id/revoke-share

POST /api/v1/conversation/:id/revoke-share

Sets isPublic: false. The slug is kept. No body.

Path parameters
id integer path required
Numeric conversation ID.

Response. 200 {"message":"Conversation share successfully revoked"}.

POST /conversation/:id/regenrate-share-slug

POST /api/v1/conversation/:id/regenrate-share-slug

Replaces the public slug so old links stop working. The path is spelled regenrate, exactly as shown. No body.

Path parameters
id integer path required
Numeric conversation ID.

Response. 200 {"slug":"<new 32 chars>"}.

POST /conversation/:id/share-with-teams

POST /api/v1/conversation/:id/share-with-teams

Shares the conversation read-only with your teams. The list is the full desired state for your teams: any team you belong to that is not listed is unshared, except teams that already have chat_contribute (such as the team a team conversation was created for). Also ensures a public slug exists.

Path parameters
id integer path required
Numeric conversation ID.
Body
shares array required
Items { "team_id": uint, "access_type": "chat_view" | "none" }. Only chat_view can be granted here; chat_contribute is rejected with 400.

Response. 200 {"message":"Conversation shared successfully with teams","publicSlug":"<32 chars>"}. Errors. 403 {"error":"You are not a member of team <name>"}, 404 for unknown teams.

Messages

POST /message/:id/export

POST /api/v1/message/:id/export

Exports one message to a DOCX or PDF file, either as a download link or straight into a connected storage integration such as Google Drive. You need to own the conversation or have at least chat_view on it.

Path parameters
id integer path required
Numeric message ID.
Body
format string required
docx or pdf.
target string optional
Empty or local: produce a downloadable file. Otherwise a connected storage provider identifier, for example google_drive.
folder_id string optional
Destination folder in the provider. Required for non-local targets.
name string optional
File name for provider targets (the extension is added). Defaults to the conversation title.

Response. 200 with exactly one of:

{ "path": "https://<storage host>/<bucket>/<userId>/<random>.pdf" }
{ "entry": { "id": "...", "name": "Q4 subject lines.docx", "kind": "file", "mime_type": "...", "web_url": "...", "parent_id": "...", "...": "..." } }

Errors. 400 {"error":"invalid format"}, 400 {"error":"invalid folder_id"}, 400 {"error":"unknown export target"}, 403 {"error":"you are not allowed to export this message"} or a destination folder that is not writable, 404 message not found or integration not connected, 409 {"error":"reconnect_required"}, 501 provider cannot upload, 500.