1. Feedback Actions
  2. Update From Call

Feedback Actions

Update From Call Feedback Action

Explains the Call feedback action that runs when a phone call comes in through a SIP connection. Useful for answering phone calls with a voice assistant, turning calls away, following up on missed calls, and getting digits the caller typed on the keypad.

Overview

Stubber can take phone calls through your phone system (PBX). A SIP connection logs Stubber into the PBX as an extension, like a desk phone. Any call to that extension, whether someone dials it on the PBX or the PBX routes a phone number to it, rings Stubber, and the _update_from_call feedback action is triggered on the stub.

The stub that gets the call is set on the SIP connection, in the manage platform under Notifications → SIP. The PBX login is a SIP Connection credential. A connection can also send calls from chosen numbers to a draft stub, see Testing with a draft stub.

Phone calls were called dial at first. The old names still work where a template passes them in: the answer_dial_call, reject_dial_call and collect_dial_digits tasks, the dialcalluuid param, and "channel": "dial" in Start Voice Chat. The feedback action and its data use the new names only: _update_from_call, _incoming_call_data and call_data.calluuid.

The message of the stubpost, i.e. stubpost.message, says who is calling, eg Incoming call from Joe Soap (0215550123).
All the details of the call are available in the stubpost.data._incoming_call_data data path.

Two call events are posted: incoming and missed. Once a call is answered, it reports back like any live voice call, with the _update_from_voice feedback action, including when it ends. A third event, digits, brings the keys the caller typed when the template asked for them, see Keypad digits.

NOTE

This is about phone calls coming in to Stubber. To make a call from a user's own PBX extension, see the Dial notification.

call_data.event tells the events apart. The flags on stubpost.data._incoming_call_data are:

Flag Incoming call Missed call Digits
call_data.event incoming missed digits
is_call_event true true true
is_incoming_call true false false
WARNING

is_incoming_call is false for both missed calls and digits. To handle missed calls, check call_data.event = 'missed', not is_incoming_call = false.

Answering a call

Run Start Voice Chat with the call channel and the call's audio format, then Answer Call with the call and the new voice session. Use is_incoming_call as the condition on both:

loading...
WARNING

Pass call_data.audio_format to start_voice_chat with ~~, as above. Phone calls use the G.711 audio format the phone system prefers, μ-law or A-law, and the voice call has to match it. Without it, the voice call uses μ-law, and an A-law call can't be answered.

answer_call returns once the caller hears the assistant, or with an error when answering failed:

Result Meaning
success The call is live. payload.status is answered
call_not_ringing The call isn't ringing anymore, eg the caller hung up first or the call was already answered or rejected
unknown_call No call with this calluuid in your organization
technical error Answering failed. error.details.step says where. The caller's call ends
NOTE

The call must be answered within 25 seconds of the incoming event. The caller hears ringing until then. After that the call is turned away and a missed event follows.

To end an answered call, end the voice call as usual, eg with a hangup voice notification.

Rejecting a call

To not take the call, run Reject Call:

loading...

It only works while the call is ringing. The caller's phone system is told the call was declined, and no missed event follows.

Missed calls

A call that was never answered triggers _update_from_call a second time, with call_data.event set to missed and is_incoming_call set to false. Use it to follow up, eg to schedule a call back:

        "conditions": ["stubpost.data._incoming_call_data.call_data.event = 'missed'"]

      

call_data.reason says why the call was missed:

reason Meaning
caller_cancelled The caller hung up while it was ringing
answer_timeout The call wasn't answered within 25 seconds

Keypad digits

To get digits the caller types on their keypad, eg a menu choice or a customer number, run Collect Call Digits on the answered call, in the same action as the voice notification that asks the question. Once the caller is done, _update_from_call is triggered on the stub that ran the task, with call_data.event set to digits:

        "conditions": [
  "stubpost.data._incoming_call_data.call_data.event = 'digits'",
  "stubpost.data._incoming_call_data.call_data.name = 'customer_number'"
]

      

call_data.digits has what the caller typed, without the #, and call_data.name the name you gave the task. call_data.ended_by says how the entry ended:

ended_by Meaning
terminator The caller pressed #
max_digits The caller typed the task's max_digits digits
timeout The caller stopped typing. digits has what they typed
no_input The caller typed nothing in time. digits is an empty string

Keys pressed while nothing is being collected are ignored. See Collect Call Digits for a menu followed by a customer number.

Testing with a draft stub

A SIP connection has a live stub, which gets every call, and optionally a draft stub with a list of caller numbers. Calls from those numbers go to the draft stub instead, so you can try a draft template on the real extension while everyone else still reaches the live one. Missed calls go to the same stub as the call.

call_data.route says which stub got the call: live or draft.

Only the digits of a number are compared, and they must match exactly. +27 82 123 4567 matches a caller presented as 27821234567, but not 0821234567. Enter the numbers the way your phone system presents the caller, which is the from of a call. Callers who withhold their number always go to the live stub.

Things to know

  • Phone audio. Phone calls are 8 kHz telephone audio (G.711), so the assistant hears and speaks at phone quality.
  • One call at a time per extension. A SIP connection takes one call at a time unless Stubber raises its limit. Other callers get a busy signal.
  • One voice call per stub. A stub can only have one live voice call. To take calls on more than one extension at once, have the stub that gets the calls create a stub per call.
  • Who is calling. from is the number or extension the phone system presents for the caller. It can be an internal extension, eg 101, or a full phone number.
  • Keypad presses (DTMF) only reach the stub while Collect Call Digits collects them.

Data Structure

Feedback Action Data

