—— PROGRAMMABLE UNIFIED COMMUNICATIONS

Your application instructs calls

Your application instructs calls

A CPaaS hands you events and makes your code track the call. SignalWire owns the state, lifecycle, and routing of every interaction so your application instructs call behavior instead of rebuilding the machinery underneath it.

A CPaaS hands you events and makes your code track the call. SignalWire owns the state, lifecycle, and routing of every interaction so your application instructs call behavior instead of rebuilding the machinery underneath it.

redirect.py — live call, owned by the platform

copy

01

02

03

04

05

06

07

08

09

10

11

12

from signalwire.relay import RelayClient

client = RelayClient(project="...", token="...", contexts=["default"])

@client.on_call
async def handle(call):
    await call.answer()

    # The call is live and the platform owns its state. You command it over one
    # persistent connection, no webhook round-trip, no state you rebuilt yourself.

    # Speak a prompt into the live call
    await call.play([{"type": "tts",
                      "params": {"text": "One moment while I pull up your account."}}])

    # Transfer it to a human, with call state intact
    await call.transfer("https://example.com/agents/support-human")

client.run()

redirect.py

copy

01

02

03

04

05

06

07

08

09

10

11

12

from signalwire.relay import RelayClient

client = RelayClient(project="...", token="...", contexts=["default"])

@client.on_call
async def handle(call):
    await call.answer()

    # The call is live and the platform owns its state. You command it over one
    # persistent connection, no webhook round-trip, no state you rebuilt yourself.

    # Speak a prompt into the live call
    await call.play([{"type": "tts",
                      "params": {"text": "One moment while I pull up your account."}}])

    # Transfer it to a human, with call state intact
    await call.transfer("https://example.com/agents/support-human")

client.run()

redirect.py — live call, owned by the platform

copy

01

02

03

04

05

06

07

08

09

10

11

12

from signalwire.relay import RelayClient

client = RelayClient(project="...", token="...", contexts=["default"])

@client.on_call
async def handle(call):
    await call.answer()

    # The call is live and the platform owns its state. You command it over one
    # persistent connection, no webhook round-trip, no state you rebuilt yourself.

    # Speak a prompt into the live call
    await call.play([{"type": "tts",
                      "params": {"text": "One moment while I pull up your account."}}])

    # Transfer it to a human, with call state intact
    await call.transfer("https://example.com/agents/support-human")

client.run()
Bottom Linear Gradient  Lines image

THE PROBLEM

What look like telecom problems are actually architecture problems

Maintaining call state in your application layer is what fails at scale — not the network. The CPaaS hands you raw events and leaves the rest to you.

When the machinery fails

Broken calls

A dropped WebSocket or a missed reconnect ends the call. The state your app was tracking dies with it.

Broken calls

A dropped WebSocket or a missed reconnect ends the call. The state your app was tracking dies with it.

No clear point of failure

Latency or a garbled transfer spans four vendors. Nobody owns the call, so nobody owns the bug.

No clear point of failure

Latency or a garbled transfer spans four vendors. Nobody owns the call, so nobody owns the bug.

Ungoverned AI

The model is steered by a prompt and hope. There is no deterministic layer enforcing what it can and cannot do.

Ungoverned AI

The model is steered by a prompt and hope. There is no deterministic layer enforcing what it can and cannot do.

your code maintains all of this

  • 01

    State synchronization

    // you maintain

  • 02

    Audio routing

    // you maintain

  • 03

    Error recovery

    // you maintain

  • 04

    Protocol negotiation

    // you maintain

  • 05

    WebSocket management

    // you maintain

  • 06

    Vendor SDK integration

    // you maintain

  • 07

    Transcript buffering

    // you maintain

  • 08

    TTS routing

    // you maintain

  • 09

    Turn tracking

    // you maintain

  • 10

    Barge-in handling

    // you maintain

  • 11

    Session tracking

    // you maintain

  • 12

    Transfer logic

    // you maintain

  • 13

    Compliance logging

    // you maintain

  • 14

    Codec transcoding

    // you maintain

  • 15

    Reconnect handling

    // you maintain

  • 16

    Race condition handling

    // you maintain

  • 17

    Event ordering

    // you maintain

  • 18

    Timeout handling

    // you maintain

  • 19

    Retry logic

    // you maintain

  • 20

    Call state reconstruction

    // you maintain

  • 21

    Conference bridging

    // you maintain

  • 22

    Recording management

    // you maintain

  • 23

    DTMF handling

    // you maintain

  • 24

    Escalation handling

    // you maintain

  • 25

    Latency monitoring

    // you maintain

