Text-to-voice
Architecture
The integration consists of an HTTP setup phase followed by a real-time WebSocket phase.
sequenceDiagram
participant Caller
participant Seamly
participant Client
Caller->>Seamly: calls
Seamly->>Client: POST /api/conversations
Client-->>Seamly: 201 + call configuration
Seamly->>Client: open WebSocket
Seamly->>Client: { "type": "connect", ... }
Client-->>Seamly: { "type": "connected" }
Client-->>Seamly: first message and turn_end
Seamly-->>Caller: TTS audio
Caller->>Seamly: speaks
Seamly->>Client: transcribed message events
Client-->>Seamly: response message events
Seamly->>Client: { "type": "ended" }
Integration flow
Call start
When an incoming call is routed to your integration, Seamly sends an authenticated request to your conversation endpoint before the conversation starts.
POST /api/conversations
Content-Type: application/json
X-Seamly-Signature: <HMAC signature>
Request body
{
"project_id": "c8dc0d63-19be-479e-b89c-c99276b7e023",
"channel_name": "voice",
"phone_number": "+31881111111",
"environment": {
"from_phone_anonymous": false,
"from_phone_number": "+31612345678"
}
}
| Property | Type | Required | Description |
|---|---|---|---|
| project_id | string | Yes | UUID of the Seamly project. Use this to identify the relevant tenant or configuration in your system. |
| channel_name | string | Yes |
Name of the Seamly channel through which the call arrived. This is normally
voice
.
|
| phone_number | string | Yes | Dialled number in international E.164 format. |
| environment | object | Yes | Information about the caller and call environment. Additional properties may be added in the future. |
| environment.from_phone_anonymous | boolean | Yes | Whether the caller withheld their telephone number. |
| environment.from_phone_number | string or null | Yes |
Caller's telephone number in E.164 format, or
null
when unavailable.
|
The request schema permits additional properties at the top level and inside
environment
. Your implementation should ignore unknown properties rather than reject the request.
Successful response
Return HTTP
201 Created
with a JSON body that configures the conversation.
{
"type": "text",
"context": {
"variables": {
"customer_id": "C-10482",
"customer_name": "Sam Jones"
}
},
"conversation_id": "client-conversation-7f31a4",
"language": "en-GB",
"language_detection": {
"language_candidates": ["en-GB", "nl-NL"]
},
"participant": {
"name": "Sam Jones"
},
"speech_synthesis": {
"voice": "TuUL8QNimlzrTWC5Op2c",
"rate": 1.0
},
"user_silence_timeout": 30,
"_links": {
"socket": {
"href": "wss://api.acme.com/conversations/client-conversation-7f31a4/websocket"
}
}
}
Response properties
| Property | Type | Required | Description |
|---|---|---|---|
| type | string | Yes | Either text or audio. For an audio integration see Voice-to-voice . |
| conversation_id | string | Yes | Conversation ID in your system. Use it to correlate the HTTP request, WebSocket connection, logs, and business records. |
| language | string | Yes | Initial language of the call. |
| participant.name | string | Yes | Display name used for the participant in Seamly conversation history. |
| speech_synthesis.voice | string | Yes | ElevenLabs voice ID used for TTS. |
| speech_synthesis.rate | number | No |
Speaking-rate multiplier.
1.0
is normal speed; lower values are slower and higher values are faster.
|
| _links.socket.href | URI | Yes | Secure WebSocket URL Seamly must connect to for this conversation. |
| context.variables | object | No |
Arbitrary conversation context. Seamly returns these variables in the initial WebSocket
connect
event.
|
| language_detection.language_candidates | array | No |
Between two and four candidate languages. Include the value supplied in
language
. Detection applies to the caller's initial two turns.
|
| user_silence_timeout | integer | No |
Number of seconds of caller silence after which Seamly hangs up. Allowed range:
1
to
300
. The default is
30
.
|
The response schema does not allow unknown top-level properties or unknown properties inside the defined nested objects, except for context.variables, which may contain arbitrary conversation-specific key-value pairs.
Error responses
The specification defines the following error responses:
-
401 Unauthorizedwhen request authentication fails. -
500 Internal Server Errorwhen your application cannot create or configure the conversation.
Return errors in this format:
{
"error": "Human-readable error message"
}
Accept the WebSocket connection
After validating the HTTP response, Seamly connects to the URL in
_links.socket.href
using a secure WebSocket connection.
Your WebSocket URL may contain a path parameter, signed token, or other correlation value needed by your application.
All messages are JSON objects with a
type
property.
Complete the connection handshake
As soon as the WebSocket is established, Seamly sends a
connect
event.
{
"type": "connect",
"context": {
"variables": {
"customer_id": "C-10482",
"customer_name": "Sam Jones"
}
},
"conversation_id": "client-conversation-7f31a4",
"seamly_conversation_id": "01KY4HW2CESEDC0SQRRMQE0WPA"
}
After receiving
connect
for the initial connection, your application must:
-
Send a
connectedacknowledgement. - Immediately send the first conversational message.
-
End the first turn with
turn_endso Seamly knows the caller may respond.
{ "type": "connected" }
For subsequent connections to the same conversation, acknowledge
connect
and resume the conversation as described in WebSocket reconnection.
Exchange conversational messages
A conversational turn contains one or more messages. Each message consists of:
-
message_start -
One or more
message_partevents containing final transcript texts -
message_end
After all messages in the turn have been sent, send
turn_end
.
Complete message example
{ "type": "message_start", "interruptible": true, "language": "en-GB" }
{ "type": "message_part", "text": "Hello, " }
{ "type": "message_part", "text": "how may I help you?" }
{ "type": "message_end" }
{ "type": "turn_end" }
Seamly concatenates the text from consecutive
message_part
events. Include spaces and punctuation exactly where they belong. In the example above, the trailing space in
"Hello, "
is significant.
message_start
{
"type": "message_start",
"interruptible": true,
"language": "en-GB"
}
| Property | Description |
|---|---|
| interruptible | When your application sends the message, determines whether the caller may interrupt its playback. |
| language |
Locale of the message. When language detection is enabled, Seamly reflects the detected caller language in incoming
message_start
events.
|
Although both fields are required by the schema in both directions,
interruptible
only affects messages sent from your application to Seamly.
message_part
{
"type": "message_part",
"text": "Your streamed text"
}
A part may contain a complete message, a sentence, or even a single word. Sending smaller parts allows Seamly to start generating audio with TTS and playing it while your application is still generating the rest of the response.
message_end
{ "type": "message_end" }
Send this after one or more
message_part
events. Follow it with another
message_start
when the same turn contains another message, or with
turn_end
when the turn is complete.
turn_end
{ "type": "turn_end" }
This event transfers control to the other party. Do not wait for the caller's next message until the current outgoing turn has been closed with
turn_end
.
Handle interruptions and committed text
When an outgoing message is interruptible, the caller may begin speaking before playback has finished. Seamly then sends
commit_turn
as soon as the interruption is detected.
{
"type": "commit_turn",
"text": "Hello, how may I "
}
The
text
value contains the portion the caller actually heard. Treat this value as the committed assistant output when updating your conversation history.
After receiving
commit_turn
:
-
Stop sending any remaining
message_partevents for the interrupted message. -
Do not send its pending
message_end. -
Wait for the caller's message sequence, which follows as
message_start, one or moremessage_partevents,message_end, andturn_end.
Your implementation should be able to cancel or disregard in-progress generation promptly when this event arrives.
End a call
Client-initiated hang-up
To instruct Seamly to end the call, send:
{ "type": "hangup" }
In a bit Seamly will send and "ended" message and will close the WebSocket.
Call transfer
Transferring a call is not available yet. Do not use it until Seamly confirms support.
Seamly call-ended notification
At the end of the call, Seamly sends:
{ "type": "ended" }
If Seamly has recorded the conversation, the ended message will contain the URL of the recording:
{
"type": "ended",
"recording": {
"wav_file": "https://api.seamly-app.com/service-api/client/7cb4788f-1fd6-4bac-a280-c87d354b5058/conversations/01KY4HW2CESEDC0SQRRMQE0WPA/recording"
}
}
This recording is available for 7 days and can be retrieved by sending a GET request to the
wav_file
URL. The request must be signed by setting the
X-Seamly-Signature
request header. The signing method is identical to how Seamly signs its requests. See the HMAC signature above for more information.
After receiving
ended
, finalise your conversation state and allow the WebSocket connection to close.
Report errors
Either side may send an
error
event:
{
"type": "error",
"reason": "validation_error",
"description": "Unexpected field in message"
}
| Property | Description |
|---|---|
| reason | Stable, machine-readable, atom-like error reason. |
| description | Human-readable explanation suitable for diagnostics. |
Log both fields together with the conversation ID and Seamly conversation ULID. Do not include secrets or unnecessary personal data.
Error handling
Seamly reports WebSocket protocol errors to your application and attempts to reconnect when an established connection is lost. You cannot currently configure recovery from unrecoverable failures. If Seamly cannot recover, the call ends without an error message being spoken to the caller.
Conversation setup failures
Your HTTP endpoint must return
201 Created
with a valid conversation configuration. HTTP failures, invalid JSON or response fields, unavailable languages, and invalid speech settings prevent Seamly from starting the conversation. Your WebSocket URL must use
wss://
.
Seamly cannot currently report these setup failures back to you through the Voice API. Setup also fails if Seamly cannot establish the initial WebSocket connection. The reconnection behaviour below applies only after a WebSocket connection has been established.
WebSocket protocol errors
If your application sends a JSON message that fails validation or violates the expected message sequence, Seamly responds with an
error
message:
{
"type": "error",
"reason": "unexpected_message",
"description": "Unexpected message_part message, send a 'connected' message first"
}
| Reason | Examples | Handling |
|---|---|---|
| validation_error | Invalid message fields, an invalid language tag, or a language that is not enabled for your project | Seamly rejects the message and keeps the connection open. |
| unexpected_message |
Sending messages before
connected
, sending
message_part
without an open message, sending empty or whitespace-only text, or ending a turn before ending its message
|
Seamly rejects the message and keeps the connection open. |
Correct the rejected message and continue from the last accepted message in the sequence. An error does not reset the current message or turn. If you send malformed JSON, Seamly closes the WebSocket connection and attempts reconnection instead of returning an
error
message.
After you have sent the
connected
acknowledgement, Seamly only logs valid
error
messages sent by your application. Sending an
error
does not trigger recovery, end the call, reset the current turn, or produce speech for the caller. To explain a failure to the caller while the connection is available, send a conversational message using the normal message sequence. You can then send
hangup
to end the call.
WebSocket reconnection
Your WebSocket endpoint must accept new connections for an existing conversation. When an established connection is lost, Seamly attempts to reconnect to the same URL and sends a new
connect
message containing both conversation IDs and the current context variables. Use these IDs to identify the existing conversation and acknowledge the connection with
connected
. Resume the conversation rather than creating a new one or repeating the initial greeting.
Account for the following behaviour in your integration:
- Seamly attempts reconnection immediately, allowing up to three reconnect attempts within a five-second window. A later disconnection outside that window starts a new retry budget.
-
If a reconnect attempt fails to establish a connection, or the retry budget is exhausted, Seamly stops trying to reconnect and ends the call. Do not rely on receiving a final
errororendedmessage over the unavailable connection. -
Seamly queues caller messages received while disconnected and sends them after you acknowledge the new connection with
connected. -
There is no explicit timeout for your
connectedacknowledgement in the Voice API call. Acknowledge promptly so queued caller messages can be delivered. -
After Seamly accepts your
hangupmessage, it does not reconnect when the socket closes.
Message delivery during reconnection
A successful reconnection does not guarantee that all messages were delivered. Seamly does not automatically replay WebSocket frames it already sent, and a connection can fail partway through a streamed message or turn. Your application may therefore receive an incomplete message or turn. Likewise, do not assume that Seamly received every frame you sent just before the disconnection.
WebSocket event reference
Events sent by Seamly to your application
| Event | Purpose |
|---|---|
| connect | Initiates the application-level connection and supplies context plus Seamly's and your application side's conversation identifiers. |
| message_start | Starts a caller message. |
| message_part | Supplies part of the caller's transcribed text. |
| message_end | Marks the end of one caller message. |
| turn_end | Marks the end of the caller's turn. |
| commit_turn | Reports how much of an interruptible message the caller actually heard. |
| ended | Reports that the call has ended. |
| error | Reports a protocol or processing error. |
Events sent by your application to Seamly
| Event | Purpose |
|---|---|
| connected |
Acknowledges Seamly's
connect
event.
|
| message_start | Starts an outgoing message and defines its language and interruptibility. |
| message_part | Supplies all or part of the text Seamly should convert into audio using TTS |
| message_end | Marks the end of one outgoing message. |
| turn_end | Marks the end of your applications turn. |
| hangup | Instructs Seamly to end the call. |
| transfer | Reserved for future call-transfer support; currently unavailable . |
| error | Reports a protocol or processing error. |
Message sequencing rules
Your implementation should enforce these rules:
-
Wait for
connectbefore sendingconnectedor conversation content. -
Send exactly one
connectedacknowledgement for each WebSocket connection. -
On the initial connection, send the first conversational message immediately after
connected. After reconnection, resume the existing conversation without repeating the greeting. -
Begin every text message with
message_start. -
Send at least one
message_partbeforemessage_end. -
Preserve spaces between streamed
message_partvalues. -
Send
turn_endonly after all messages in the turn have ended. -
Stop sending an interruptible message after receiving
commit_turn.
Language handling
Return a valid initial locale in the HTTP
language
property. Seamly uses that locale for STT and TTS configuration.
To enable language detection, return
language_detection.language_candidates
containing between two and four locales. The initial
language
must also be included in that list. Seamly applies detection to the caller's first two turns and reports the active language in incoming
message_start
events.
TTS and streaming
Seamly currently uses ElevenLabs EU for TTS. Return an ElevenLabs voice ID from the EU residency in
speech_synthesis.voice
. You may also return a
rate
multiplier.
For responsive conversations, stream generated output using multiple
message_part
events. Seamly can begin speaking before your model has generated the entire response. Choose part boundaries that preserve correct spacing and natural pronunciation.
When sending content that must not be interrupted, set
interruptible
to
false
. Follow it with a separate message marked
interruptible: true
when only part of a turn must be protected from interruption.
Context and correlation
Use
context.variables
for information your application wants Seamly to carry into the WebSocket session, such as a customer identifier, CRM lookup result, routing decision, or short-lived verification value.
Do not use context variables for secrets that do not need to leave your system. Treat all context as conversation data that may appear in operational logs or conversation records.
For reliable diagnostics, correlate:
-
project_idfrom the HTTP request. -
Your
conversation_idfrom the HTTP response. -
Seamly's ULID from the WebSocket
connectevent. - The dialled telephone number and channel name.
Security recommendations
-
Verify the
X-Seamly-Signaturebefore processing a conversation request. - Compare signatures using a constant-time comparison function.
- Reject malformed JSON and invalid message sequences.
- Return a short-lived credential or signed value as a variable in your API endpoint and validate the variable when Seamly sends the connect WebSocket message.
Specification files
This guide describes the behaviour defined by:
- OpenAPI — HTTP conversation creation and configuration.
- AsyncAPI — WebSocket connection and message events.
Treat the machine-readable specifications as the authoritative schema definitions. This guide explains how to combine them into a working integration and highlights operational details that must be agreed with Seamly during onboarding.