Skip to content

Latest commit

 

History

History
244 lines (182 loc) · 4.17 KB

File metadata and controls

244 lines (182 loc) · 4.17 KB
title Callbacks and DTMF
description Voice provider callbacks, statuses and keypad actions

Callbacks and DTMF

IncidentRelay can receive provider callbacks for:

  • call status changes;
  • DTMF phone keypad input;
  • provider errors.

Callback URL

When IncidentRelay creates a call, it passes callback_url to the provider.

Example:

https://incidentrelay.example.com/api/integrations/voice/callback/12/change-me-channel-secret

The URL format is:

/api/integrations/voice/callback/{channel_id}/{secret}

Callback flow

The provider should send call events to the callback URL.

IncidentRelay will:

1. Validate callback secret.
2. Load the globally configured voice provider.
3. Pass raw callback payload to provider.parse_callback().
4. Find notification by provider call_id.
5. Store callback status and payload.
6. Write callback event history.
7. Apply DTMF action if configured.

Status callback

Example provider callback:

{
  "call_id": "abc-123",
  "event_type": "status",
  "status": "answered"
}

Provider normalization:

return [
    VoiceCallCallbackEvent(
        call_id=payload["call_id"],
        event_type="status",
        status=payload["status"],
        raw=payload,
    )
]

Recommended statuses:

queued
ringing
answered
completed
failed
busy
no_answer
cancelled
unknown

IncidentRelay stores the latest status in the notification record and also stores callback history.

DTMF callback

DTMF means phone keypad input.

Default mapping:

{
  "1": "acknowledge",
  "2": "resolve"
}

Example spoken message:

IncidentRelay alert 123.
Disk is full.
Severity critical.
Press 1 to acknowledge.
Press 2 to resolve.

If the user presses 1, provider sends:

{
  "call_id": "abc-123",
  "event_type": "dtmf",
  "digit": "1"
}

IncidentRelay maps it to:

acknowledge

If the user presses 2, IncidentRelay maps it to:

resolve

A provider may also send the normalized action directly:

{
  "call_id": "abc-123",
  "event_type": "dtmf",
  "action": "acknowledge"
}

In this case IncidentRelay does not need to map the digit.

Error callback

Providers may send error events.

{
  "call_id": "abc-123",
  "event_type": "error",
  "status": "failed",
  "message": "Insufficient balance"
}

Provider normalization:

return [
    VoiceCallCallbackEvent(
        call_id=payload["call_id"],
        event_type="error",
        status="failed",
        message=payload.get("message"),
        raw=payload,
    )
]

Provider-specific callback signatures

Some providers sign webhook callbacks.

parse_callback() receives:

payload
headers
raw_body
query_args

Use headers and raw_body to validate provider signatures.

Example:

def parse_callback(self, payload, headers=None, raw_body=None, query_args=None):
    signature = headers.get("X-Provider-Signature") if headers else None

    if not self._is_valid_signature(raw_body or b"", signature):
        raise RuntimeError("invalid provider callback signature")

    ...

IncidentRelay validates its own callback secret before calling parse_callback().

Provider-specific signature validation is optional but recommended when the provider supports it.

Polling call status

Some providers do not support callbacks.

In this case, the provider may implement:

def get_call_status(self, call_id: str) -> VoiceCallResult:
    ...

Example:

def get_call_status(self, call_id: str) -> VoiceCallResult:
    response = requests.get(
        f"{self.config['api_url'].rstrip('/')}/calls/{call_id}",
        headers={
            "Authorization": f"Bearer {self.config['api_token']}",
        },
        timeout=int(self.config.get("timeout", 10)),
    )
    response.raise_for_status()

    data = response.json() if response.content else {}

    return VoiceCallResult(
        call_id=call_id,
        status=str(data.get("status") or "unknown"),
        raw=data,
    )

Set capability:

capabilities = VoiceProviderCapabilities(
    tts=True,
    status_callback=False,
    dtmf_callback=False,
    status_polling=True,
)