Side by side

CPaaS and SignalWire are built differently

Dimension

Traditional CPaaS

SignalWire

Call state

Rebuilt in your application

Lives in your call

Transfers

Context is dropped

Context travels with the call

AI governance

Prompt and hope

Governed by code

Vendor stack

Five vendors stitched together

One platform

Voice AI latency

2-4s, compounded

800-1200ms end to end

Dimension

Traditional CPaaS

Call state

Rebuilt in your application

Transfers

Context is dropped

AI governance

Prompt and hope

Vendor stack

Five vendors stitched together

Voice AI latency

2-4s, compounded

Dimension

SignalWire

Call state

Lives in your call

Transfers

Context travels with the call

AI governance

Governed by code

Vendor stack

One platform

Voice AI latency

800-1200ms end to end

THE SOLUTION

The missing layer

The missing layer

Networks, Kubernetes, and databases all share the same architecture: a control plane that owns state and absorbs complexity. Communications is the one system that never got one.

Networks, Kubernetes, and databases all share the same architecture: a control plane that owns state and absorbs complexity. Communications is the one system that never got one.

SignalWire is that control plane. Every call, channel, and command passes through it, and it owns their state, routing, and lifecycle as the programmable surface over the industry's most powerful media engine.

CPaaS gave developers APIs but left them rebuilding session tracking, transfer logic, and compliance logging on their own. Telecom owned the interaction but never let developers build. SignalWire closes that gap with one system that owns the interaction and lets you program it. This is Programmable Unified Communications.

SignalWire is that control plane. Every call, channel, and command passes through it, and it owns their state, routing, and lifecycle as the programmable surface over the industry's most powerful media engine.

CPaaS gave developers APIs but left them rebuilding session tracking, transfer logic, and compliance logging on their own. Telecom owned the interaction but never let developers build. SignalWire closes that gap with one system that owns the interaction and lets you program it. This is Programmable Unified Communications.

Top Linear Gradient  Lines image

HOW IT WORKS

Three principles that make the call self-aware

01

Calls are stateful objects that carry their own context

History, tool results, and transfer lineage live inside the call, persisted by the platform. You read state; you never rebuild it.

state.py

copy

01

02

03

04

05

06

07

08

09

10

11

12

from signalwire.relay import RelayClient

client = RelayClient(project="...", token="...", contexts=["default"])

@client.on_call
async def handle(call):
    await call.answer()

    # The call is live and the platform owns its state. You command it over one
    # persistent connection, no webhook round-trip, no state you rebuilt yourself.

    # Speak a prompt into the live call
    await call.play([{"type": "tts",
                      "params": {"text": "One moment while I pull up your account."}}])

    # Transfer it to a human, with call state intact
    await call.transfer("https://example.com/agents/support-human")

client.run()

state.py

copy

01

02

03

04

05

06

07

08

09

10

11

12

from signalwire.relay import RelayClient

client = RelayClient(project="...", token="...", contexts=["default"])

@client.on_call
async def handle(call):
    await call.answer()

    # The call is live and the platform owns its state. You command it over one
    # persistent connection, no webhook round-trip, no state you rebuilt yourself.

    # Speak a prompt into the live call
    await call.play([{"type": "tts",
                      "params": {"text": "One moment while I pull up your account."}}])

    # Transfer it to a human, with call state intact
    await call.transfer("https://example.com/agents/support-human")

client.run()

state.py

copy

01

02

03

04

05

06

07

08

09

10

11

12

from signalwire.relay import RelayClient

client = RelayClient(project="...", token="...", contexts=["default"])

