prod - 20261103-155809 - acd260dde964f9fc12a4ec4d79828e2feffa7ef1

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 Unauthorized when request authentication fails.
  • 500 Internal Server Error when 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:

  1. Send a connected acknowledgement.
  2. Immediately send the first conversational message.
  3. End the first turn with turn_end so 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:

  1. message_start
  2. One or more message_part events containing final transcript texts
  3. 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_part events 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 more message_part events, message_end , and turn_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 error or ended message 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 connected acknowledgement in the Voice API call. Acknowledge promptly so queued caller messages can be delivered.
  • After Seamly accepts your hangup message, 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 connect before sending connected or conversation content.
  • Send exactly one connected acknowledgement 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_part before message_end .
  • Preserve spaces between streamed message_part values.
  • Send turn_end only 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_id from the HTTP request.
  • Your conversation_id from the HTTP response.
  • Seamly's ULID from the WebSocket connect event.
  • The dialled telephone number and channel name.

Security recommendations

  • Verify the X-Seamly-Signature before 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.