Streaming Workflows
Manage streaming workflows and their long-lived sessions (voice, telephony, WebRTC, WebSocket pipelines). Streaming workflows are the same workflows documents as batch; the workflowType: "streaming" filter selects them. A session runs either on the workflow's production deployment or, as a draft, on the caller's own test worker, and exposes its bidirectional frame stream over the WebSocket sub-resource documented at the end of this page. For building and deploying streaming workflows in the UI, see Streaming Workflows.
All endpoints require authentication via X-API-Key header and the appropriate scope.
Streaming Workflow Object
{
"_id": "wf_abc123",
"name": "voice-intake",
"description": "Voice agent that triages inbound calls.",
"workflowType": "streaming",
"status": "active",
"version": 1,
"ownerId": "user_456",
"ownerName": "Jane Smith",
"organizationId": "org_xyz",
"sharedWith": [],
"isPublic": false,
"isTemplate": false,
"nodes": [ /* node graph */ ],
"connections": [ /* connections */ ],
"scopes": {},
"settings": {
"autoSave": true,
"timeout": 300000,
"retryAttempts": 3,
"enableLogging": true,
"logLevel": "info",
"recording": { "enabled": false }
},
"tags": [],
"createdAt": "2026-04-10T09:00:00Z",
"lastUpdated": "2026-05-12T14:22:00Z"
}
This is the same document the Workflows endpoints return, with workflowType: "streaming".
Streaming Session Object
{
"_id": "sess_abc123",
"sessionId": "sess_abc123",
"deploymentId": "dep_xyz",
"workflowId": "wf_abc123",
"workflowVersionId": "ver_003",
"organizationId": "org_xyz",
"userId": "user_456",
"status": "active",
"startedAt": "2026-05-16T09:30:00Z",
"connectedAt": "2026-05-16T09:30:02Z",
"endedAt": null,
"durationSeconds": null,
"turnCount": 4,
"audioSecondsIn": 42.5,
"audioSecondsOut": 38.1,
"summary": null,
"disposition": null,
"transcriptLength": 8,
"errorCount": 0,
"fatalError": null,
"failureReason": null,
"idleTimeoutSeconds": 300,
"maxDurationSeconds": 3600,
"correlationId": "corr_789",
"metadata": { "callerId": "+15551234567" },
"createdAt": "2026-05-16T09:30:00Z",
"updatedAt": "2026-05-16T09:31:10Z"
}
status is one of pending, scheduling, ready, connecting, active, draining (not finished) or ended, failed, orphaned (finished). A failed session's failureReason says why, and the last state_history entry carries the full cause. deployment_failed means the session's pod could never start (for example, Kubernetes reports CreateContainerConfigError or ImagePullBackOff). The session fails as soon as its deployment does, rather than waiting in pending. The WebSocket URL and token are returned only when the session is started (see POST /streaming-sessions); they are not part of the session record.
GET /api/v1/streaming-workflows
List streaming workflows accessible to the caller (owned, shared, or open to all users within the org).
Scope: streaming-workflows:read
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | No | Search by name or description |
status | string | No | Filter by workflow status |
limit | integer | No | Max results (default 50, max 200) |
cursor | string | No | meta.nextCursor of the previous page; omit for the first page |
sort | string | No | Sort field(s), -prefix for descending |
Response 200 OK
{
"data": [ /* Streaming Workflow objects */ ],
"meta": {
"total": 4,
"limit": 50,
"nextCursor": null,
"requestId": "req_abc123"
}
}
GET /api/v1/streaming-workflows/:id
Get a single streaming workflow by ID.
Scope: streaming-workflows:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workflow ID |
Response 200 OK
Returns the full Streaming Workflow object.
GET /api/v1/streaming-workflows/:id/deployments
List deployments for a streaming workflow, most recently updated first. A deployment's status is one of deploying, deployed, undeploying, undeployed or failed; the workflow's production deployment is the newest row with ephemeral not true (ephemeral: true rows are builder test workers).
Scope: streaming-workflows:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workflow ID |
Response 200 OK
{
"data": [
{
"_id": "streaming-wf_abc123",
"deploymentId": "streaming-wf_abc123",
"workflowId": "wf_abc123",
"workflowName": "voice-intake",
"status": "deployed",
"resources": { "cpu": "1", "memory": "2Gi", "disk": "5Gi", "gpu": "0", "gpuType": null },
"idleTimeoutSeconds": 300,
"maxSessionDurationSeconds": 3600,
"maxConcurrentSessions": 10,
"autoscaling": {
"enabled": true,
"minReplicas": 0,
"maxReplicas": 4,
"targetSessionsPerPod": 4,
"scaleDownCooldownSeconds": 300
},
"activeSessionCount": 2,
"totalSessionCount": 37,
"lastDeployedAt": "2026-05-12T14:22:00Z",
"createdAt": "2026-05-12T14:22:00Z",
"updatedAt": "2026-05-12T14:23:10Z"
}
],
"meta": {
"requestId": "req_abc123"
}
}
GET /api/v1/streaming-workflows/:id/sessions
List streaming sessions for a workflow.
Scope: streaming-sessions:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workflow ID |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | Filter by session status. Pass active to match any non-terminal state (pending / scheduling / ready / connecting / active / draining). |
limit | integer | No | Max results |
cursor | string | No | meta.nextCursor of the previous page; omit for the first page |
sort | string | No | Sort field(s), -prefix for descending |
Response 200 OK
{
"data": [ /* Streaming Session objects */ ],
"meta": {
"total": 12,
"limit": 50,
"nextCursor": null,
"requestId": "req_abc123"
}
}
POST /api/v1/streaming-workflows/:id/deploy
Deploy a streaming workflow. The graph is validated first (one trigger, at least one response, required inputs and connectors wired, every connection between declared ports that share a frame type); a graph that fails is refused with the problems listed. The call then returns as soon as the deploy is queued, and the deployment proceeds in the background. Poll GET /streaming-workflows/:id/deployments (or the statusUrl in the response) until the deployment's status is deployed.
Settings the call leaves out keep the values of the workflow's previous deploy, else the defaults below.
Scope: streaming-workflows:deploy
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workflow ID |
Request Body
{
"cpu": "1",
"memory": "2Gi",
"disk": "5Gi",
"gpu": "0",
"idleTimeoutSeconds": 300,
"maxSessionDurationSeconds": 3600,
"maxConcurrentSessions": 10,
"autoscaling": {
"enabled": true,
"minReplicas": 0,
"maxReplicas": 4,
"targetSessionsPerPod": 4,
"scaleDownCooldownSeconds": 300
}
}
| Field | Type | Required | Description |
|---|---|---|---|
cpu | string | No | Worker CPU (e.g. 1). Give it with memory; omit both to size the worker from the nodes in the graph |
memory | string | No | Worker memory (e.g. 2Gi), with cpu |
disk | string | No | Worker disk (e.g. 5Gi) |
gpu | string | No | GPU count as a string (e.g. "1") |
gpuType | string | No | GPU type, when gpu is more than 0 |
idleTimeoutSeconds | integer | No | A session with no activity this long is ended. 60 to 86400, default 300 |
maxSessionDurationSeconds | integer | No | A session is ended at this length. 300 to 604800, default 3600 |
maxConcurrentSessions | integer | No | Most sessions at once; a start beyond it is refused. 1 to 10000, default no limit |
autoscaling | object | No | Scaling, below. Default: enabled: false |
autoscaling
| Field | Type | Description |
|---|---|---|
enabled | boolean | false: one worker, started by the first session and stopped after the cooldown once the last session ends. true: minReplicas to maxReplicas workers, one per targetSessionsPerPod sessions |
minReplicas | integer | 0 to 20, default 0. 0 stops every worker when there are no sessions; the next session starts one |
maxReplicas | integer | 1 to 50, at least minReplicas, default 10 |
targetSessionsPerPod | integer | 1 to 100, default 10 |
scaleDownCooldownSeconds | integer | 60 to 900, default 300 |
Response 202 Accepted
The workflow, as GET /streaming-workflows/:id shows it, deploymentStatus queued. Poll its deployments until a replica is ready.
POST /api/v1/streaming-workflows/:id/undeploy
Undeploy a streaming workflow.
Scope: streaming-workflows:deploy
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workflow ID |
Response 200 OK
The workflow, as GET /streaming-workflows/:id shows it, back to draft.
POST /api/v1/streaming-sessions
Start a streaming session. A production session runs the workflow's deployed version on its deployment, as the user who deployed it. A draft session runs the workflow's saved definition on the caller's own test worker (the builder's Run), as the caller; it never uses the deployment. Without mode, a deployed workflow's session is production and an undeployed workflow's is draft. Starting a production session for a workflow that is not deployed is refused, as is a session beyond the deployment's maxConcurrentSessions.
Scope: streaming-sessions:write
Request Body
{
"workflowId": "wf_abc123",
"metadata": { "callerId": "+15551234567" },
"dispositionSchemaVersion": "v1",
"estimatedDurationSeconds": 600,
"correlationId": "corr_789"
}
| Field | Type | Required | Description |
|---|---|---|---|
workflowId | string | Yes | Workflow to run |
mode | string | No | production or draft, see above |
metadata | object | No | Free-form metadata attached to the session |
dispositionSchemaVersion | string | No | Disposition schema version the caller expects |
correlationId | string | No | Correlation ID for tracing |
Response 201 Created
{
"data": {
"sessionId": "sess_abc123",
"wsUrl": "wss://app.strongly.ai/api/v1/streaming/sessions/sess_abc123/<session token>/ws",
"wsToken": "<session token>"
},
"meta": {
"requestId": "req_abc123"
}
}
wsUrl is the WebSocket address for the session, with wsToken (a short-lived session token) already in its path. When the caller is an app or a workflow running on the platform, wsUrl is the platform's internal address, which the caller can reach directly, and the response also carries publicWsUrl, the public address to hand to the caller's own browser users or a phone carrier. Connect as described in WebSocket -- /api/v1/streaming/sessions/:sessionId/:wsToken/ws.
GET /api/v1/streaming-sessions
List streaming sessions accessible to the caller.
Scope: streaming-sessions:read
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
workflowId | string | No | Filter by workflow ID |
status | string | No | Filter by status. Pass active to match any non-terminal state. |
limit | integer | No | Max results |
cursor | string | No | meta.nextCursor of the previous page; omit for the first page |
sort | string | No | Sort field(s), -prefix for descending |
Response 200 OK
{
"data": [ /* Streaming Session objects */ ],
"meta": {
"total": 5,
"limit": 50,
"nextCursor": null,
"requestId": "req_abc123"
}
}
GET /api/v1/streaming-sessions/:id
Get a single streaming session.
Scope: streaming-sessions:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Response 200 OK
Returns the full Streaming Session object. Returns 404 not-found for a session the caller cannot access, so the response never reveals whether another organization's session exists.
GET /api/v1/streaming-sessions/:id/handoffs
List the multi-agent handoff history for a session (read-only). The caller must have access to the session.
Scope: streaming-sessions:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Response 200 OK
{
"data": {
"handoffs": [
{
"_id": "ho_abc123",
"sessionId": "sess_abc123",
"fromAgent": "triage",
"toAgent": "billing",
"reason": "billing question",
"startedAt": "2026-05-16T09:31:00Z",
"completedAt": "2026-05-16T09:31:02Z"
}
]
},
"meta": {
"requestId": "req_abc123"
}
}
DELETE /api/v1/streaming-sessions/:id
End a streaming session.
Scope: streaming-sessions:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Response 204 No Content
POST /api/v1/streaming-sessions/:id/inject
Inject a text message into a running session -- useful for surfacing operator messages, simulated user turns, or out-of-band instructions on top of the live audio/video stream.
Scope: streaming-sessions:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Request Body
{
"text": "Please ask the caller for their account number.",
"role": "system"
}
| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Message body to inject |
role | string | No | Role to attribute the message to (e.g. system, user, assistant) |
Response 200 OK
{
"data": {
"sessionId": "sess_abc123",
"podStatus": 200,
"body": {
"success": true,
"injected": "Please ask the caller for their account number."
}
},
"meta": {
"requestId": "req_abc123"
}
}
GET /api/v1/streaming-sessions/:id/transcript
Paginated transcript for a session, sorted oldest-first.
Scope: streaming-sessions:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Max results |
cursor | string | No | meta.nextCursor of the previous page; omit for the first page |
Response 200 OK
{
"data": [
{
"_id": "txn_abc",
"sessionId": "sess_abc123",
"role": "user",
"text": "I'd like to check my balance.",
"emittedAt": "2026-05-16T09:30:05Z"
}
],
"meta": {
"total": 38,
"limit": 50,
"nextCursor": null,
"requestId": "req_abc123"
}
}
GET /api/v1/streaming-sessions/:id/errors
List streaming errors recorded for a session, oldest-first.
Scope: streaming-sessions:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Response 200 OK
{
"data": [
{
"_id": "err_abc",
"sessionId": "sess_abc123",
"code": "stt.timeout",
"message": "STT call exceeded watchdog",
"nodeId": "stt-1",
"timestamp": "2026-05-16T09:30:08Z"
}
],
"meta": {
"requestId": "req_abc123"
}
}
WebSocket -- /api/v1/streaming/sessions/:sessionId/:wsToken/ws
The session is the resource; /ws is the sub-resource that initiates a WebSocket upgrade. Clients connect only to this public endpoint, never to the session's pod directly. The endpoint authenticates the upgrade, then passes frames in both directions with no transcoding.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId | string | Yes | Session ID ([A-Za-z0-9_-]{1,128}) |
wsToken | string | Yes | The wsToken returned by POST /api/v1/streaming-sessions |
Authentication
The session token travels in the URL path, not in a header or query string, so the same URL works for browsers (whose WebSocket constructor cannot set headers), programmatic clients and telephony carriers that strip query strings. Use the wsUrl returned by POST /api/v1/streaming-sessions as is:
wss://<your-instance>/api/v1/streaming/sessions/<sessionId>/<wsToken>/ws
The wsToken is short-lived and valid only for this session.
Resume Cursor
After a disconnect, clients may pass ?resume_from=<watermark> to ask the upstream pod to replay buffered JSON frames from that watermark. The cursor must be a numeric string of 1 to 19 digits; malformed values are ignored.
Frame Protocol
The connection carries raw frames in both directions with no transcoding. The Strongly streaming frame schema (audio, transcript, tool-call, error, disposition, etc.) is defined alongside the pipeline node catalog; see the streaming pipeline reference for the full frame roster. Keepalive: WebSocket pings are sent every 15 seconds so an idle connection stays open.
Close Codes
When the session side closes, the client connection is closed with the same code and reason.
| Code | Meaning | Client Behavior |
|---|---|---|
1000 | Normal closure (session ended cleanly) | Done. |
1011 | The connection to the session failed after the upgrade | Do not retry on this connection. |
1012 | Session was reassigned to a different pod | Reconnect -- retryable. |
1013 | Session capacity reached or cold start in progress | Reconnect with exponential backoff -- retryable. |
During the initial connect the upgrade is held open while the session's pod starts (from zero this can take minutes): 1006, 1011, 1012 and 1013 from the pod side are retried for you for as long as the session is still waiting for its pod (pending, scheduling, ready or connecting). Clients only see these codes if they occur after the upgrade is live.
Pre-Upgrade Failure Responses
The upgrade may fail before the WebSocket is established. These come back as plain HTTP responses:
| Status | Meaning |
|---|---|
401 Unauthorized | Malformed path, or missing or malformed wsToken |
404 Not Found | Unknown session, or session is in a terminal state (only pending / scheduling / ready / connecting / active can be connected to) |
500 Internal Server Error | The session lookup failed unexpectedly |
502 Bad Gateway | Connecting to the session failed |
503 Service Unavailable | The session stopped waiting for its pod before it connected: it failed or ended (read GET /streaming-sessions/:id for failureReason), or the pod side refused the connection |
GET /api/v1/streaming-sessions/:id/status
Whether the deployment behind a session is ready: sessionId, session (status,
workflowId, deploymentId, startedAt, endedAt) and deployment (its deploymentId,
deploymentName, namespace, status and health). The session is ready once
deployment.health.replicas.ready is at least 1.
Scope: streaming-sessions:read
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session id |
Response 200 OK
GET /api/v1/streaming-sessions/:id/logs
The latest log lines of the pod behind a session.
Scope: streaming-sessions:read
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session id |
lines | integer | No | How many lines (at most 2000) |
container | string | No | streaming-worker (default), auth-proxy or egress-proxy |
Response 200 OK: the log lines.
GET /api/v1/streaming-sessions/:id/recordings
A session's recordings, when it ran with settings.recording.enabled: each one's audio,
transcript and manifest keys and their bucket.
Scope: streaming-sessions:read
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session id |
limit | integer | No | Results per page |
cursor | string | No | meta.nextCursor of the previous page |
Response 200 OK: a page of recordings.