You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I audited the connect.to destination formats documented on the SWML connect reference against the implementation in mod_infrastructure (master @ a8ba2dc, 2026-08-05). The five documented formats are all correct, but the audit turned up one entirely undocumented destination type, one documented parameter that does not exist and hard-fails any script that uses it, and a set of precision gaps that are currently guesswork for users.
Filing this as one ticket since it's all the same page/spec pair.
All five documented formats match the implementation:
Documented format
Detection
Resulting device
Phone number, E.164
fallback branch (swml.c:4355)
phone / to_number
sip:alice@example.com
relay_apis.c:642
sip / to
/public/test_room
relay_apis.c:702
fabric / to
queue:support
relay_apis.c:718
queue / queue_name
stream:wss://example.com/audio
relay_apis.c:750 + swml_schema.c:1036
stream / url
2. Missing: the call:<uuid> destination is undocumented
Document call: as a sixth connect.to destination type
connect accepts to: "call:<call-uuid>" to bridge the current call directly to an existing in-progress call, interrupting whatever that call is doing. It is accepted by the same validator as every other format (swml_schema.c:1054), translated at swml.c:4297, and executed by the relay_intercept app (relay.c:5821). It appears nowhere in the docs or the TypeSpec.
version: 1.0.0sections:
main:
- connect:
to: "call:ba79c846-4834-42b9-9cd3-2f87c30f2972"
Behavior worth documenting:
The call ID must be a well-formed 36-character UUID (relay_apis.c:1038-1040). A malformed value passes script validation but fails at execution time, so the error surfaces later than users expect.
The target call is located across the cluster via the registrar; if it lives on another node the bridge is set up over an internal link (relay.c:5570, relay.c:5917). Cross-node is transparent to the user.
Constraints (each is a distinct, user-visible failure):
Only one destination is allowed — a parallel/serial fan-out containing a call: (or queue:) target is rejected with Only 1 device allowed when connecting to call or queue (relay.c:7643).
A call cannot connect to itself — checked against both call_id and segment_id (relay.c:7666).
The target must belong to the same project (relay.c:5617).
The target must not be cXML/LaML-controlled (relay.c:5626) — returns busy.
call_state_url / call_state_events are rejected at validation time for call: targets (swml_schema.c:1098), and unlike queue and stream there is no alternative webhook.
Failure surfaces in connect_failed_reason as NOT_FOUND (target gone), BUSY (LaML target), or error.
Default post-bridge behavior is that the target leg parks and returns to script control (relay.c:5655).
3. Incorrect: connect.transfer_after_bridge does not exist and breaks scripts
Remove connect.transfer_after_bridge from all four tabs and from the TypeSpec
Document connect.execute_after_queue in its place
transfer_after_bridge is documented as a connect property in all four tabs (index.mdx:125, :356, :566, :778) and in main.tsp:161, with the note "Required when connecting to a queue" (main.tsp:158).
The connect method's accepted-parameter list (swml_schema.c:1645) does not include transfer_after_bridge. Unknown parameters are a hard script error, not a silent no-op — check_unknown_object_params (swml_schema.c:872-884) returns SWML_METHOD_PARAMETER_UNDEFINED. So anyone who follows this documentation gets a failed script, not a working one.
The real parameter is execute_after_queue (swml_schema.c:1645, translated at swml.c:4292), and it is optional, not required. transfer_after_bridge is the internal name this maps to downstream (relay.c:7446), which is presumably how it leaked into the docs. Note that transfer_after_bridgeis correct for the enter_queue method — that page is fine; only connect is wrong.
One caveat for whoever writes the replacement: execute_after_queue is accepted at the verb level only. It is not in the per-destination schema (swml_schema.c:1597), so putting it inside a serial/parallel entry is rejected as an unknown parameter. Since a queue target must be the only destination anyway, the verb-level form is the only one that makes sense — but that should be stated rather than left to be discovered.
4. Precision gaps
E.164 requires a leading +. The validator (cjson_check.c:261) requires +, digits only after it, and total length 5–19. A bare 15552345678 is not treated as a phone number — it matches no prefix, fails the destination validator, and the script errors. "E.164 format" is technically sufficient but is a common trip-up worth one explicit sentence.
sips: is accepted.relay_apis.c:648 accepts both sip: and sips: (case-insensitively). sips: appears nowhere in the docs.
queue: and stream: prefixes are case-sensitive. They use strncmp (relay_apis.c:724, :756), so Queue:support is rejected. sip:/sips: and the wss:// portion of a stream URL are case-insensitive.
stream: requires wss:// — ws:// is rejected.swml_schema.c:1036-1050 enforces TLS. The docs show stream:wss:// in examples but never state that plaintext ws:// is a validation error.
from accepts a narrower set than to.from is validated by is_endpoint_uri (swml_schema.c:1115): phone, SIP, or Resource Address only. queue:, call:, and stream: are not valid from values. The docs describe from only as "Caller ID number", which doesn't convey that it shares a validator with to.
call_state_url / call_state_events are rejected for queue, call, and stream destinations. Not ignored — a validation error (swml_schema.c:1066-1113). The docs list both as general connect properties with no such caveat. The error messages already point users at the right alternative (status_url on enter_queue for queues, the stream's own status_url for streams); the docs should say the same thing up front.
ringback also accepts a boolean. Documented as string[] (index.mdx:105, main.tsp:101-103), but connect accepts a boolean: ringback: false suppresses synthetic ringback and passes the B-leg's early media through to the A-leg (swml.c:4586-4600). This is a genuinely useful and completely undocumented capability. Note it is connect-only — dial accepts only the play form.
A stream: destination must be the only destination. Same one-destination rule as call/queue (relay.c:7474, relay.c:7698).
status_url and queue destinations. The status_url description enumerates behavior for phone/SIP/Resource Address and for stream, but not for queue. Queue lifecycle webhooks go to the status_url configured on enter_queue, not to connect.
5. Related: dial accepts to values it cannot execute
Document the dial destination set, or track the underlying validator mismatch
The SWML dial method validates to with the same permissive validator as connect (swml_schema.c:1655, :1689), but the execution layer for dial accepts only phone, sip, and fabric devices (relay_apis.c:1021-1025). A dial with a queue:, call:, or stream: target therefore passes script validation and fails at execution.
This is arguably an implementation bug rather than a docs bug, and I'm happy to file it against mod_infrastructure instead if that's preferred. Flagging it here because whatever the docs currently imply about dial destinations is wrong either way — the two methods do not accept the same destination types in practice.
Summary
I audited the
connect.todestination formats documented on the SWMLconnectreference against the implementation inmod_infrastructure(master@a8ba2dc, 2026-08-05). The five documented formats are all correct, but the audit turned up one entirely undocumented destination type, one documented parameter that does not exist and hard-fails any script that uses it, and a set of precision gaps that are currently guesswork for users.Filing this as one ticket since it's all the same page/spec pair.
Files in scope
fern/products/swml/pages/reference/methods/calling/connect/index.mdxspecs/swml/calling/Methods/connect/main.tspfern/apis/signalwire-rest/openapi.yaml(generated —transfer_after_bridgeappears at ~35588, 35758, 35928, 36104, 36805)Implementation references (repo:
signalwire/mod_infrastructure)swml.c:4206-4362—create_connect_device(), the destination dispatcherrelay_apis.c:642-757— the prefix predicatesswml_schema.c:1036-1118—is_valid_stream_url,is_connect_to_endpoint_uri,is_endpoint_urirelay.c:5605,relay.c:5821,relay.c:7441— call/queue intercept path1. Confirmed correct (no action needed)
All five documented formats match the implementation:
swml.c:4355)phone/to_numbersip:alice@example.comrelay_apis.c:642sip/to/public/test_roomrelay_apis.c:702fabric/toqueue:supportrelay_apis.c:718queue/queue_namestream:wss://example.com/audiorelay_apis.c:750+swml_schema.c:1036stream/url2. Missing: the
call:<uuid>destination is undocumentedcall:as a sixthconnect.todestination typeconnectacceptsto: "call:<call-uuid>"to bridge the current call directly to an existing in-progress call, interrupting whatever that call is doing. It is accepted by the same validator as every other format (swml_schema.c:1054), translated atswml.c:4297, and executed by therelay_interceptapp (relay.c:5821). It appears nowhere in the docs or the TypeSpec.Behavior worth documenting:
relay_apis.c:1038-1040). A malformed value passes script validation but fails at execution time, so the error surfaces later than users expect.relay.c:5570,relay.c:5917). Cross-node is transparent to the user.parallel/serialfan-out containing acall:(orqueue:) target is rejected withOnly 1 device allowed when connecting to call or queue(relay.c:7643).call_idandsegment_id(relay.c:7666).relay.c:5617).relay.c:5626) — returns busy.call_state_url/call_state_eventsare rejected at validation time forcall:targets (swml_schema.c:1098), and unlike queue and stream there is no alternative webhook.connect_failed_reasonasNOT_FOUND(target gone),BUSY(LaML target), orerror.relay.c:5655).3. Incorrect:
connect.transfer_after_bridgedoes not exist and breaks scriptsconnect.transfer_after_bridgefrom all four tabs and from the TypeSpecconnect.execute_after_queuein its placetransfer_after_bridgeis documented as aconnectproperty in all four tabs (index.mdx:125,:356,:566,:778) and inmain.tsp:161, with the note "Required when connecting to a queue" (main.tsp:158).The
connectmethod's accepted-parameter list (swml_schema.c:1645) does not includetransfer_after_bridge. Unknown parameters are a hard script error, not a silent no-op —check_unknown_object_params(swml_schema.c:872-884) returnsSWML_METHOD_PARAMETER_UNDEFINED. So anyone who follows this documentation gets a failed script, not a working one.The real parameter is
execute_after_queue(swml_schema.c:1645, translated atswml.c:4292), and it is optional, not required.transfer_after_bridgeis the internal name this maps to downstream (relay.c:7446), which is presumably how it leaked into the docs. Note thattransfer_after_bridgeis correct for theenter_queuemethod — that page is fine; onlyconnectis wrong.One caveat for whoever writes the replacement:
execute_after_queueis accepted at the verb level only. It is not in the per-destination schema (swml_schema.c:1597), so putting it inside aserial/parallelentry is rejected as an unknown parameter. Since a queue target must be the only destination anyway, the verb-level form is the only one that makes sense — but that should be stated rather than left to be discovered.4. Precision gaps
+. The validator (cjson_check.c:261) requires+, digits only after it, and total length 5–19. A bare15552345678is not treated as a phone number — it matches no prefix, fails the destination validator, and the script errors. "E.164 format" is technically sufficient but is a common trip-up worth one explicit sentence.sips:is accepted.relay_apis.c:648accepts bothsip:andsips:(case-insensitively).sips:appears nowhere in the docs.queue:andstream:prefixes are case-sensitive. They usestrncmp(relay_apis.c:724,:756), soQueue:supportis rejected.sip:/sips:and thewss://portion of a stream URL are case-insensitive.stream:requireswss://—ws://is rejected.swml_schema.c:1036-1050enforces TLS. The docs showstream:wss://in examples but never state that plaintextws://is a validation error.fromaccepts a narrower set thanto.fromis validated byis_endpoint_uri(swml_schema.c:1115): phone, SIP, or Resource Address only.queue:,call:, andstream:are not validfromvalues. The docs describefromonly as "Caller ID number", which doesn't convey that it shares a validator withto.call_state_url/call_state_eventsare rejected for queue, call, and stream destinations. Not ignored — a validation error (swml_schema.c:1066-1113). The docs list both as generalconnectproperties with no such caveat. The error messages already point users at the right alternative (status_urlonenter_queuefor queues, the stream's ownstatus_urlfor streams); the docs should say the same thing up front.ringbackalso accepts a boolean. Documented asstring[](index.mdx:105,main.tsp:101-103), butconnectaccepts a boolean:ringback: falsesuppresses synthetic ringback and passes the B-leg's early media through to the A-leg (swml.c:4586-4600). This is a genuinely useful and completely undocumented capability. Note it isconnect-only —dialaccepts only the play form.stream:destination must be the only destination. Same one-destination rule as call/queue (relay.c:7474,relay.c:7698).status_urland queue destinations. Thestatus_urldescription enumerates behavior for phone/SIP/Resource Address and for stream, but not for queue. Queue lifecycle webhooks go to thestatus_urlconfigured onenter_queue, not toconnect.5. Related:
dialacceptstovalues it cannot executedialdestination set, or track the underlying validator mismatchThe SWML
dialmethod validatestowith the same permissive validator asconnect(swml_schema.c:1655,:1689), but the execution layer fordialaccepts onlyphone,sip, andfabricdevices (relay_apis.c:1021-1025). Adialwith aqueue:,call:, orstream:target therefore passes script validation and fails at execution.This is arguably an implementation bug rather than a docs bug, and I'm happy to file it against
mod_infrastructureinstead if that's preferred. Flagging it here because whatever the docs currently imply aboutdialdestinations is wrong either way — the two methods do not accept the same destination types in practice.