Call transfer and termination behavior
Agent handover transfers the active call to another party via SIP REFER (or, optionally, SIP INVITE). Call termination sends carrier-facing SIP and Q.850 reasons and may include configured or bot-provided BYE headers. This page describes both paths, their enforced SIP header limits, and their failure behavior. See also SIP signaling reference, SIP trunk log reference for inbound SIP trunk health messages, and SIP header context rules.
SIP header limits (INVITE / REFER)
For INVITE and REFER transfers, custom SIP headers are validated at three layers — the runtime-command zod schema, the action resolver, and the egress-intent publish step — and rejected when out of budget (never silently truncated). Hangup uses the same limits but drops over-budget optional headers so the call can still end.
| Limit | Value | Source |
|---|---|---|
| Max optional bot/configured headers | 47 | SIP_HEADER_MAX_COUNT |
| Complete SIP message header ceiling | 64 | AUDIOCODES_SIP_MAX_HEADERS |
| Reserved standard/platform headers | 16 + Reason | SIP_STANDARD_PLATFORM_HEADER_COUNT + Reason slot |
| Max header name length | 128 chars | SIP_HEADER_MAX_NAME_CHARS |
| Max header value length | 4096 chars | AUDIOCODES_SIP_MAX_HEADER_VALUE_CHARS |
Max single header line (name + : + value + CRLF) | 4096 chars | AUDIOCODES_SIP_MAX_HEADER_CHARS |
| Max total custom headers (sum of name + value chars) | 12,000 chars | SIP_HEADER_MAX_TOTAL_CHARS |
| Estimated total SIP message (custom + ~768-char platform buffer) | 65,000 chars | AUDIOCODES_SIP_MAX_MESSAGE_CHARS |
| JSON request body | 1 MB | Fastify default |
The line limit is the tighter single-header bound: a 4,096-character value plus
a non-empty name fails the 4,096-character line check (name + : + value + CRLF).
RFC 3261 §18.1.1's 1,300-byte UDP-safe figure is emitted as a warning only — oversized requests are expected to travel over TCP/TLS and are not rejected on that basis alone.
Platform termination signaling
Delphi owns the mapping from application causes to numeric SIP and Q.850 causes. Bot and flow input can provide diagnostic text but cannot directly set the numeric values. The selected cause is used consistently for the platform SIP header, Asterisk hangup reason, and downstream Q.850 signaling.
| Platform cause | Category | SIP cause | Q.850 cause | Typical use |
|---|---|---|---|---|
conversation_complete | System | 200 | 16 | Normal bot or flow completion. |
bot_timeout | BotSide | 408 | 102 | Bot/no-input timeout. |
stt_error / tts_error | SpeechService | 503 | 41 | Speech provider timeout or failure. |
internal_failure | Error | 500 | 41 | Internal, setup, Redis, or provider-init error. |
policy_rejected | ClientSide | 403 | 21 | Policy, subscription, or permission rejection. |
unknown_number | ClientSide | 404 | 1 | Unknown or invalid destination/extension. |
busy | ClientSide | 486 | 17 | Busy destination. |
handover | System | 200 | 16 | Transfer, reroute, or escalation. |
Reason text has control characters replaced with spaces, repeated whitespace collapsed, and quotes/backslashes escaped for an RFC quoted string. It is limited to 256 characters; a blank value falls back to the platform cause name.
Signaling by call path
| Call path | Carrier-facing signaling |
|---|---|
| Answered call ends | The BYE contains Asterisk's Q.850 Reason and Delphi's SIP Reason. TelPro preserves the Q.850 value and appends the platform SIP value. |
| Unanswered inbound call is rejected | The platform SIP Reason is added to the final INVITE response because no BYE exists yet. |
| In-dialog transfer uses SIP REFER | The REFER contains the platform SIP Reason. |
| Transfer creates a new outbound SIP INVITE | The new dialog contains X-Transfer-Reason: <cause-id>;sip-cause=<n>;q850-cause=<n>;text="<text>" instead of reusing a Reason header from the original dialog. |
RFC 3326 permits multiple Reason values. A typical answered termination can
therefore contain both:
Reason: Q.850;cause=16
Reason: SIP;cause=200;text="BotConversationCompleted"
Bot hangup header propagation
An external managed bot can supply hangupSipHeaders in the canonical
{name, value} action envelope or the SeeTest/mock-bot {eventName, input}
envelope. See Managed callback actions
for payload fields and examples.
For an external bot hangup, Delphi resolves optional BYE headers as follows:
- Headers configured in Flow Builder form the base set.
- Bot
hangupSipHeadersare added to that set. - When names collide case-insensitively, the bot value wins for that call.
- An omitted or empty bot array leaves configured headers in place.
- The resolved optional headers are sent with the platform-generated
Reasonon the same BYE.
This additive behavior applies only to external managed-bot hangups. Explicit header arrays from flow-engine failover, SDK, API, and sandbox/tool hangups keep their existing replacement semantics.
Bot headers cannot replace protected routing or dialog headers such as Via,
From, To, Call-ID, CSeq, Route, or Reason. Invalid header-name
characters and control characters in values are sanitized before the final
wire-level budget check.
Hangup is fail-open
Optional BYE headers must not keep a call connected:
- If configured headers plus bot additions exceed a hard budget, the complete bot set is dropped and a valid configured set is retained.
- If the configured set is itself invalid, all optional headers are dropped.
- If outbound header rules make the final set exceed a budget, the optional custom set is dropped at the final wire check.
- The platform
Reasonis generated and validated separately, so it does not consume the 47 optional-header slots. - A Redis or TelPro header-intent publication failure is logged but does not block hangup or transfer execution.
The call still disconnects after any optional-header drop. Transfer remains fail-closed: an over-budget transfer header set is rejected before carrier-facing SIP signaling.
Operator signals
| Signal | Meaning |
|---|---|
termination_signal_selected | TelPhi selected the platform cause and SIP/Q.850 values. |
transfer_signal_selected | TelPhi selected the platform handover reason for REFER or INVITE. |
sip_egress_intent_published | Carrier-facing headers were published for BYE, REFER, INVITE, or the final response. |
sip_egress_intent_failed | Header publication failed; call termination or transfer continued fail-open. |
optional_termination_headers_sanitized | Invalid optional header names or values were normalized or removed. |
optional_termination_headers_dropped | Optional platform-path headers exceeded the budget; only the platform reason was kept. |
hangup_headers_dropped | Bot/configured BYE headers exceeded a budget; hangup continued without the dropped set. |
hangup_success | The channel disconnected successfully. |
Transfer target fallback
A bot transfer or handover event may omit transferTarget. When it does, the
gateway resolves the target with this precedence:
- Bot override —
transferTarget(ortarget) on the event. - Managed-action default —
gatewayProfile.actions.transfer.target, configured in the flow's managed transfer inspector (TelWeb → flow → managed transfer action → Default target). - Hard error —
Transfer requires a target: the bot omitted transferTarget and no default target is configured in the flow managed transfer action.
The fallback code path is wired into every dispatch path (external bot, TOBi, sandbox tool, and flow engine). Two things are worth noting:
- A target set on a flow transfer node is used by the flow engine's transfer node only. It is not consulted by bot-driven handovers.
- The managed transfer action default target is what bot-driven handovers fall back to. If it is empty, the handover fails with the error above.
To avoid the failure, set the Default target in the managed transfer action, or
always send transferTarget from the bot. TelWeb emits a non-blocking readiness
warning when a non-sip_bye managed transfer is enabled but has no default target.
Custom SIP headers on transfer
Two behaviors determine which headers reach the transfer request:
- Bot headers replace flow defaults — they are not merged.
transferSipHeadersfrom the bot andsipHeadersfrom the flow default are resolved with a first-defined policy: if the bot sends any headers, the flow default headers are dropped (and vice versa). To combine them, use header context rules (headerRules) on the flow default — rules are an allowlist/transform over call metadata plus explicit headers, so bot headers pass through only when a rule matches. - For REFER, custom headers travel via a Redis Call-ID index, not as
channel-stamped headers. They appear in the
sip_egress_intent_publishedandsip_headers_preparedlog events (componentcall_action_service) underheaderNames/headerCount— not as PJSIP headers on the Stasis channel. This is by design: REFER stays in the existing SIP dialog, so TelPro resolves the action headers by SIP Call-ID.
If configured headers are absent from the egress intent, check whether the bot
also sent transferSipHeaders (which replaces them) and whether headerRules are
configured on the flow default.
Transfer failure logging
Two failure paths surface in the call log:
- Missing or blank target:
resolveTransferActionthrows aCallActionValidationErrorwhose message distinguishes "the bot omittedtransferTargetand no default configured" from "the bot sent a blank target". The runtime-command dispatcher records this as aruntime_command_failedaction on the conversation. - SIP header budget exceeded: the egress intent is not published and a
call_action_service/transfer_blocked(statuserror) log is emitted with codesip_header_budget_exceeded, the method, target, and header count.
REFER transfer outcome
After dispatching the REFER, TelPhi polls the ARI channel variable
VAI_REFER_TRANSFER_STATUS for up to 5 s (100 ms cadence). On a fast successful
handoff, the channel transfers away or tears down before the variable is
readable, so the status is observed as undefined.
A blind REFER is fire-and-forget: "dispatched" is success. The status mapper treats
an unobserved result as success in blind mode (SIP REFER dispatched (blind),
refer_transfer_dispatched / status: success). Explicit local dispatch failures
(TRANSFERSTATUS = FAILURE / MISCONF) still surface as real errors.
In notify mode an unobserved result is a genuine failure, because notify
expects a REFER NOTIFY that reports the downstream answer. To get the true
far-end answer/decline outcome (and bot regain-control on failure), configure
referOutcomeMode: notify + transferNotifications: true.
Downstream reject behavior
Downstream response codes are mapped by mapReferHandoverStatus:
| SIP response | Mapped status |
|---|---|
| 486 Busy Here / 603 Decline | declined |
| 480 Temporarily Unavailable | notAvailable |
| 408 Request Timeout | timeout |
| 404 Not Found | notReachable |
With the default referOutcomeMode: blind, the handoff is fire-and-forget: the bot
leg is disconnected regardless of the downstream outcome, so the caller hears the
downstream busy/decline tone.
To let the bot regain control on a rejected transfer (e.g. play a fallback
prompt or try a second target), configure referOutcomeMode: notify together with
transferNotifications: true. This requests Refer-Sub: true so a provider that
supports REFER NOTIFY can report the downstream outcome, and the bot leg is kept
alive on failure.
Handover reason
handoverReason (aliased with hangupReason) is optional. It is mapped to the
transfer reason field. The carrier-facing header depends on the transfer method:
| Method | Reason signaling |
|---|---|
sip_bye disconnect-reconnect | SIP Reason on the BYE. |
sip_refer / sip_refer_replace | Platform SIP Reason on the REFER. |
sip_invite and other INVITE transfers | X-Transfer-Reason on the new outbound dialog. |
When the reason is absent, Delphi uses handover. Numeric causes remain
platform-controlled (SIP 200 / Q.850 16 for the handover cause).