SendMessage
Sends a message and streams the answer as server-sent events.
The stream ends with exactly one message.completed or message.failed. If the connection
drops, the SDK re-attaches with ResumeConversationStream and Last-Event-ID, so no event is
lost or repeated. Not retried automatically: a send that fails before the stream starts is
reported to the caller.
Not yet available. The SDKs have this operation, but no Caitlyn release serves it yet: a call gets 404. This page describes how it will work. GetAgentVersion is the one v3 operation served today.
| HTTP | POST /api/v3/conversations/{conversationId}/send |
| TypeScript | caitlyn.conversations.send(conversationId, { … }) |
| Python | caitlyn.conversations.send(conversation_id, …) |
| Go | client.Conversations.Send(ctx, conversationID, &caitlyn.SendMessageInput{…}) |
| Rust | client.conversations().send(conversation_id, SendMessageInput { … }) |
| Stability | Generally available |
| Since | Caitlyn 5.115.0 |
| Retried by the SDKs | No: it is not safe to repeat (Retries) |
Request
Section titled “Request”Member names are shown as they are in TypeScript and on the wire. Python and Rust use snake case (knowledge_base_id) and Go uses exported names (KnowledgeBaseID).
conversationId is the method's first argument rather than an input field.
| Member | Type | Sent in | Description |
|---|---|---|---|
conversationId (required) |
string | path | The conversation to send to. 1 to 128 characters. |
message (required) |
string | body | The message, at most 32,000 characters. 1 to 32000 characters. |
context |
list of ContextItem | body | Structured context for this message only, at most 20 items and 16 KB in total. Not stored on the conversation. At most 20 items. |
Response
Section titled “Response”A text/event-stream of ConversationEvent events. The stream ends after message.completed, which the SDKs' finalMessage() (Python final_message(), Go FinalMessage(), Rust final_message()) returns; a failure ends it with message.failed, which the SDKs raise as the typed error. A dropped connection is resumed with ResumeConversationStream and Last-Event-ID, so no event is lost or repeated. See Streaming.
| Event | Data | Description |
|---|---|---|
message.started |
MessageStarted | The answer has started. |
phase.changed |
PhaseChanged | The agent moved to another phase of its workflow. |
progress.updated |
ProgressUpdated | A progress step started, changed or finished. |
text.delta |
TextDelta | A fragment of answer text. The one recorded exception to the noun.past_tense naming rule. |
citation.added |
CitationAdded | A citation was added to the answer. |
tool.started |
ToolStarted | A tool call started. |
tool.finished |
ToolFinished | A tool call finished. |
input.requested |
InputRequested | The agent asked the person for input through a widget. |
message.completed |
MessageCompleted | The answer is complete. Ends the stream. |
message.failed |
MessageFailed | The answer failed. Ends the stream; its data is a problem details object. |
Response headers:
| Member | Type | Sent in | Description |
|---|---|---|---|
requestId |
string | header Caitlyn-Request-Id |
The request's identifier. The same value is in every error body; quote it to support. 1 to 128 characters. |
apiVersion |
string | header Caitlyn-Api-Version |
The server's model version, for example 5.120.0. The SDKs keep the latest value they have seen and refuse to call operations or send members newer than it. 5 to 32 characters. Pattern ^[0-9]+\.[0-9]+\.[0-9]+$. |
Permissions
Section titled “Permissions”- Permission:
chat-session-messages.create. - Checked on: the conversation named by
conversationId. - Who can call it: people (signed in, or a personal access token), API clients (machine sign-in), guests (a guest token).
The checks run in this order: who is calling, the workspace and the permission (each refused with 403), then the record (404, which is also what a record the caller may not see returns). Errors explains why.
Errors
Section titled “Errors”The SDKs raise the error class for the response's HTTP status, whether or not the operation lists it (Errors). The statuses this operation returns:
| Status | Error | Codes | When |
|---|---|---|---|
| 400 | ValidationError |
VALIDATION_FAILED, UNKNOWN_FIELD, UNKNOWN_FILTER, UNKNOWN_SORT, INVALID_CURSOR |
The request is malformed: a member is missing, the wrong type or out of range; it has a member, filter or sort field the operation does not define; or its cursor is not valid for this list. |
| 401 | UnauthenticatedError |
UNAUTHENTICATED |
No credential was sent, or it is expired or not valid. |
| 403 | PermissionDeniedError |
PERMISSION_DENIED, NOT_AVAILABLE_TO_CREDENTIALS, PRIMARY_WORKSPACE_REQUIRED |
The caller may not call this operation: it lacks the permission, its credential type may not call it, or the operation is served only on the organisation's primary workspace. Decided before any record is read. A record the caller may not see is 404 instead. |
| 404 | NotFoundError |
NOT_FOUND |
The record does not exist, or the caller may not see it. The two are deliberately the same, so identifiers cannot be probed. |
| 409 | ConflictError |
CONFLICT, ALREADY_EXISTS, IDEMPOTENCY_KEY_IN_USE, OPERATION_NOT_CANCELLABLE |
The resource's state does not allow the request, a unique name is taken, or a request with the same idempotency key is still in progress. |
| 413 | PayloadTooLargeError |
PAYLOAD_TOO_LARGE |
The body, a file or the send context is over its documented limit. |
| 429 | TooManyRequestsError |
RATE_LIMITED |
Too many requests; wait for Retry-After and try again. Retried by the SDKs. |
| 500 | InternalError |
INTERNAL |
An unexpected server failure. Not retried by the SDKs. |
| 503 | ServiceUnavailableError |
SERVICE_UNAVAILABLE |
A dependency is briefly unavailable, or the request's audit event could not be recorded; wait for Retry-After and try again. Retried by the SDKs. |
Examples
Section titled “Examples”Ask a question
Section titled “Ask a question”The answer arrives as a stream of events; the example shows only the request.
import { Caitlyn } from '@caitlyn-ai/sdk';
const caitlyn = new Caitlyn({ workspace: 'your-company.getcaitlyn.ai', auth: { mode: 'token', token: process.env.CAITLYN_TOKEN! } });
const stream = caitlyn.conversations.send('0193a6f2-7c4e-7b1a-9d2e-3f5a8c1b0e47', { message: 'How much annual leave do part-time staff get?',});for await (const event of stream) { if (event.type === 'text.delta') process.stdout.write(event.delta);}console.log(await stream.finalMessage());import os
from caitlyn import Caitlyn, TextDeltaEventfrom caitlyn.auth import Token
caitlyn = Caitlyn(workspace="your-company.getcaitlyn.ai", auth=Token(os.environ["CAITLYN_TOKEN"]))
with caitlyn.conversations.send( "0193a6f2-7c4e-7b1a-9d2e-3f5a8c1b0e47", message="How much annual leave do part-time staff get?",) as stream: for event in stream: if isinstance(event, TextDeltaEvent): print(event.delta, end="", flush=True) print(stream.final_message())package main
import ( "context" "fmt" "log" "os"
caitlyn "github.com/caitlyn-ai/caitlyn-go")
func main() { ctx := context.Background() client, err := caitlyn.NewClient("your-company.getcaitlyn.ai", caitlyn.Token(os.Getenv("CAITLYN_TOKEN"))) if err != nil { log.Fatal(err) } stream, err := client.Conversations.Send(ctx, "0193a6f2-7c4e-7b1a-9d2e-3f5a8c1b0e47", &caitlyn.SendMessageInput{ Message: "How much annual leave do part-time staff get?", }) if err != nil { log.Fatal(err) } defer stream.Close() for event, err := range stream.Events() { if err != nil { log.Fatal(err) } if delta, ok := event.(*caitlyn.TextDelta); ok { fmt.Print(delta.Delta) } }}use caitlyn_sdk::{Auth, Client, ConversationEvent, SendMessageInput};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let client = Client::new("your-company.getcaitlyn.ai", Auth::token(std::env::var("CAITLYN_TOKEN")?))?; let mut stream = client .conversations() .send("0193a6f2-7c4e-7b1a-9d2e-3f5a8c1b0e47", SendMessageInput { message: "How much annual leave do part-time staff get?".into(), ..Default::default() }) .await?; while let Some(event) = stream.next().await { if let ConversationEvent::TextDelta(delta) = event? { print!("{}", delta.delta); } } Ok(())}The HTTP exchange:
POST /api/v3/conversations/0193a6f2-7c4e-7b1a-9d2e-3f5a8c1b0e47/send HTTP/1.1Host: your-company.getcaitlyn.aiAuthorization: Bearer <token>Accept: text/event-streamContent-Type: application/json
{ "message": "How much annual leave do part-time staff get?"}HTTP/1.1 200Content-Type: text/event-streamA conversation that does not exist, or that the caller may not see
Section titled “A conversation that does not exist, or that the caller may not see”import { Caitlyn, NotFoundError } from '@caitlyn-ai/sdk';
const caitlyn = new Caitlyn({ workspace: 'your-company.getcaitlyn.ai', auth: { mode: 'token', token: process.env.CAITLYN_TOKEN! } });
try { const stream = caitlyn.conversations.send('0193a6f2-0000-7000-8000-000000000000', { message: 'Hello' }); for await (const event of stream) { if (event.type === 'text.delta') process.stdout.write(event.delta); } console.log(await stream.finalMessage());} catch (err) { if (!(err instanceof NotFoundError)) throw err; console.error(err.status, err.code, err.detail, err.requestId);}import os
from caitlyn import Caitlyn, NotFoundError, TextDeltaEventfrom caitlyn.auth import Token
caitlyn = Caitlyn(workspace="your-company.getcaitlyn.ai", auth=Token(os.environ["CAITLYN_TOKEN"]))
try: with caitlyn.conversations.send( "0193a6f2-0000-7000-8000-000000000000", message="Hello", ) as stream: for event in stream: if isinstance(event, TextDeltaEvent): print(event.delta, end="", flush=True) print(stream.final_message())except NotFoundError as err: print(err.status, err.code, err.detail, err.request_id)package main
import ( "context" "errors" "fmt" "log" "os"
caitlyn "github.com/caitlyn-ai/caitlyn-go")
func main() { ctx := context.Background() client, err := caitlyn.NewClient("your-company.getcaitlyn.ai", caitlyn.Token(os.Getenv("CAITLYN_TOKEN"))) if err != nil { log.Fatal(err) } stream, err := client.Conversations.Send(ctx, "0193a6f2-0000-7000-8000-000000000000", &caitlyn.SendMessageInput{ Message: "Hello", }) if err != nil { var apiErr *caitlyn.NotFoundError if errors.As(err, &apiErr) { fmt.Println(apiErr.Status, apiErr.Code, apiErr.Detail, apiErr.RequestID) return } log.Fatal(err) } defer stream.Close() for event, err := range stream.Events() { if err != nil { log.Fatal(err) } if delta, ok := event.(*caitlyn.TextDelta); ok { fmt.Print(delta.Delta) } }}use caitlyn_sdk::{Auth, Client, ConversationEvent, SendMessageInput};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let client = Client::new("your-company.getcaitlyn.ai", Auth::token(std::env::var("CAITLYN_TOKEN")?))?; let outcome: Result<(), caitlyn_sdk::Error> = async { let mut stream = client .conversations() .send("0193a6f2-0000-7000-8000-000000000000", SendMessageInput { message: "Hello".into(), ..Default::default() }) .await?; while let Some(event) = stream.next().await { if let ConversationEvent::TextDelta(delta) = event? { print!("{}", delta.delta); } } Ok(()) } .await; match outcome { Err(caitlyn_sdk::Error::NotFound(err)) => eprintln!("{} {} {:?} {:?}", err.status, err.code, err.detail, err.request_id), other => other?, } Ok(())}The HTTP exchange:
POST /api/v3/conversations/0193a6f2-0000-7000-8000-000000000000/send HTTP/1.1Host: your-company.getcaitlyn.aiAuthorization: Bearer <token>Accept: text/event-streamContent-Type: application/json
{ "message": "Hello"}HTTP/1.1 404Content-Type: application/problem+json
{ "type": "https://build.caitlyn.ai/errors/NOT_FOUND", "title": "Not found", "status": 404, "code": "NOT_FOUND", "requestId": "01JC9Q2W7R4M0H2T6N1S8V3B60"}An answer is already being written
Section titled “An answer is already being written”import { Caitlyn, ConflictError } from '@caitlyn-ai/sdk';
const caitlyn = new Caitlyn({ workspace: 'your-company.getcaitlyn.ai', auth: { mode: 'token', token: process.env.CAITLYN_TOKEN! } });
try { const stream = caitlyn.conversations.send('0193a6f2-7c4e-7b1a-9d2e-3f5a8c1b0e47', { message: 'And for casual staff?', }); for await (const event of stream) { if (event.type === 'text.delta') process.stdout.write(event.delta); } console.log(await stream.finalMessage());} catch (err) { if (!(err instanceof ConflictError)) throw err; console.error(err.status, err.code, err.detail, err.requestId);}import os
from caitlyn import Caitlyn, ConflictError, TextDeltaEventfrom caitlyn.auth import Token
caitlyn = Caitlyn(workspace="your-company.getcaitlyn.ai", auth=Token(os.environ["CAITLYN_TOKEN"]))
try: with caitlyn.conversations.send( "0193a6f2-7c4e-7b1a-9d2e-3f5a8c1b0e47", message="And for casual staff?", ) as stream: for event in stream: if isinstance(event, TextDeltaEvent): print(event.delta, end="", flush=True) print(stream.final_message())except ConflictError as err: print(err.status, err.code, err.detail, err.request_id)package main
import ( "context" "errors" "fmt" "log" "os"
caitlyn "github.com/caitlyn-ai/caitlyn-go")
func main() { ctx := context.Background() client, err := caitlyn.NewClient("your-company.getcaitlyn.ai", caitlyn.Token(os.Getenv("CAITLYN_TOKEN"))) if err != nil { log.Fatal(err) } stream, err := client.Conversations.Send(ctx, "0193a6f2-7c4e-7b1a-9d2e-3f5a8c1b0e47", &caitlyn.SendMessageInput{ Message: "And for casual staff?", }) if err != nil { var apiErr *caitlyn.ConflictError if errors.As(err, &apiErr) { fmt.Println(apiErr.Status, apiErr.Code, apiErr.Detail, apiErr.RequestID) return } log.Fatal(err) } defer stream.Close() for event, err := range stream.Events() { if err != nil { log.Fatal(err) } if delta, ok := event.(*caitlyn.TextDelta); ok { fmt.Print(delta.Delta) } }}use caitlyn_sdk::{Auth, Client, ConversationEvent, SendMessageInput};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let client = Client::new("your-company.getcaitlyn.ai", Auth::token(std::env::var("CAITLYN_TOKEN")?))?; let outcome: Result<(), caitlyn_sdk::Error> = async { let mut stream = client .conversations() .send("0193a6f2-7c4e-7b1a-9d2e-3f5a8c1b0e47", SendMessageInput { message: "And for casual staff?".into(), ..Default::default() }) .await?; while let Some(event) = stream.next().await { if let ConversationEvent::TextDelta(delta) = event? { print!("{}", delta.delta); } } Ok(()) } .await; match outcome { Err(caitlyn_sdk::Error::Conflict(err)) => eprintln!("{} {} {:?} {:?}", err.status, err.code, err.detail, err.request_id), other => other?, } Ok(())}The HTTP exchange:
POST /api/v3/conversations/0193a6f2-7c4e-7b1a-9d2e-3f5a8c1b0e47/send HTTP/1.1Host: your-company.getcaitlyn.aiAuthorization: Bearer <token>Accept: text/event-streamContent-Type: application/json
{ "message": "And for casual staff?"}HTTP/1.1 409Content-Type: application/problem+json
{ "type": "https://build.caitlyn.ai/errors/CONFLICT", "title": "Conflict", "status": 409, "code": "CONFLICT", "detail": "The conversation is still answering the previous message. Wait for it to finish, or resume its stream.", "requestId": "01JC9Q2W7R4M0H2T6N1S8V3B61"}