@client.on_call
async def handle(call):
    await call.answer()

    # The call is live and the platform owns its state. You command it over one
    # persistent connection, no webhook round-trip, no state you rebuilt yourself.

    # Speak a prompt into the live call
    await call.play([{"type": "tts",
                      "params": {"text": "One moment while I pull up your account."}}])

    # Transfer it to a human, with call state intact
    await call.transfer("https://example.com/agents/support-human")

client.run()
How it works - 02 Image

02

Resources are addressable & composable

A phone number, an AI agent, a queue, a video room: each is an addressable resource. The ingress protocol doesn't constrain routing.

03

AI runs inside the call, not alongside it

The AI runs as a state machine of steps you define. At each step it sees only the tools and data you allow. This is System-Directed AI: the state machine controls the model, not the other way around.

agent.py

copy

01

02

03

04

05

06

07

08

09

10

11

12

from signalwire import AgentBase, FunctionResult

class OrderAgent(AgentBase):
    def __init__(self):
        super().__init__(name="orders", route="/orders")
        self.prompt_add_section("Role", body="You help callers look up their orders.")
        self.define_tool(
            name="check_order",
            description="Look up an order by its number.",
            parameters={
                "type": "object",
                "properties": {
                    "order_number": {
                        "type": "string",
                        "description": "The order number to look up.",
                    }
                },
                "required": ["order_number"],
            },
            handler=self.check_order,
        )

    def check_order(self, args, raw_data):
        order_number = args["order_number"]  # your backend lookup goes here
        return FunctionResult(f"Order {order_number} shipped — it arrives Tuesday.")

agent.py

copy

01

02

03

04

05

06

07

08

09

10

11

12

from signalwire import AgentBase, FunctionResult

class OrderAgent(AgentBase):
    def __init__(self):
        super().__init__(name="orders", route="/orders")
        self.prompt_add_section("Role", body="You help callers look up their orders.")
        self.define_tool(
            name="check_order",
            description="Look up an order by its number.",
            parameters={
                "type": "object",
                "properties": {
                    "order_number": {
                        "type": "string",
                        "description": "The order number to look up.",
                    }
                },
                "required": ["order_number"],
            },
            handler=self.check_order,
        )

    def check_order(self, args, raw_data):
        order_number = args["order_number"]  # your backend lookup goes here
        return FunctionResult(f"Order {order_number} shipped — it arrives Tuesday.")

agent.py

copy

01

02

03

04

05

06

07

08

09

10

11

12

from signalwire import AgentBase, FunctionResult

class OrderAgent(AgentBase):
    def __init__(self):
        super().__init__(name="orders", route="/orders")
        self.prompt_add_section("Role", body="You help callers look up their orders.")
        self.define_tool(
            name="check_order",
            description="Look up an order by its number.",
            parameters={
                "type": "object",
                "properties": {
                    "order_number": {
                        "type": "string",
                        "description": "The order number to look up.",
                    }
                },
                "required": ["order_number"],
            },
            handler=self.check_order,
        )

    def check_order(self, args, raw_data):
        order_number = args["order_number"]  # your backend lookup goes here
        return FunctionResult(f"Order {order_number} shipped — it arrives Tuesday.")

See it IN PRODUCTION

Talk to a live agent, and the code that steers it

Talk to a live agent, and the code that steers it

Talk to Sigmond

A voice AI that answers questions about SignalWire over WebRTC

Tap to talk to a live agent

handler.py

copy

01

02

03

04

05

06

07

08

09

10

11

12

# pip install signalwire-sdk   →   python sigmond.py
# Sigmond: a voice AI that answers questions about SignalWire.
# His persona is a prompt. His knowledge is a tool he calls. Your code owns both.

from signalwire import AgentBase, FunctionResult

# A tiny stand-in knowledge base so this runs with zero setup. In production you
# swap this for the native_vector_search skill (see the note at the bottom).
DOCS = {
    "swaig": "SWAIG is SignalWire's AI Gateway, the tool-calling system that lets the AI in.",
    "latency": "Conversational turns land around 1.2 seconds because the AI kernel runs inside the media engine.",
    "puc": "Programmable Unified Communications: the platform owns the call and your code programs it.",
}


