Guides

Telephony & SIP

Put your workflows on real phone numbers. Vollo speaks SIP to any carrier — Twilio Elastic SIP Trunking is the primary, best-tested path — and routes inbound calls to workflows with dispatch rules.

How the pieces fit#

Three resources, in order:

All three are managed in the dashboard under Telephony, or via the API with the telephony scope.

SIP trunks#

Twilio Elastic SIP Trunking (recommended)#

You will connect the two platforms in both directions: origination (Twilio delivers inbound calls to Vollo) and termination (Vollo sends outbound calls to Twilio). Worked example for a workspace whose SIP domain is acme:

  1. Create the trunk in Vollo

    cURL / shellCreate a SIP trunk
    curl -X POST https://api.vollo.io/api/v1/sip-trunks \
      -H "Authorization: Bearer pk_live_8f3KJd92mA4qL7Zx1RttVWpc" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Twilio production",
        "provider": "twilio",
        "termination_uri": "vollo-acme.pstn.twilio.com",
        "auth_username": "vollo-prod",
        "auth_password": "REPLACE-WITH-TRUNK-PASSWORD",
        "transport": "tls"
      }'
    JSON201 Created (excerpt)
    {
      "data": {
        "id": "b7e1d4c8-3f9a-4b2e-8c6d-1e5a9f3b7d2c",
        "name": "Twilio production",
        "provider": "twilio",
        "origination_uri": "sip:acme.sip.vollo.io;transport=tls",
        "termination_uri": "vollo-acme.pstn.twilio.com",
        "transport": "tls",
        "status": "active"
      }
    }

    The returned origination_uri is your workspace's inbound SIP address — you'll paste it into Twilio next.

  2. Configure the Twilio trunk

    In the Twilio console, create an Elastic SIP Trunk (say vollo-acme) and set:

    Twilio settingValue
    Origination URIsip:acme.sip.vollo.io;transport=tls — priority 10, weight 10, enabled
    Termination SIP URIvollo-acme.pstn.twilio.com
    Credential listUsername vollo-prod, the password you set above — Vollo authenticates outbound INVITEs with these
    Secure trunking (TLS + SRTP)Enabled — matches "transport": "tls"

    Then assign your Twilio phone number(s) to that trunk under Numbers.

  3. Register the number in Vollo and route it

    See Phone numbers and Dispatch rules below. Call the number — your workflow should answer within a ring.

Generic SIP#

Any carrier that can send SIP to a URI works the same way: point its outbound route at your origination_uri (sip:acme.sip.vollo.io;transport=tls, port 5061; UDP/TCP on 5060 also accepted with "transport": "udp"), and create the trunk with your carrier's termination host and credentials. Vollo authenticates inbound traffic by SIP domain plus, optionally, a source-IP allowlist ("allowed_ips": ["203.0.113.0/24"] on the trunk). Media is RTP with Opus, G.711 µ-law/A-law negotiated; DTMF via RFC 4733 telephone events.

Phone numbers#

Register each E.164 number that lives on a trunk:

cURL / shellRegister a number
curl -X POST https://api.vollo.io/api/v1/phone-numbers \
  -H "Authorization: Bearer pk_live_8f3KJd92mA4qL7Zx1RttVWpc" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "+12125550142",
    "sip_trunk_id": "b7e1d4c8-3f9a-4b2e-8c6d-1e5a9f3b7d2c",
    "project_id": "9b2e7c1a-4d3f-4e8a-9c5b-2f7d1e6a8b3c",
    "label": "Main support line"
  }'

Numbers serve both directions: inbound calls to the number are routed by its dispatch rules, and outbound calls placed with POST /calls (or the Dial node) must use a registered number as from — it becomes the caller ID, sent out through the number's trunk.

Dispatch rules#

Dispatch rules map an inbound call to a workflow. They live on the phone number and are evaluated in ascending priority order; the first rule whose filters all match wins. A rule with no filters always matches — put one last as the catch-all.

cURL / shellRoute a number
curl -X PATCH https://api.vollo.io/api/v1/phone-numbers/f2a8c5e1-7d4b-4e9c-b3f6-8a1d5c2e7b9f \
  -H "Authorization: Bearer pk_live_8f3KJd92mA4qL7Zx1RttVWpc" \
  -H "Content-Type: application/json" \
  -d '{
    "dispatch_rules": [
      {
        "workflow_id": "7f3a9d2e-8c1b-4f6a-b5d4-e2c9a1f7b8d0",
        "priority": 10,
        "time_window": {
          "days": ["mon", "tue", "wed", "thu", "fri"],
          "start": "09:00",
          "end": "17:00",
          "timezone": "America/New_York"
        }
      },
      {
        "workflow_id": "1d6b8e3a-5c2f-4a7d-9e4b-3f8c1a6d5e2b",
        "priority": 20
      }
    ]
  }'

This answers business-hours calls with the support workflow and everything else with an after-hours workflow. Each rule supports:

FieldTypeNotes
workflow_idstringRequired. The workflow that answers. Its latest published version runs.
prioritynumberLower evaluates first. Default 100.
time_windowobjectdays (monsun), start/end (24-hour HH:MM, end exclusive), timezone (IANA name). Windows crossing midnight are allowed (22:0006:00).
caller_idobjectallow and/or block: lists of E.164 numbers or prefixes ("+9627"). Block wins over allow. Anonymous callers match the literal "anonymous".
variablesobjectSeed values merged into the workflow's variables — e.g. tag the line: {"line": "support"}.

If no rule matches, the call is rejected with SIP 480 Temporarily Unavailable and a call.ended webhook with hangup_reason: "no_dispatch_rule".

Inbound call flow#

Outbound call flow#

Outbound calls start from the API (or from a Dial node inside a running workflow):

If the callee doesn't answer within the ring timeout, the call ends with status: "no_answer" — no workflow runs, and a call.ended webhook fires either way.

Transfer to a human#

Two nodes hand a live call to a person:

Destinations are templates, so they can be dynamic: tel:+12125550188 or a SIP URI like sip:support@pbx.acme.com. After a successful transfer the execution finishes with status transferred and the transcript up to the hand-off is preserved on the conversation.

DTMF#

Vollo receives keypad tones as RFC 4733 telephone events on trunk calls (and as data messages on web sessions). Collect them with the Collect DTMF node:

JSONCollect DTMF — node config
{
  "prompt": "Please enter your six-digit account number, then press pound.",
  "num_digits": 6,
  "terminator": "#",
  "timeout_seconds": 15,
  "save_to_variable": "account_number"
}

Collection ends when num_digits digits arrive, the terminator key is pressed, or the timeout elapses (routing to the timeout handle). Digits pressed while a Speak prompt is playing barge in and count. Combine with Transform → extract_digits when callers speak digits instead of typing them.