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.
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
- Start with
POST /conversation(waits for the full reply) orPOST /conversation/stream(returns at once; the reply arrives over the WebSocket). Both create the conversation and send the first message in one call. - Follow up with
POST /conversation/:idorPOST /conversation/:id/stream. - 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. - Read history with
GET /conversation/:id(paginated, newest first) and list conversations withGET /conversation. - Watch progress live with the WebSocket
GET /conversation/:id/ws, or pollGET /conversation/:idand read the AI messagestatus.
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": []
}
| Field | Type | Meaning |
|---|---|---|
conversation_context_id | uint or null | The conversation this message belongs to. |
ai_model_id, ai_model | uint, object | The model that produced (AI) or was selected for (user) this message. |
agent_id, agent | uint, object | The agent that answered, if any. |
is_auto_model | bool | true when auto-routing picked the model. |
prompt_id | uint or null | Saved prompt used for this user message. |
text | string | Message text. For AI messages, citation markers are already rendered. |
raw_text | string | The unprocessed AI text (read and stream paths only). |
parts | array | Structured 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. |
thinking | string or null | Reasoning text for thinking models. |
isUser | bool | true for user messages, false for AI messages. |
status | string | thinking, generating or finished. |
error_text, error_id, error_code | string or null | Set 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_requests | array | Pending or decided MCP tool approvals. See MCP tool approvals. |
tools | array | Tools enabled for this turn. |
citations | array | Web 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_knowledge | uint[], bool | Knowledge bases searched for this turn, and whether the model could go beyond them. |
response_duration_ms | int or null | Generation time. |
type | string | normal or sub_conversation (a model comparison wrapper). |
attachments | array | Attached 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.
-
messagestring optional - The user message. Mention
@<agent-tag>to route the turn to one of your agents. Start and follow-up. -
attachmentsarray optional - Items
{ "id": uint, "type": "file" | "transcription", "temp": bool }. Uploaded files or finished transcriptions.temp: trueuses the attachment for this turn only, without recording it on the message. Start and follow-up. -
knowledge_base_idsuint[] optional - Knowledge bases to retrieve context from for this turn. Start and follow-up.
-
knowledge_base_limit_node_idsuint[] optional - Restrict retrieval to these knowledge-base folders or files. Start and follow-up.
-
world_knowledgebool optional defaultfalse 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_modebool optional - Request the provider’s priority service tier. Only has an effect on models that support it. Start and follow-up.
-
toolsarray optional - Items
{ "name", "use", "mcp_tool_id"? }. See Tools. Start and follow-up. -
ai_model_iduint optional - Model to use, from
GET /ai_model. Ignored ifauto_model_typeis set. Start only. -
auto_model_typestring optional best,balancedorfastest. Auto-routing picks the best model for each message and takes precedence overai_model_id. Start only.-
ai_stylestring optional defaultprompty - Response style: the
tonevalue of an AI style. Start only. -
reasoning_effortstring optional - Reasoning effort for thinking models. An unsupported value falls back to the model’s default; non-thinking models store null. Start only.
-
agent_iduint 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_idsuint[] optional - Your agents that can be
@mentionedin this conversation. Ignored whenagent_idis set. Start only. -
team_iduint optional - Create a team conversation (the team gets
chat_contributeaccess). See When the AI answers. Start only. -
X-Project-IDinteger 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_idbody 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
toolscapabilities. Otherwise it is silently ignored. remote_mcpalso 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_searchandimage_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
/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
| Status | Body | When |
|---|---|---|
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
/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 the200loses nothing. - Polling. Call
GET /conversation/:id?page=1&size=2and read the newest AI message (isUser: false, first in the list, since order is newest first). The AI message is created withstatus: "thinking"and saved on every chunk, so you seegeneratingwith growingtext, thenfinished. Stop atfinishedand checkerror_id. Right after the200the 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
/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.
-
idstring path required - Conversation ID or slug.
-
messagestring optional - The follow-up text.
-
attachmentsarray optional - As in the request body.
-
knowledge_base_idsuint[] optional - Knowledge bases for this turn.
-
knowledge_base_limit_node_idsuint[] optional - Restrict retrieval to these nodes.
-
world_knowledgebool optional - Allow knowledge beyond the knowledge base.
-
flash_modebool optional - Priority service tier, where supported.
-
toolsarray 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
/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.
-
idstring 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
@mentionsthe 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_idat creation makes an agent chat. Its model cannot be changed later.- In a single-user conversation,
@tagroutes 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 withinvited_agent_idsorPOST /conversation/:id/agentscan be mentioned. - The answering agent is recorded on the AI message (
agent_id,agent).
Streaming
GET /conversation/:id/ws
/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.
-
idinteger 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>}:
type | data | When |
|---|---|---|
message | Message (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_end | none (key absent) | Generation finished, successfully or not. Terminal for the turn. |
error_message | Message 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_data | RunHistory object | Only 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
IDyou 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
/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.
-
idinteger 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
/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.
-
idstring path required - Conversation ID, slug, or the conversation’s public share slug.
-
pageinteger query optional default1 - Page number.
-
sizeinteger query optional default10 - 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
}
| Field | Meaning |
|---|---|
credits_used | Credits for the whole conversation, including comparison threads (decimal string). |
messages | This page of messages, newest first. AI messages have citations rendered in text, original in raw_text. |
next_message_ai_model_id, next_message_ai_model | The model the next turn will use. Falls back to the last AI message’s model. |
next_message_ai_model_style | Next turn’s style. Note the key differs from the conversation object’s next_message_ai_style. |
next_message_auto_model_type | best, balanced or fastest when auto-routed. |
forked_from_conversation_id, _slug, _name | Set on forks. |
has_sub_conversations | A model comparison exists. |
unsupported_files | Files attached earlier that the next model cannot read. |
current_user_permission | chat_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
/api/v1/conversation Lists your chat conversations, most recently active first. Only interactive chat conversations with at least one message are listed.
-
pageinteger query optional default1 - Page number. Totals are in the
X-Total-Count,X-PageandX-Page-Sizeheaders. -
sizeinteger query optional default10 - Items per page.
-
searchstring query optional - Substring match on any message text or on the conversation name.
-
agent_iduint query optional - List the chats of this agent. Without it, agent chats are excluded.
-
team_iduint 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-IDinteger 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
/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.
-
idstring path required - Conversation ID or slug.
Managing conversations
PATCH /conversation/:id
/api/v1/conversation/:id Renames a conversation and moves it to the top of the list. Owner only.
-
idstring path required - Conversation ID or slug.
-
namestring 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
/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.
-
idstring path required - Conversation ID or slug.
-
ai_model_iduint optional - Model for the next turns.
400 {"error":"AI model not found"}if unknown. -
ai_stylestring optional - Style for the next turns.
-
auto_model_typestring optional best,balanced,fastestor null. Always written: omitting it or sending null turns auto-routing off. Send it on every call to stay auto-routed.-
reasoning_effortstring 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
/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.
-
idstring path required - ID or slug of the source conversation.
-
message_iduint required - The last message to copy. Must be a
normalmessage of this conversation or of one of its comparison threads (404or400otherwise).
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
/api/v1/conversation/:id Soft-deletes the conversation and its messages and hides its prompt runs from run history. Owner only.
-
idinteger 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
/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.
-
idstring path required - Conversation ID or slug.
Response. 200 with:
- for a plain chat turn, a Message. On models with
stream_support: truethe call returns at once with the message instatus: "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
/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.
-
idstring 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
/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).
-
idstring path required - Conversation ID or slug.
-
agent_iduint 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
/api/v1/conversation/:id/agents/:agentId Removes an invited agent. Owner only.
-
idstring path required - Conversation ID or slug.
-
agentIdinteger 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.
- 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 textApproval required for action: <name>with the server label and arguments is also appended to the message text. - Each entry has Go field names as keys (no JSON tags).
Approved: nullmeans pending;trueorfalsemeans 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
}
- Decide with
POST /conversation/:id/approve, using the entry’s numericID, notApprovalRequestID. - 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
/api/v1/conversation/:id/approve Approves or rejects a pending MCP tool call.
-
idinteger path required - Numeric conversation ID.
-
approval_request_iduint required - The
IDof the entry inmcp_approval_requests. -
is_approvedbool required trueto approve,falseto reject.-
use_streambool 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
/api/v1/conversation/:id/sub-conversation Starts a comparison and waits for every thread.
-
idstring path required - ID or slug of your conversation (owner only). The literal values
nullorundefinedcreate a new empty conversation first.
-
ai_model_idsuint[] required - At least 2 model IDs.
-
messagestring optional - The message sent to every model.
-
prompt_instance_iduint optional - Instead of
message, run a saved prompt instance in each thread. -
attachmentsarray optional - Only used with
prompt_instance_id. -
flash_modebool optional - Only used with
prompt_instance_id. -
reasoning_effortstring 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
/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
/api/v1/sub-conversation/:id/close Closes an active comparison in a conversation you own.
-
idinteger path required - Numeric sub-conversation ID (
sub_conversation_idorsub_conversation.ID).
-
ai_model_iduint required - Becomes the parent’s next-message model. Must be greater than 0.
-
ai_stylestring optional - Becomes the parent’s style.
-
thread_iduint optional - The winning thread.
0means none.
Response. 204 No Content. Errors. 404 if not found or not active.
POST /sub-conversation/:id/select-thread
/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.
-
idinteger path required - Numeric sub-conversation ID.
-
thread_iduint 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
/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}.
-
idinteger path required - Numeric conversation ID.
-
share_typestring required workspace(members of your workspace),internal(any signed-in StickyPrompts user with the link) orpublic(anyone with the link).
Response. 200 {"message":"Conversation shared successfully","publicSlug":"<32 chars>"}. Errors. 400 {"error":"Invalid share type"}.
POST /conversation/:id/revoke-share
/api/v1/conversation/:id/revoke-share Sets isPublic: false. The slug is kept. No body.
-
idinteger path required - Numeric conversation ID.
Response. 200 {"message":"Conversation share successfully revoked"}.
POST /conversation/:id/regenrate-share-slug
/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.
-
idinteger path required - Numeric conversation ID.
Response. 200 {"slug":"<new 32 chars>"}.
POST /conversation/:id/share-with-teams
/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.
-
idinteger path required - Numeric conversation ID.
-
sharesarray required - Items
{ "team_id": uint, "access_type": "chat_view" | "none" }. Onlychat_viewcan be granted here;chat_contributeis rejected with400.
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
/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.
-
idinteger path required - Numeric message ID.
-
formatstring required docxorpdf.-
targetstring optional - Empty or
local: produce a downloadable file. Otherwise a connected storage provider identifier, for examplegoogle_drive. -
folder_idstring optional - Destination folder in the provider. Required for non-local targets.
-
namestring 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.
Related
- Prompts and runs - run saved prompts; each run creates a conversation.
- Errors, pagination and quotas - error shapes,
429reasons and pagination headers. - MCP tool reference -
send_message,get_conversation,list_conversations,delete_conversation.