class Sigmond(AgentBase):
    def __init__(self):
        super().__init__(name="sigmond", route="/sigmond")
        self.add_language(name="English", code="en-US", voice="inworld.Mark")

        # Persona: who Sigmond is. The prompt is the ONE place the model gets latitude.
        self.prompt_add_section("Personality", body="You are Sigmond, a warm, concise voice guide to SignalWire.")
        self.prompt_add_section("Goal", body="Answer the caller's SignalWire questions accurately.")
        self.prompt_add_section("Rules", bullets=[
            "Call search_docs before you state a fact.",
            "If the docs do not cover it, say so. Never invent product details.",
        ])

    # A SWAIG function is a tool the AI calls mid-call. It runs on YOUR server.
    # The model decides WHEN to search; your code decides WHAT comes back.
    # The type hint (query: str) becomes the tool's schema. No JSON to hand-write.
    @AgentBase.tool(name="search_docs")
    def search_docs(self, query: str, raw_data=None):
        """Search SignalWire documentation to answer a caller's question."""
        for key, fact in DOCS.items():
            if key in query.lower():
                return FunctionResult(fact)
        return FunctionResult("I don't have that in the docs, but I can connect you with someone who does.")


# In production, delete the tool above and let the built-in skill do real vector
# search over an index you build from your docs with the sw-search CLI.
# (Needs the search extras: pip install "signalwire-sdk[search-queryonly]")
#
#   self.add_skill("native_vector_search", {
#       "tool_name": "search_docs",
#       "description": "Search SignalWire documentation.",
#       "index_file": "./signalwire-docs.swsearch",
#   })

# To talk to Sigmond from a browser, a separate @signalwire/js front-end dials his address (WebRTC). Not shown here.

if __name__ == "__main__":
    Sigmond().run()   # serve it, then route a phone number or browser (WebRTC) call to Sig

handler.py

copy

01

02

03

04

05

06

07

08

09

10

11

12

# pip install signalwire-sdk   →   python sigmond.py
# Sigmond: a voice AI that answers questions about SignalWire.
# His persona is a prompt. His knowledge is a tool he calls. Your code owns both.

from signalwire import AgentBase, FunctionResult

# A tiny stand-in knowledge base so this runs with zero setup. In production you
# swap this for the native_vector_search skill (see the note at the bottom).
DOCS = {
    "swaig": "SWAIG is SignalWire's AI Gateway, the tool-calling system that lets the AI in.",
    "latency": "Conversational turns land around 1.2 seconds because the AI kernel runs inside the media engine.",
    "puc": "Programmable Unified Communications: the platform owns the call and your code programs it.",
}


class Sigmond(AgentBase):
    def __init__(self):
        super().__init__(name="sigmond", route="/sigmond")
        self.add_language(name="English", code="en-US", voice="inworld.Mark")

        # Persona: who Sigmond is. The prompt is the ONE place the model gets latitude.
        self.prompt_add_section("Personality", body="You are Sigmond, a warm, concise voice guide to SignalWire.")
        self.prompt_add_section("Goal", body="Answer the caller's SignalWire questions accurately.")
        self.prompt_add_section("Rules", bullets=[
            "Call search_docs before you state a fact.",
            "If the docs do not cover it, say so. Never invent product details.",
        ])

    # A SWAIG function is a tool the AI calls mid-call. It runs on YOUR server.
    # The model decides WHEN to search; your code decides WHAT comes back.
    # The type hint (query: str) becomes the tool's schema. No JSON to hand-write.
    @AgentBase.tool(name="search_docs")
    def search_docs(self, query: str, raw_data=None):
        """Search SignalWire documentation to answer a caller's question."""
        for key, fact in DOCS.items():
            if key in query.lower():
                return FunctionResult(fact)
        return FunctionResult("I don't have that in the docs, but I can connect you with someone who does.")


# In production, delete the tool above and let the built-in skill do real vector
# search over an index you build from your docs with the sw-search CLI.
# (Needs the search extras: pip install "signalwire-sdk[search-queryonly]")
#
#   self.add_skill("native_vector_search", {
#       "tool_name": "search_docs",
#       "description": "Search SignalWire documentation.",
#       "index_file": "./signalwire-docs.swsearch",
#   })