Example data for an incoming call:

        "_incoming_call_data": {
  "is_call_event": true,
  "is_incoming_call": true,
  "from": "0215550123",
  "from_name": "Joe Soap",
  "to": "801",
  "call_data": {
    "event": "incoming",
    "calluuid": "238e63a5-e071-47d9-bac2-360b77fc5991",
    "sipconnectionuuid": "161bc664-3c90-4a1e-ba07-6d1f5438dfe4",
    "route": "live",
    "audio_format": { "type": "audio/pcma", "rate": 8000 },
    "answer_deadline": "2026-10-15T10:00:25.000Z"
  }
}

      

Example data for a missed call:

        "_incoming_call_data": {
  "is_call_event": true,
  "is_incoming_call": false,
  "from": "0215550123",
  "from_name": "Joe Soap",
  "to": "801",
  "call_data": {
    "event": "missed",
    "calluuid": "238e63a5-e071-47d9-bac2-360b77fc5991",
    "sipconnectionuuid": "161bc664-3c90-4a1e-ba07-6d1f5438dfe4",
    "route": "live",
    "audio_format": { "type": "audio/pcma", "rate": 8000 },
    "reason": "answer_timeout"
  }
}

      

Example data for digits the caller typed:

        "_incoming_call_data": {
  "is_call_event": true,
  "is_incoming_call": false,
  "from": "0215550123",
  "from_name": "Joe Soap",
  "to": "801",
  "call_data": {
    "event": "digits",
    "calluuid": "238e63a5-e071-47d9-bac2-360b77fc5991",
    "sipconnectionuuid": "161bc664-3c90-4a1e-ba07-6d1f5438dfe4",
    "name": "customer_number",
    "digits": "100234",
    "ended_by": "terminator"
  }
}

      

Properties

is_call_event
boolean

true for every call event.


is_incoming_call
boolean

true for a ringing call to answer or reject, false for a missed call and for digits.


from
string

The caller's number or extension, as the phone system presents it.


from_name
string

The caller's name, when the phone system knows it. Otherwise null.


to
string

The extension that was called, i.e. the SIP connection's extension.


call_data.event
string

incoming, missed or digits.


call_data.calluuid
string

The id of the call. Pass it to answer_call or reject_call.


call_data.sipconnectionuuid
string

The SIP connection the call came in on.


call_data.route
string

incoming and missed only. Which of the SIP connection's stubs got the call: live, or draft for a caller in the draft numbers. See Testing with a draft stub.


call_data.audio_format
object

incoming and missed only. The call's audio format, audio/pcmu (μ-law) or audio/pcma (A-law) at 8000. Pass it to start_voice_chat as audio.format.


call_data.answer_deadline
string

incoming only. The ISO 8601 date-time by which the call must be answered.


call_data.reason
string

missed only. Why the call was missed: caller_cancelled or answer_timeout.


call_data.name
string

digits only. The name given to Collect Call Digits, or null.


call_data.digits
string

digits only. The digits the caller typed, without the #. An empty string when they typed nothing.


call_data.ended_by
string

digits only. How the entry ended: terminator, max_digits, timeout or no_input.

Last incoming platform

Like a WhatsApp call, the incoming and missed events also set the stub's last incoming platform and caller in stubpost.data._com_platforms:

        "_com_platforms": {
  "last_incoming_platform": "call",
  "last_from_raw": "0215550123",
  "last_from": { "call": "0215550123" }
}

      

No notification platform handles call, so an auto_platform notification on the stub isn't sent after a phone call.

The voice call that follows doesn't change these: Update From Voice only adds the call's state in _com_platforms.voice.

JSONSchema

The JSONSchema of the stubpost.data._incoming_call_data is:

        {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Incoming Call Data",
  "type": "object",
  "properties": {
    "_incoming_call_data": {
      "type": "object",
      "properties": {
        "is_call_event": { "type": "boolean", "description": "True for every call event." },
        "is_incoming_call": {
          "type": "boolean",
          "description": "True for a ringing call, false for a missed call and for digits."
        },
        "from": { "type": "string", "description": "The caller's number or extension." },
        "from_name": {
          "type": ["string", "null"],
          "description": "The caller's name, when known."
        },
        "to": { "type": "string", "description": "The extension that was called." },
        "call_data": {
          "type": "object",
          "properties": {
            "event": { "type": "string", "enum": ["incoming", "missed", "digits"] },
            "calluuid": { "type": "string", "description": "UUID of the call." },
            "sipconnectionuuid": { "type": "string", "description": "UUID of the SIP connection." },
            "route": {
              "type": "string",
              "enum": ["live", "draft"],
              "description": "Which of the SIP connection's stubs got the call."
            },
            "audio_format": {
              "type": "object",
              "properties": {
                "type": { "type": "string", "enum": ["audio/pcmu", "audio/pcma"] },
                "rate": { "type": "number", "enum": [8000] }
              },
              "required": ["type", "rate"]
            },
            "answer_deadline": {
              "type": "string",
              "description": "incoming only. When the call must be answered by."
            },
            "reason": {
              "type": "string",
              "enum": ["caller_cancelled", "answer_timeout"],
              "description": "missed only."
            },
            "name": {
              "type": ["string", "null"],
              "description": "digits only. The name given to collect_call_digits."
            },
            "digits": {
              "type": "string",
              "description": "digits only. What the caller typed, without the #."
            },
            "ended_by": {
              "type": "string",
              "enum": ["terminator", "max_digits", "timeout", "no_input"],
              "description": "digits only."
            }
          },
          "required": ["event", "calluuid", "sipconnectionuuid"]
        }
      },
      "required": ["is_call_event", "is_incoming_call", "from", "to", "call_data"]
    }
  },
  "required": ["_incoming_call_data"]
}