Conversations
Project-scope conversations are server-stored, multi-thread, and partitioned per end-user.
Prerequisites. You'll need an
agent_id(an agent that's a member of the project — see Invites and members) and theproject_id(returned when you create a project, or visible in the dashboard URL). Mint a project key (jg_p_*) from the API → API keys sub-tab in the dashboard, or via the endpoint in Webhooks & Rate Limits.
Mental model
Every conversation has exactly one owner type:
- Project, no user — created by a
jg_p_*call withoutX-USER-ID. Visible to other "no user" callers and to the project owner via JWT. - Project, external user — created by a
jg_p_*call carryingX-USER-ID. Visible only to calls bearing the same project key + sameX-USER-ID, and to the project owner via JWT.
A conversation can have messages from multiple agents — the participating-agent list is derived dynamically from DISTINCT agent_id in messages, not stored. Multi-agent threads inside one project just work.
Chat ID namespace
Underneath, the agent runtime keys its per-thread state by a chat_id string. The shape encodes the principal so audit logs are clear:
| Caller | chat_id shape |
|---|---|
| Owner JWT | ui:{account_id}:conv:{conversation_id} |
| Project key (no user) | project:{pid}:key:{api_key_id}:conv:{conversation_id} |
| Project key (passthrough user) | project:{pid}:user:{shadow_user_id}:conv:{conversation_id} |
You don't need to construct these — they're derived from the auth context plus conversation_id.
SSE streaming behavior
POST /api/projects/{id}/chat returns Server-Sent Events. The first event is metadata, then a stream of content chunks, then done. Events look like:
data: {"type":"meta","conversation_id":"f9e8..."}
data: {"type":"content","text":"Hel"}
data: {"type":"content","text":"lo"}
data: {"type":"done"}
On error mid-stream you'll see {"type":"error","message":"..."}.
The user message is persisted before the agent runs, and the assistant reply is persisted on a background "drain" task — so even if your HTTP client disconnects mid-stream, the message log on the server is complete. The drain considers the stream over when either (a) the agent emits its terminal done, or (b) 8 seconds elapse with no new tokens — that's the idle timeout for hung tool-callbacks.
Endpoints
POST /api/projects/{id}/chat
Stream a chat reply. Auto-creates a conversation when conversation_id is omitted; the new id is in the first SSE event.
Auth: Project key (Mode 2 or Mode 3). The path id must match the key's project.
POST /api/projects/3a7b5c1d-.../chat
Authorization: Bearer jg_p_a1b2c3d4...
Content-Type: application/json
X-USER-ID: customer_47291
{
"agent_id": "8f4d2e1a-9b3c-4d5e-6f7a-8b9c0d1e2f3a",
"message": "What's my portfolio worth?",
"conversation_id": null
}
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: {"type":"meta","conversation_id":"a1b2c3d4-..."}
data: {"type":"content","text":"Your portfolio is currently worth $12,450."}
data: {"type":"done"}
cURL:
curl -N -X POST https://api.juglans.ai/api/projects/3a7b5c1d-.../chat \
-H "Authorization: Bearer jg_p_a1b2c3d4..." \
-H "Content-Type: application/json" \
-H "X-USER-ID: customer_47291" \
-d '{"agent_id":"8f4d2e1a-...","message":"What is my portfolio worth?"}'
Errors (all {"error": "<msg>"}, with extra fields where noted):
403— key is for a different project.404— agent is not a member of this project, orconversation_iddoesn't belong to the caller's partition.429— project rate limit exceeded; body addsretry_after_secondsandlimit_rpmalongsideerror.
POST /api/projects/{id}/conversations
Create an empty conversation eagerly (most callers just let chat auto-create one).
Auth: Project key.
POST /api/projects/3a7b5c1d-.../conversations
Authorization: Bearer jg_p_a1b2c3d4...
Content-Type: application/json
X-USER-ID: customer_47291
{
"title": "Onboarding"
}
{
"conversation": {
"id": "a1b2c3d4-...",
"account_id": null,
"project_id": "3a7b5c1d-...",
"external_user_id": "e0f1a2b3-...",
"title": "Onboarding",
"created_at": "2026-04-29T10:00:00Z",
"last_message_at": null,
"archived_at": null,
"agent_ids": []
}
}
GET /api/projects/{id}/conversations
List the caller's conversations. Auto-filtered by X-USER-ID: Mode 3 sees only that user's threads, Mode 2 sees only external_user_id = NULL threads.
Auth: Project key.
GET /api/projects/3a7b5c1d-.../conversations
Authorization: Bearer jg_p_a1b2c3d4...
X-USER-ID: customer_47291
{
"conversations": [
{
"id": "a1b2c3d4-...",
"project_id": "3a7b5c1d-...",
"external_user_id": "e0f1a2b3-...",
"title": "Onboarding",
"created_at": "2026-04-29T10:00:00Z",
"last_message_at": "2026-04-29T10:05:00Z",
"archived_at": null,
"agent_ids": ["8f4d2e1a-..."]
}
]
}
GET /api/projects/{id}/conversations/{cid}
Fetch one conversation with its full message log.
Auth: Project key.
GET /api/projects/3a7b5c1d-.../conversations/a1b2c3d4-...
Authorization: Bearer jg_p_a1b2c3d4...
X-USER-ID: customer_47291
{
"conversation": { "id": "a1b2c3d4-...", "title": "Onboarding", "agent_ids": ["8f4d2e1a-..."], "...": "..." },
"messages": [
{ "id": "...", "role": "user", "agent_id": null, "content": "What's my portfolio worth?", "created_at": "..." },
{ "id": "...", "role": "assistant", "agent_id": "8f4d2e1a-...", "content": "Your portfolio is currently worth $12,450.", "created_at": "..." }
]
}
PATCH /api/projects/{id}/conversations/{cid}
Update the title and/or archive flag.
Auth: Project key.
PATCH /api/projects/3a7b5c1d-.../conversations/a1b2c3d4-...
Authorization: Bearer jg_p_a1b2c3d4...
Content-Type: application/json
{
"title": "Onboarding (closed)",
"archived": true
}
{ "conversation": { "id": "a1b2c3d4-...", "title": "Onboarding (closed)", "archived_at": "2026-04-29T11:00:00Z", "...": "..." } }
DELETE /api/projects/{id}/conversations/{cid}
Hard-delete the conversation and its messages. 204 No Content.
Auth: Project key.
curl -X DELETE https://api.juglans.ai/api/projects/3a7b5c1d-.../conversations/a1b2c3d4-... \
-H "Authorization: Bearer jg_p_a1b2c3d4..." \
-H "X-USER-ID: customer_47291"
A note on cross-partition access
The handlers deliberately return 404 Not Found (not 403) when a caller asks about a conversation that exists but belongs to a different partition — we don't reveal that someone else owns it. If you're sure a conversation_id is valid and getting 404, double-check whether X-USER-ID matches the value used when the conversation was created.