# To talk to Sigmond from a browser, a separate @signalwire/js front-end dials his address (WebRTC). Not shown here.

if __name__ == "__main__":
    Sigmond().run()   # serve it, then route a phone number or browser (WebRTC) call to Sig

handler.py

copy

01

02

03

04

05

06

07

08

09

10

11

12

# pip install signalwire-sdk   →   python sigmond.py
# Sigmond: a voice AI that answers questions about SignalWire.
# His persona is a prompt. His knowledge is a tool he calls. Your code owns both.

from signalwire import AgentBase, FunctionResult

# A tiny stand-in knowledge base so this runs with zero setup. In production you
# swap this for the native_vector_search skill (see the note at the bottom).
DOCS = {
    "swaig": "SWAIG is SignalWire's AI Gateway, the tool-calling system that lets the AI in.",
    "latency": "Conversational turns land around 1.2 seconds because the AI kernel runs inside the media engine.",
    "puc": "Programmable Unified Communications: the platform owns the call and your code programs it.",
}


class Sigmond(AgentBase):
    def __init__(self):
        super().__init__(name="sigmond", route="/sigmond")
        self.add_language(name="English", code="en-US", voice="inworld.Mark")

        # Persona: who Sigmond is. The prompt is the ONE place the model gets latitude.
        self.prompt_add_section("Personality", body="You are Sigmond, a warm, concise voice guide to SignalWire.")
        self.prompt_add_section("Goal", body="Answer the caller's SignalWire questions accurately.")
        self.prompt_add_section("Rules", bullets=[
            "Call search_docs before you state a fact.",
            "If the docs do not cover it, say so. Never invent product details.",
        ])

    # A SWAIG function is a tool the AI calls mid-call. It runs on YOUR server.
    # The model decides WHEN to search; your code decides WHAT comes back.
    # The type hint (query: str) becomes the tool's schema. No JSON to hand-write.
    @AgentBase.tool(name="search_docs")
    def search_docs(self, query: str, raw_data=None):
        """Search SignalWire documentation to answer a caller's question."""
        for key, fact in DOCS.items():
            if key in query.lower():
                return FunctionResult(fact)
        return FunctionResult("I don't have that in the docs, but I can connect you with someone who does.")


# In production, delete the tool above and let the built-in skill do real vector
# search over an index you build from your docs with the sw-search CLI.
# (Needs the search extras: pip install "signalwire-sdk[search-queryonly]")
#
#   self.add_skill("native_vector_search", {
#       "tool_name": "search_docs",
#       "description": "Search SignalWire documentation.",
#       "index_file": "./signalwire-docs.swsearch",
#   })

# To talk to Sigmond from a browser, a separate @signalwire/js front-end dials his address (WebRTC). Not shown here.

if __name__ == "__main__":
    Sigmond().run()   # serve it, then route a phone number or browser (WebRTC) call to Sig

800–1200

ms

per min/
AI runtime

~500

ms

Turn detection

5 > 1

Vendor stack consolidated to one platform

0

State machines you build or maintain

Top Linear Gradient  Lines image

THE OUTCOME

When the platform owns the call, you get the outcomes

Instruct calls mid-flight

Reach a call by ID and redirect, transfer, or hand it to an agent. No webhook round-trip.

AI can't go off script

The model sees only the tools and data you scope to each step. It can't act outside what you allow.

One platform, not five vendors

Voice, AI, transcription, and call control run on one runtime, instrumented and billed as one system.

Calls carry their own context

A transfer carries the caller's identity and history with it, so the next agent already knows who's calling and why.

Low-latency AI

The AI kernel runs in the media path, not across vendor hops, for ~800–1200ms round trip latency.

Multi-protocol ingress

The platform normalizes PSTN, SIP, WebRTC and WhatsApp into one call before your code touches it, so you route every caller the same way.

Native observability

The platform records every step, tool call, and decision the agent made, and why.

Native Observability Image
Top Linear Gradient  Lines image

