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 the project_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 without X-USER-ID. Visible to other "no user" callers and to the project owner via JWT.
  • Project, external user — created by a jg_p_* call carrying X-USER-ID. Visible only to calls bearing the same project key + same X-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, or conversation_id doesn't belong to the caller's partition.
  • 429 — project rate limit exceeded; body adds retry_after_seconds and limit_rpm alongside error.

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.