Managed callback actions
Managed LLM providers can continue a call by returning messages, and can also ask Delphi to run configured call actions. This page is the field-level reference for those callback action payloads.
The most important rule: callback actions are requests, not permissions. The Flow Designer still controls which actions are enabled, which sources may send them, and which platform/team restrictions apply.
Where actions appear
For TOBi-style callbacks, actions are sent in the top-level actions array of the webhook body:
{
"conversation": {"identifier": {"id": "channel-123"}},
"messages": {
"message": [
{
"content": {
"textMessage": {
"textPlain": "I will connect you now."
}
}
}
]
},
"actions": [
{
"name": "transfer",
"value": {
"transferTarget": "tel:+491701234567",
"transferSipHeaders": [{"name": "X-Queue", "value": "care"}]
}
}
]
}
Each item has:
| Field | Type | Meaning |
|---|---|---|
name | string | Action name. Supported call-control names are transfer, handover, and hangup. |
value | object | Action-specific parameters. Unknown fields are ignored by the managed action mapper. |
handover is treated as a synonym for transfer.
Message-level custom events may also use the SeeTest/mock-bot envelope. Delphi treats eventName as name, merges input into value, and preserves top-level handoverReason, hangupReason, and hangupSipHeaders aliases:
{
"eventName": "hangup",
"hangupReason": "BotConversationCompleted",
"input": {
"hangupSipHeaders": [{"name": "X-Bot-Header", "value": "from-bot"}]
}
}
Resolution and safety
When a callback action arrives, Delphi normalizes it into a runtime command with source: "external_llm", then resolves it against the flow's managed gateway settings.
Order of precedence:
- Callback action fields override the managed action defaults for that one call.
- Flow defaults in
gatewayProfile.actions.transfer/gatewayProfile.actions.hangupfill missing values. - Hard defaults apply only when neither the callback nor the flow sets a value.
gatewayProfile.commandPoliciesmust allow the command type fromexternal_llm.- Platform restrictions still apply: blocked destinations, missing trunks, SIP routing failure, or disabled managed actions can still reject the request.
Hangup headers from an external managed bot are the exception to field-level replacement: configured UI headers form the base set and callback hangupSipHeaders are added. If both sets contain the same header name (case-insensitive), the callback value wins for that call. An empty callback array does not erase configured headers. Explicit headers from flow-engine failover, SDK, API, and sandbox/tool hangups retain replacement behavior. Transfer header behavior is unchanged.
SIP header budget
Callback actions can provide SIP headers for transfer and hangup actions, for example transferSipHeaders or hangupSipHeaders. Delphi measures the custom header set (header name + value characters) after flow defaults and outbound header rules have been applied. A request can therefore exceed the budget even if the raw callback payload looked small, because rule expansion can add or transform headers.
Names, literal values and normalization
Since v0.9.17-patch1, configured custom names are checked on save for valid SIP
token syntax, protected signaling names and case-insensitive duplicates.
Runtime bot input is handled defensively: invalid name characters become _,
control characters in values become spaces, repeated whitespace is collapsed
and leading/trailing whitespace is trimmed. A repaired name that does not begin with a letter receives an X- prefix. Printable punctuation, quotes,
backslashes and Unicode values remain literal; empty values are supported.
Names are compared case-insensitively after normalization. The last occurrence wins, including names that become identical only after sanitization. Bot hangup overrides follow the precedence described above. Protected signaling/internal headers are dropped. Normalization and dropped/colliding headers generate warnings so operators can identify changes without losing the hangup action.
Hard limits
These limits match VoiceAI Connect / AudioCodes-style message-policy guidance used for bot-provided BYE and transfer headers:
| Limit | Value | What counts |
|---|---|---|
| Optional custom header count | 47 | Bot/configured headers after hangup merge. Delphi reserves 16 standard/platform slots and one Reason slot. |
| Complete SIP message header count | 64 | AudioCodes message-policy ceiling, including the 17 reserved slots. |
| Custom header character budget | 12,000 | Sum of all custom header names + values (not including standard SIP headers Delphi/Kamailio add later). |
| Single header name | 128 characters | Name length sanity guard. |
| Single header value | 4,096 characters | AudioCodes message-policy default. |
| Single header line | 4,096 characters | name + : + value + CRLF. A 4,096-character value plus a non-empty name fails this check. |
| Estimated complete SIP message | 65,000 | Custom headers plus a 768-character reserved platform buffer. |
The 47-header allowance is for optional bot/configured headers, not the whole BYE. Typical TOBi/Genesys hangup payloads with fewer than 47 X-Genesys-* BYE headers and under 12,000 name/value characters fit within the hard budgets. The line limit is the tighter single-header bound: keep each value well under 4,096 so Name: value plus CRLF still fits.
Warnings (not hard failures)
Delphi also estimates the final SIP request size by adding a 768-character reserved allowance for standard/platform headers (Via / From / To / Call-ID / routing / trace / Reason, and similar). If that estimate exceeds the RFC 3261 ~1,300-character UDP-safe size, Flow Builder and runtime logs may show a warning. That warning does not reject the action by itself — modern SIP paths usually use TCP/TLS for larger messages.
How enforcement works
- Runtime command parse — Header structure is validated. Transfer headers must also satisfy the hard budget at this stage. Hangup budget enforcement is deferred so optional over-budget headers cannot prevent teardown.
- Action resolution — Configured hangup headers and callback headers are merged. Transfer still fails closed if the resolved custom headers exceed the budget. Hangup instead drops the entire callback header set, retains valid configured headers, and continues teardown. If the configured set itself is invalid, all optional headers are dropped.
- BYE publish (hangup) — Before ARI hangup, Delphi may publish carrier-facing BYE headers. If that final set is still over budget, custom headers are dropped (the SIP
Reasonheader is kept when it alone fits) and hangup continues. - Flow Builder UI — The SIP headers editor shows live used/remaining budget. Hard-limit issues display as errors; RFC UDP-size pressure displays as a warning.
Hangup vs transfer behavior
| Action | Over-budget custom headers |
|---|---|
| Hangup | Headers are dropped; the call still disconnects. Look for hangup_headers_dropped, then normally hangup_success. |
| Transfer | The transfer is rejected before SIP signaling. Fix or shorten headers and retry. |
Errors and log messages to search for
| Where | Signal | Meaning |
|---|---|---|
Conversation actionsLog / runtime reject | runtime_command_rejected with message like Invalid runtime command payload (sipHeaders: …) | Header structure was malformed, or transfer headers exceeded the budget. Hangup is not rejected solely for exceeding the count/character budget. |
| TelPhi system log | hangup_headers_dropped | Hangup continued after dropping over-budget BYE headers. |
| TelPhi system log | hangup_success | Channel hangup completed (may note droppedOverBudgetHeaders: true). |
| TelPhi system log | hangup_aborted | Legacy behavior (pre-12k / fail-closed hangup). Current builds should not abort hangup solely for header budget. |
| TelPhi / Flow Builder budget text | SIP headers total N characters; maximum is 12000 | Hard custom-character budget exceeded. |
| TelPhi / Flow Builder budget text | Too many SIP headers (N); maximum is 47 | Optional custom-header count exceeded. |
| TelPhi / Flow Builder budget text | SIP header "…" value/line is … characters; maximum is 4096 | Single-header size exceeded. |
| TelPhi / Flow Builder warning | Estimated request size above ~1300 including reserved platform headers | Advisory only; does not block hangup. |
Example accepted header set (including typical Genesys-style hangup headers):
[
{"name": "X-Genesys-hangupReason", "value": "conversationCompleted"},
{"name": "X-Genesys-IPCC_Skill", "value": "care"},
{"name": "X-Queue", "value": "care"}
]
Example that is still rejected (or causes hangup header drop):
[
{
"name": "X-Bot-Transcript",
"value": "A multi-kilobyte transcript or JSON document copied into a SIP header..."
}
]
Use short routing identifiers in SIP headers. Put large payloads in application state, callback metadata, or an external system and pass only a compact reference in SIP.
Custom values are sanitized before signaling, and protected SIP routing/header names such as Via, From, To, Call-ID, CSeq, Route, and Reason cannot be replaced by bot-provided headers. Delphi generates the platform Reason header separately. For its SIP/Q.850 cause mapping and wire behavior, see Call termination and transfer signaling.
Failover and bot readiness use the same transfer/hangup action shapes, but they are configured in the flow rather than sent by the provider callback. See Failover activities and Bot readiness.
Runtime configuration updates
v0.9.14 lets managed providers update a small set of live-call runtime settings without granting separate transfer or hangup permissions.
| Command | Typical fields | Effect |
|---|---|---|
update_dtmf_config | sendDTMF, bargeIn, bargeInOnDTMF, bargeInMinWordCount, maxDigits, timeoutMs, interDigitTimeoutMs, terminator, enabled | Updates live DTMF collection and barge-in / interruption-prevention flags for the call. |
update_stt_config | sttSegmentationSilenceTimeoutMs (aliases segmentationSilenceTimeoutMs, segmentationTimeout) | Updates Azure STT segmentation silence for the current call and resets the recognizer. |
update_tts_config | provider / voice / timeout fields from sessionParams | Updates TTS runtime settings for the current call. |
update_bot_delay_config | botNoInputTimeoutMs, botNoInputSpeech, botNoInputUrl, botNoInputRetries, botNoInputGiveUpTimeoutMs, resumeRecognitionTimeoutMs | Changes intermediate comfort-message and final give-up behavior while waiting for bot responses. |
These updates are runtime configuration changes for the current call. They do not bypass transfer restrictions, hangup policy, or provider permissions for actual call-control actions.
Immediate apply: update_dtmf_config, update_stt_config, update_tts_config, and update_bot_delay_config take effect as soon as Delphi receives the config event. They are not queued behind current TTS. That is required for interruption prevention: send sessionParams: { bargeIn: false } in the same callback as a long announcement and speech/DTMF will not barge that announcement. Hangup and transfer still wait for accepted playback to drain.
Example DTMF / barge-in config update (TOBi config custom event):
{
"name": "config",
"value": {
"sessionParams": {
"bargeIn": false,
"bargeInOnDTMF": false
}
}
}
Example DTMF collection-only update:
{
"name": "update_dtmf_config",
"value": {
"enabled": true,
"maxDigits": 6,
"timeoutMs": 8000,
"interDigitTimeoutMs": 2500,
"terminator": "#"
}
}
Example bot-delay config update:
{
"name": "update_bot_delay_config",
"value": {
"botNoInputTimeoutMs": 10000,
"botNoInputSpeech": "One moment, I am still checking that.",
"botNoInputRetries": 1,
"botNoInputGiveUpTimeoutMs": 30000
}
}
Transfer action
Use name: "transfer" or name: "handover" to request a handoff.
Transfer fields
| Callback field | Type | Maps to | Meaning |
|---|---|---|---|
transferTarget | string | target | Destination phone number, tel: URI, sip: URI, or sips: URI. A leading tel: prefix is stripped before execution. |
target | string | target | Alternative name for transferTarget. |
transferMethod | string | method | Transfer execution method. Supported values: sip_refer, sip_refer_replace, sip_invite, sip_invite_bridge, sip_invite_reinvite_p2p, sip_bye. |
transferReferredByURL | string | referredBy | Value for SIP Referred-By metadata when the method uses SIP referral semantics. |
transferNotifications | boolean | transferNotifications | Whether Delphi should keep notification/control behavior for transfer outcome handling when supported by the integration. |
transferNotificationsHangupMS | number | notificationsHangupMs | Grace period in milliseconds before disconnecting after a successful transfer notification path. Maximum 10000. |
transferRoutingPool | string | routingPool | Routing pool selector for number targets. Ignored for full SIP URI targets, which route by URI host. |
transferSipHeaders | array | sipHeaders | Explicit SIP headers to add to the transfer request. Overrides configured default headers for this call. |
transferSipQueryParams | object or array | sipQueryParams | Query parameters appended to the target / Refer-To URI. |
sipQueryParams | object or array | sipQueryParams | Alternative name for transferSipQueryParams. |
transferMessage | string | message | Alias for the transfer announcement message. |
transferPlayMessage | boolean | playMessage | Alias that controls whether Delphi plays the configured transfer message. |
transferCanConfirm | boolean | canConfirm | Alias that controls whether the provider may ask for confirmation before transfer. |
handoverReason | string | reason | SIP Reason text for sip_bye disconnect-reconnect transfers. |
hangupReason | string | reason | Fallback reason when handoverReason is not set. Used by sip_bye. |
transferTarget is required for all transfer methods except sip_bye. With sip_bye, the carrier can reroute based on BYE headers instead of a target URI.
Transfer methods
| Method | Behavior |
|---|---|
sip_refer | Blind SIP REFER handoff. |
sip_refer_replace | SIP REFER with replace/attended semantics where supported. |
sip_invite | Originates a new outbound leg and keeps the gateway in the media path. |
sip_invite_bridge | INVITE-based bridge behavior. |
sip_invite_reinvite_p2p | INVITE setup with peer-to-peer RTP re-INVITE when supported. |
sip_bye | Disconnect-reconnect handoff. Sends BYE with routing headers/reason so the carrier can reroute the caller. |
Header format
Use {name, value} objects for SIP headers:
{
"name": "transfer",
"value": {
"transferTarget": "sip:agent@pbx.example.com",
"transferSipHeaders": [
{"name": "X-Genesys-IPCC_Skill", "value": "care"},
{"name": "X-Delphi-Intent", "value": "billing"}
]
}
}
Query parameter format
transferSipQueryParams accepts either an object:
{
"name": "transfer",
"value": {
"transferTarget": "sip:agent@pbx.example.com",
"transferSipQueryParams": {
"X-Case": "12345",
"X-Language": "de-DE"
}
}
}
or an array of {name, value} pairs:
{
"name": "transfer",
"value": {
"transferTarget": "sip:agent@pbx.example.com",
"sipQueryParams": [
{"name": "X-Case", "value": "12345"},
{"name": "X-Language", "value": "de-DE"}
]
}
}
Transfer examples
Basic PSTN transfer:
{
"name": "transfer",
"value": {
"transferTarget": "tel:+491701234567"
}
}
Transfer through a routing pool with SIP headers:
{
"name": "transfer",
"value": {
"transferTarget": "+49301234567",
"transferRoutingPool": "support-pool",
"transferSipHeaders": [{"name": "X-Delphi-Reason", "value": "customer_requested_agent"}]
}
}
Disconnect-reconnect transfer using BYE headers:
{
"name": "transfer",
"value": {
"transferMethod": "sip_bye",
"handoverReason": "reroute_to_queue",
"transferSipHeaders": [
{"name": "X-Reroute", "value": "queue-7"},
{"name": "X-Caller-Language", "value": "de-DE"}
]
}
}
Hangup action
Use name: "hangup" when the provider decides the call should end.
Hangup fields
| Callback field | Type | Maps to | Meaning |
|---|---|---|---|
hangupReason | string | reason | Reason recorded for the hangup and, where applicable, placed on SIP signaling / call status data. |
hangupSipHeaders | array | sipHeaders | SIP headers added to configured BYE headers. On a case-insensitive name collision, this callback value wins for the call. |
botOperationResult | string | botOperationResult | Bot outcome recorded for CDR/call-status use. Supported values: success, failure. |
botOperationData | object | botOperationData | Additional structured bot outcome data recorded with the operation result. |
Hangup execution also uses the flow's configured hangup defaults. Full playback and disconnect order: Speech playback and hangup.
Accepted hangup envelopes (SCRUM-779)
TelAPI accepts all of these and normalizes them to { name: "hangup", value: { … } } before Redis. A missing or blank handoverReason / hangupReason still hangs up; the Flow Designer default reason is used.
| Shape | Example |
|---|---|
| Canonical | { "name": "hangup", "value": { "hangupReason": "conversationCompleted", "hangupSipHeaders": […] } } |
| SeeTest / mock | { "eventName": "hangup", "input": { "hangupSipHeaders": […] } } |
| SeeTest with top-level reason | { "eventName": "hangup", "handoverReason": "conversationCompleted", "input": { "hangupSipHeaders": […] } } |
| Inside a spoken message | messages[].content.event.custom using either shape above |
| Body root | The same object at the webhook body root (kept on the OpenAPI schema so it is not dropped) |
RFC-invalid characters in a header name (%, #, &, @, spaces) are rewritten to _ before BYE publish, for example X-Genesys-%IPCC_G#RD_ALL_DE&@TAILS → X-Genesys-%IPCC_G_RD_ALL_DE__TAILS. Invalid headers are dropped; hangup still continues. Empty header values (Genesys GRD_LIST="") are allowed.
UI default vs bot override (SCRUM-786)
WebUI hangup settings are the defaults. A non-blank bot field wins for that call.
| Field | Precedence |
|---|---|
SIP Reason / hangup reason | Bot handoverReason / hangupReason → Flow Designer default → runtime_command |
| Farewell message | Bot message when allowMessageOverride is true (default) → Flow Designer farewellMessage |
| BYE custom headers | Flow Designer defaults merged with bot hangupSipHeaders; bot wins on the same header name |
reasonRules on the flow, if present, can still rewrite the wire SIP Reason from call metadata after that merge. If the SIP ladder shows the UI reason while the bot sent a reason, check reasonRules first.
To retest: publish a flow with a default hangup reason and farewell; send a SeeTest hangup without reason (defaults apply); send one with handoverReason (bot reason on the BYE).
| Flow setting | Effect |
|---|---|
waitForSilence | Whether Delphi waits for active TTS playback before disconnecting. Defaults to true. |
farewellMessage | Optional message Delphi can play before the hangup. |
allowMessageOverride | Whether a runtime hangup command may replace the configured farewell message. |
delayBotDisconnectMs | Grace period after wait-for-silence, before the channel hangup. Maximum 30000. |
headerRules | Outbound Channel Data Forwarding rules that produce BYE headers from call metadata. |
Hangup examples
End the call with a reason:
{
"name": "hangup",
"value": {
"hangupReason": "conversationCompleted"
}
}
End the call and add BYE headers:
{
"name": "hangup",
"value": {
"hangupReason": "conversationCompleted",
"hangupSipHeaders": [
{"name": "X-Genesys-hangupReason", "value": "conversationCompleted"},
{"name": "X-Genesys-IPCC_Skill", "value": "care"}
]
}
}
Record a bot operation outcome:
{
"name": "hangup",
"value": {
"hangupReason": "done",
"botOperationResult": "success",
"botOperationData": {
"feedback": "cooperative",
"intent": "appointment_confirmed"
}
}
}
Failover activities
Failover activities are configured on the managed gateway profile, not sent by callback. They describe what Delphi should do when a runtime failure happens after the call is already active.
Failover is stored under gatewayProfile.failover:
{
"gatewayProfile": {
"failover": {
"enabled": true,
"botNoInputGiveUpTimeoutMs": 30000,
"botNoInputTimeoutMs": 10000,
"botNoInputSpeech": "One moment, I am still checking that.",
"botNoInputRetries": 1,
"policies": [
{
"triggers": ["bot_timeout"],
"action": "transfer",
"transfer": {
"target": "+49301234567",
"method": "sip_refer",
"routingPool": "support-pool",
"sipHeaders": [{"name": "X-Failover", "value": "bot_timeout"}]
}
},
{
"triggers": ["bot_error", "system_error"],
"action": "hangup",
"hangup": {
"reason": "failover",
"sipHeaders": [{"name": "X-End-Reason", "value": "failover"}]
}
}
]
}
}
}
Failover fields
| Field | Type | Meaning |
|---|---|---|
enabled | boolean | Enables failover processing for the managed gateway profile. |
botNoInputGiveUpTimeoutMs | number | Final no-response watchdog for bot_timeout. Replaces botResponseTimeoutMs; allowed range depends on the active flow validation. |
botNoInputTimeoutMs | number | Intermediate no-input timeout before playing a comfort message. |
botNoInputSpeech / botNoInputUrl | string | Comfort message text or media URL played while waiting for the bot. |
botNoInputRetries | number | Number of intermediate comfort attempts before the final give-up timeout. |
policies[] | array | Ordered failover steps. Delphi evaluates matching policies in array order. Maximum 10. |
policies[].triggers[] | array | Failure classes that activate the policy. |
policies[].action | string | transfer or hangup. |
policies[].transfer | object | Transfer action shape. Uses the same fields as managed transfer defaults: target, method, mediaMode, message, playMessage, canConfirm, maxRingTime, callerId, referredBy, disconnectBotLeg, transferNotifications, notificationsHangupMs, routingPool, sipHeaders, sipQueryParams, reason. |
policies[].hangup | object | Hangup action shape. Uses the same fields as managed hangup defaults: reason, waitForSilence, sipHeaders, delayBotDisconnectMs, botOperationResult, botOperationData. |
policies[].description | string | Optional human-readable note for reviewers. |
Failover triggers
| Trigger | When it fires |
|---|---|
bot_timeout | The managed bot/LLM reaches the final botNoInputGiveUpTimeoutMs watchdog without producing a response. |
bot_error | The managed bot/LLM returns or raises an error. |
stt_error | Speech-to-text setup or runtime processing fails. |
tts_error | Text-to-speech setup or runtime playback fails. |
system_error | Internal gateway/runtime error. |
Transfer failover is converted into a normalized runtime command with source: "flow_engine" and then follows the same transfer validation as callback-triggered transfer. Hangup failover follows the same hangup validation and SIP BYE header handling as callback-triggered hangup.
Bot readiness
Bot readiness is pre-answer inbound handling. It decides whether Delphi answers immediately or waits until provider preparation, such as TOBi createConversation, succeeds.
Readiness can be configured as a flow-wide default under gatewayProfile.inbound, and entry points can override it with their own inbound block.
{
"gatewayProfile": {
"inbound": {
"answerWhen": "ready",
"readyTimeoutMs": 5000,
"onNotReady": "transfer",
"transfer": {
"target": "+49301234567",
"method": "sip_refer",
"routingPool": "support-pool",
"sipHeaders": [{"name": "X-Not-Ready", "value": "tobi_timeout"}]
}
}
}
}
Readiness fields
| Field | Type | Meaning |
|---|---|---|
answerWhen | string | ready waits until inbound readiness gates pass before sending SIP 200 OK; immediate answers without waiting. Default ready. |
readyTimeoutMs | number | Maximum wait for readiness before onNotReady runs. Maximum 60000; default 5000. Ignored when answerWhen is immediate. |
onNotReady | string | What to do when readiness fails or times out. Values: busy, hangup, transfer. Default busy. |
hangup | object | Used when onNotReady is hangup. Supports reason, sipHeaders, and headerRules. |
transfer | object | Used when onNotReady is transfer. Supports target, method, mediaMode, routingPool, sipHeaders, headerRules, maxRingTime, callerId, referredBy, and reason. |
Not-ready actions
onNotReady | Behavior |
|---|---|
busy | Rejects the inbound call with SIP busy behavior before answer. |
hangup | Ends the call with optional SIP Reason/header data. This path resolves through the managed hangup action with waitForSilence: false. |
transfer | Answers, then transfers the caller to the configured target. This path resolves through the managed transfer action. |
Use readiness for failures before the provider is ready to talk to the caller. Use failover for failures after the live managed interaction has started.
Related flow settings
The callback values above are per-call overrides. Persistent defaults live in the Flow Designer:
- Managed action inspector — UI for transfer and hangup defaults.
- Source view — JSON shape of
gatewayProfile.actions. - Webhooks — callback endpoint, auth, and delivery behavior.
- Transaction correlation — how
transactionIdpropagates through the webhook callback to SIP headers and OTel spans. - Speech playback and hangup — TTS queueing,
waitForSilence, anddelayBotDisconnectMsbehind hangup callbacks.