FAQS

Frequently asked questions

Frequently asked questions

Everything you might want to know about Programmable Unified Communications.

Everything you might want to know about Programmable Unified Communications.

What is Programmable Unified Communications?

Programmable Unified Communications is a communications architecture where the platform owns the call state, lifecycle, and routing, and your code controls behavior through a programmable API surface. Unlike CPaaS, which treats a call as a transaction your application must track, PUC treats a call as a persistent object the platform manages.

The result is that state management, transfer logic, session tracking, and AI governance stop being things you rebuild on every project. Whether you're deploying an AI voice agent, a contact center, or a complex multi-channel flow, voice, video, messaging, and AI run on one substrate. The platform handles the infrastructure complexity; your code handles the business logic.

How is SignalWire different from a traditional CPaaS?

CPaaS delivers events and expects your application to maintain state. SignalWire owns the state, lifecycle, and routing of every interaction.

With a CPaaS, your application is the glue between telephony, AI, and business logic. With SignalWire, the platform is that glue. Transfers preserve context. AI is governed deterministically. You don't need a five-vendor stack to ship a voice AI product.

Why does my voice AI lose context on transfer?

Because with traditional CPaaS, context lives in your application layer, not in the call itself. When a call transfers, the new leg has no memory of what happened on the previous one unless your application explicitly passes it, and that handoff is fragile.

On SignalWire, context moves with the call because the platform owns it. Transfer history, conversation context, tool results, they're all part of the call object, not your application's responsibility to preserve.

What is a communications control plane?

A control plane is the governance layer that sits above the media path and owns routing, state, and lifecycle decisions. In networking and infrastructure ( Kubernetes, SD-WAN) control planes are standard. Communications has never had one until now.

Traditional CPaaS pushes that responsibility onto your application. Every team building on CPaaS ends up writing the same invisible machinery: session tracking, transfer logic, compliance logging, AI governance. SignalWire's control plane is the layer that was always missing: it handles AI call routing, state ownership, and lifecycle decisions so your application doesn't have to.

Why is my voice AI response latency so high?

Most voice AI stacks route audio through multiple separate vendors (telephony, STT, LLM, and TTS), each adding network hops and processing time. The compounded latency of that architecture is 2-4 seconds, which is long enough to feel broken to a caller.

SignalWire embeds the AI kernel directly in the media processing pipeline. STT, inference, and TTS are coordinated inside the media engine with millisecond-level timing. End-to-end response latency is 800-1200ms, and as low as 600ms with speech-to-speech models.

Can I use SignalWire with my existing LLM?

Yes. SignalWire is model-agnostic. You can connect your existing LLM through the tool invocation layer. When the AI needs your backend, the call routes to your code, you return a result, and the platform handles the rest.

You don't need to re-engineer your LLM integration to add voice. SignalWire wraps around your existing AI logic and handles the telephony, state, and media complexity that voice adds.

Can SignalWire replace my entire voice AI stack?

Yes. SignalWire is designed to be the single AI telecom platform for voice, video, messaging, and AI agents. STT, LLM orchestration, TTS, call control, conferencing, recording, and compliance logging all run on one platform. It is purpose-built for AI call center deployments where state, latency, and governance cannot be an afterthought.

The practical result is one vendor, one SDK, one billing relationship, and one observability surface instead of four. Teams that have built on multi-vendor stacks typically consolidate onto SignalWire to eliminate the integration and maintenance overhead those stacks produce.

Let the platform own the call. Your code owns the behavior.

State management, transfer logic, AI governance, vendor stitching — gone by default. Spin up a space and instruct your first call in minutes.

Let the platform own the call. Your code owns the behavior.

State management, transfer logic, AI governance, vendor stitching — gone by default. Spin up a space and instruct your first call in minutes.

Let the platform own the call. Your code owns the behavior.

State management, transfer logic, AI governance, vendor stitching — gone by default. Spin up a space and instruct your first call in minutes.

Let the platform own the call. Your code owns the behavior.

State management, transfer logic, AI governance, vendor stitching — gone by default. Spin up a space and instruct your first call in minutes.