mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
feat: handling inbound calls
This commit is contained in:
parent
1821872f7a
commit
4bcf10bfae
10 changed files with 417 additions and 26 deletions
|
|
@ -135,6 +135,8 @@ async def get_telephony_configuration(user: UserModel = Depends(get_user)):
|
|||
ws_client_name = config.value.get("ws_client_name", "")
|
||||
from_numbers = config.value.get("from_numbers", [])
|
||||
|
||||
inbound_workflow_id = config.value.get("inbound_workflow_id")
|
||||
|
||||
return TelephonyConfigurationResponse(
|
||||
ari=ARIConfigurationResponse(
|
||||
provider="ari",
|
||||
|
|
@ -142,6 +144,7 @@ async def get_telephony_configuration(user: UserModel = Depends(get_user)):
|
|||
app_name=app_name,
|
||||
app_password=mask_key(app_password) if app_password else "",
|
||||
ws_client_name=ws_client_name,
|
||||
inbound_workflow_id=inbound_workflow_id,
|
||||
from_numbers=from_numbers,
|
||||
),
|
||||
)
|
||||
|
|
@ -208,6 +211,7 @@ async def save_telephony_configuration(
|
|||
"app_name": request.app_name,
|
||||
"app_password": request.app_password,
|
||||
"ws_client_name": request.ws_client_name,
|
||||
"inbound_workflow_id": request.inbound_workflow_id,
|
||||
"from_numbers": request.from_numbers,
|
||||
}
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -104,6 +104,9 @@ class ARIConfigurationRequest(BaseModel):
|
|||
default="",
|
||||
description="websocket_client.conf connection name for externalMedia (e.g., dograh_staging)",
|
||||
)
|
||||
inbound_workflow_id: Optional[int] = Field(
|
||||
default=None, description="Workflow ID for inbound calls"
|
||||
)
|
||||
from_numbers: List[str] = Field(
|
||||
default_factory=list,
|
||||
description="List of SIP extensions/numbers for outbound calls (optional)",
|
||||
|
|
@ -118,6 +121,7 @@ class ARIConfigurationResponse(BaseModel):
|
|||
app_name: str
|
||||
app_password: str # Masked
|
||||
ws_client_name: str = ""
|
||||
inbound_workflow_id: Optional[int] = None
|
||||
from_numbers: List[str]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,12 @@ from loguru import logger
|
|||
|
||||
from api.constants import REDIS_URL
|
||||
from api.db import db_client
|
||||
from api.enums import OrganizationConfigurationKey
|
||||
from api.enums import CallType, OrganizationConfigurationKey, WorkflowRunMode
|
||||
from api.services.quota_service import check_dograh_quota_by_user_id
|
||||
|
||||
# Redis key pattern and TTL for channel-to-run mapping
|
||||
_CHANNEL_KEY_PREFIX = "ari:channel:"
|
||||
_EXT_CHANNEL_KEY_PREFIX = "ari:ext_channel:"
|
||||
_CHANNEL_KEY_TTL = 3600 # 1 hour safety expiry
|
||||
|
||||
|
||||
|
|
@ -41,12 +43,14 @@ class ARIConnection:
|
|||
app_name: str,
|
||||
app_password: str,
|
||||
ws_client_name: str = "",
|
||||
inbound_workflow_id: int = None,
|
||||
):
|
||||
self.organization_id = organization_id
|
||||
self.ari_endpoint = ari_endpoint.rstrip("/")
|
||||
self.app_name = app_name
|
||||
self.app_password = app_password
|
||||
self.ws_client_name = ws_client_name
|
||||
self.inbound_workflow_id = inbound_workflow_id
|
||||
|
||||
self._ws: Optional[websockets.ClientConnection] = None
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
|
|
@ -88,6 +92,23 @@ class ARIConnection:
|
|||
keys = [f"{_CHANNEL_KEY_PREFIX}{cid}" for cid in channel_ids]
|
||||
await r.delete(*keys)
|
||||
|
||||
async def _mark_ext_channel(self, channel_id: str):
|
||||
"""Mark a channel as an external media channel we created."""
|
||||
r = await self._get_redis()
|
||||
await r.set(
|
||||
f"{_EXT_CHANNEL_KEY_PREFIX}{channel_id}", "1", ex=_CHANNEL_KEY_TTL
|
||||
)
|
||||
|
||||
async def _is_ext_channel(self, channel_id: str) -> bool:
|
||||
"""Check if a channel is an external media channel we created."""
|
||||
r = await self._get_redis()
|
||||
return await r.exists(f"{_EXT_CHANNEL_KEY_PREFIX}{channel_id}") > 0
|
||||
|
||||
async def _delete_ext_channel(self, channel_id: str):
|
||||
"""Remove the external media channel marker."""
|
||||
r = await self._get_redis()
|
||||
await r.delete(f"{_EXT_CHANNEL_KEY_PREFIX}{channel_id}")
|
||||
|
||||
@property
|
||||
def ws_url(self) -> str:
|
||||
"""Build the ARI WebSocket URL."""
|
||||
|
|
@ -211,6 +232,16 @@ class ARIConnection:
|
|||
channel_state = channel.get("state", "unknown")
|
||||
|
||||
if event_type == "StasisStart":
|
||||
# Skip external media channels we created — they fire
|
||||
# their own StasisStart but need no further handling.
|
||||
if await self._is_ext_channel(channel_id):
|
||||
logger.debug(
|
||||
f"[ARI org={self.organization_id}] StasisStart for our "
|
||||
f"externalMedia channel {channel_id}, ignoring"
|
||||
)
|
||||
await self._delete_ext_channel(channel_id)
|
||||
return
|
||||
|
||||
app_args = event.get("args", [])
|
||||
caller = channel.get("caller", {})
|
||||
logger.info(
|
||||
|
|
@ -220,31 +251,38 @@ class ARIConnection:
|
|||
f"args={app_args}"
|
||||
)
|
||||
|
||||
# Parse args to extract workflow context
|
||||
args_dict = {}
|
||||
for arg in app_args:
|
||||
for pair in arg.split(","):
|
||||
if "=" in pair:
|
||||
key, value = pair.split("=", 1)
|
||||
args_dict[key.strip()] = value.strip()
|
||||
|
||||
workflow_run_id = args_dict.get("workflow_run_id")
|
||||
workflow_id = args_dict.get("workflow_id")
|
||||
user_id = args_dict.get("user_id")
|
||||
|
||||
if not workflow_run_id or not workflow_id or not user_id:
|
||||
logger.warning(
|
||||
f"[ARI org={self.organization_id}] StasisStart missing required args: "
|
||||
f"workflow_run_id={workflow_run_id}, workflow_id={workflow_id}, user_id={user_id}"
|
||||
if channel_state == "Ring":
|
||||
# Inbound call — arrived from outside, not yet answered
|
||||
asyncio.create_task(
|
||||
self._handle_inbound_stasis_start(channel_id, channel_state, event)
|
||||
)
|
||||
return
|
||||
else:
|
||||
# Outbound call (state == "Up") — originated by us
|
||||
# Parse args to extract workflow context
|
||||
args_dict = {}
|
||||
for arg in app_args:
|
||||
for pair in arg.split(","):
|
||||
if "=" in pair:
|
||||
key, value = pair.split("=", 1)
|
||||
args_dict[key.strip()] = value.strip()
|
||||
|
||||
# Start pipeline connection in background task
|
||||
asyncio.create_task(
|
||||
self._handle_stasis_start(
|
||||
channel_id, channel_state, workflow_run_id, workflow_id, user_id
|
||||
workflow_run_id = args_dict.get("workflow_run_id")
|
||||
workflow_id = args_dict.get("workflow_id")
|
||||
user_id = args_dict.get("user_id")
|
||||
|
||||
if not workflow_run_id or not workflow_id or not user_id:
|
||||
logger.warning(
|
||||
f"[ARI org={self.organization_id}] StasisStart missing required args: "
|
||||
f"workflow_run_id={workflow_run_id}, workflow_id={workflow_id}, user_id={user_id}"
|
||||
)
|
||||
return
|
||||
|
||||
# Start pipeline connection in background task
|
||||
asyncio.create_task(
|
||||
self._handle_stasis_start(
|
||||
channel_id, channel_state, workflow_run_id, workflow_id, user_id
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
elif event_type == "StasisEnd":
|
||||
logger.info(
|
||||
|
|
@ -345,6 +383,7 @@ class ARIConnection:
|
|||
)
|
||||
ext_channel_id = result.get("id", "")
|
||||
if ext_channel_id:
|
||||
await self._mark_ext_channel(ext_channel_id)
|
||||
logger.info(
|
||||
f"[ARI org={self.organization_id}] Created external media channel: {ext_channel_id}"
|
||||
)
|
||||
|
|
@ -374,6 +413,93 @@ class ARIConnection:
|
|||
)
|
||||
return bridge_id
|
||||
|
||||
async def _handle_inbound_stasis_start(
|
||||
self, channel_id: str, channel_state: str, event: dict
|
||||
):
|
||||
"""Handle an inbound call (StasisStart with state=Ring).
|
||||
|
||||
Validates quota, creates a workflow run, then delegates to the
|
||||
standard answer→externalMedia→bridge pipeline.
|
||||
"""
|
||||
channel = event.get("channel", {})
|
||||
caller_number = channel.get("caller", {}).get("number", "unknown")
|
||||
called_number = channel.get("dialplan", {}).get("exten", "unknown")
|
||||
|
||||
try:
|
||||
# 1. Check inbound_workflow_id is configured
|
||||
if not self.inbound_workflow_id:
|
||||
logger.warning(
|
||||
f"[ARI org={self.organization_id}] Inbound call on channel {channel_id} "
|
||||
f"but no inbound_workflow_id configured — hanging up"
|
||||
)
|
||||
await self._delete_channel(channel_id)
|
||||
return
|
||||
|
||||
# 2. Load workflow to get user_id and verify organization
|
||||
workflow = await db_client.get_workflow(
|
||||
self.inbound_workflow_id, organization_id=self.organization_id
|
||||
)
|
||||
if not workflow:
|
||||
logger.warning(
|
||||
f"[ARI org={self.organization_id}] Workflow {self.inbound_workflow_id} "
|
||||
f"not found or doesn't belong to this organization — hanging up"
|
||||
)
|
||||
await self._delete_channel(channel_id)
|
||||
return
|
||||
|
||||
user_id = workflow.user_id
|
||||
|
||||
# 3. Check quota
|
||||
quota_result = await check_dograh_quota_by_user_id(user_id)
|
||||
if not quota_result.has_quota:
|
||||
logger.warning(
|
||||
f"[ARI org={self.organization_id}] Quota exceeded for user {user_id} "
|
||||
f"— hanging up inbound call {channel_id}"
|
||||
)
|
||||
await self._delete_channel(channel_id)
|
||||
return
|
||||
|
||||
# 4. Create workflow run
|
||||
call_id = channel_id
|
||||
workflow_run = await db_client.create_workflow_run(
|
||||
name=f"ARI Inbound {caller_number}",
|
||||
workflow_id=self.inbound_workflow_id,
|
||||
mode=WorkflowRunMode.ARI.value,
|
||||
user_id=user_id,
|
||||
call_type=CallType.INBOUND,
|
||||
initial_context={
|
||||
"caller_number": caller_number,
|
||||
"called_number": called_number,
|
||||
"direction": "inbound",
|
||||
"call_id": call_id,
|
||||
"provider": "ari",
|
||||
},
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[ARI org={self.organization_id}] Created inbound workflow run "
|
||||
f"{workflow_run.id} for channel {channel_id} "
|
||||
f"(caller={caller_number}, called={called_number})"
|
||||
)
|
||||
|
||||
# 5. Delegate to the standard pipeline
|
||||
await self._handle_stasis_start(
|
||||
channel_id,
|
||||
channel_state,
|
||||
str(workflow_run.id),
|
||||
str(self.inbound_workflow_id),
|
||||
str(user_id),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[ARI org={self.organization_id}] Error handling inbound StasisStart "
|
||||
f"for channel {channel_id}: {e}"
|
||||
)
|
||||
try:
|
||||
await self._delete_channel(channel_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _handle_stasis_start(
|
||||
self,
|
||||
channel_id: str,
|
||||
|
|
@ -588,9 +714,15 @@ class ARIManager:
|
|||
app_name = config["app_name"]
|
||||
app_password = config["app_password"]
|
||||
ws_client_name = config["ws_client_name"]
|
||||
inbound_workflow_id = config.get("inbound_workflow_id")
|
||||
|
||||
conn = ARIConnection(
|
||||
org_id, ari_endpoint, app_name, app_password, ws_client_name
|
||||
org_id,
|
||||
ari_endpoint,
|
||||
app_name,
|
||||
app_password,
|
||||
ws_client_name,
|
||||
inbound_workflow_id=inbound_workflow_id,
|
||||
)
|
||||
key = conn.connection_key
|
||||
|
||||
|
|
@ -604,9 +736,12 @@ class ARIManager:
|
|||
self._connections[key] = conn
|
||||
await conn.start()
|
||||
else:
|
||||
# Existing configuration - check if password changed
|
||||
# Existing configuration - check if password or inbound_workflow_id changed
|
||||
existing = self._connections[key]
|
||||
if existing.app_password != app_password:
|
||||
if (
|
||||
existing.app_password != app_password
|
||||
or existing.inbound_workflow_id != inbound_workflow_id
|
||||
):
|
||||
logger.info(
|
||||
f"[ARI Manager] Config changed for org {org_id}, reconnecting..."
|
||||
)
|
||||
|
|
@ -666,6 +801,7 @@ class ARIManager:
|
|||
"app_name": app_name,
|
||||
"app_password": app_password,
|
||||
"ws_client_name": ws_client_name,
|
||||
"inbound_workflow_id": value.get("inbound_workflow_id"),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ async def load_telephony_config(organization_id: int) -> Dict[str, Any]:
|
|||
"ari_endpoint": config.value.get("ari_endpoint"),
|
||||
"app_name": config.value.get("app_name"),
|
||||
"app_password": config.value.get("app_password"),
|
||||
"inbound_workflow_id": config.value.get("inbound_workflow_id"),
|
||||
"from_numbers": config.value.get("from_numbers", []),
|
||||
}
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class ARIProvider(TelephonyProvider):
|
|||
self.ari_endpoint = config.get("ari_endpoint", "").rstrip("/")
|
||||
self.app_name = config.get("app_name", "")
|
||||
self.app_password = config.get("app_password", "")
|
||||
self.inbound_workflow_id = config.get("inbound_workflow_id")
|
||||
self.from_numbers = config.get("from_numbers", [])
|
||||
|
||||
if isinstance(self.from_numbers, str):
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@
|
|||
"integrations/telephony/vonage",
|
||||
"integrations/telephony/cloudonix",
|
||||
"integrations/telephony/vobiz",
|
||||
"integrations/telephony/asterisk-ari",
|
||||
"integrations/telephony/webhooks",
|
||||
"integrations/telephony/custom"
|
||||
]
|
||||
|
|
|
|||
215
docs/integrations/telephony/asterisk-ari.mdx
Normal file
215
docs/integrations/telephony/asterisk-ari.mdx
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
---
|
||||
title: "Asterisk ARI Integration"
|
||||
description: "Connect Dograh AI to your Asterisk PBX using the Asterisk REST Interface (ARI)"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Asterisk ARI (Asterisk REST Interface) allows you to connect Dograh AI voice agents to your existing Asterisk PBX. ARI provides a WebSocket-based event model for controlling calls via Stasis applications, giving Dograh full control over call flow and audio streaming.
|
||||
|
||||
This guide focuses on the Dograh-specific configuration. For general Asterisk installation and administration, refer to the [official Asterisk documentation](https://docs.asterisk.org/).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before setting up the ARI integration, ensure you have:
|
||||
|
||||
- A running Asterisk instance (version 16 or later recommended)
|
||||
- ARI module enabled in Asterisk
|
||||
- `chan_websocket` (WebSocket channel driver) enabled in your Asterisk build
|
||||
- Network connectivity between your Dograh instance and Asterisk
|
||||
- Dograh AI instance running and accessible
|
||||
|
||||
<Note>
|
||||
If you compiled Asterisk from source, ensure `chan_websocket` is included during the build. This module is required for external media streaming between Asterisk and Dograh. Refer to the [Asterisk build system documentation](https://docs.asterisk.org/) for details on enabling modules.
|
||||
</Note>
|
||||
|
||||
## Asterisk Configuration
|
||||
|
||||
The following Asterisk configuration files need to be set up to work with Dograh. These are minimal examples focused on the Dograh integration -- refer to the [Asterisk documentation](https://docs.asterisk.org/) for full configuration details.
|
||||
|
||||
### Enable ARI (`ari.conf`)
|
||||
|
||||
Create an ARI user that Dograh will use to authenticate:
|
||||
|
||||
```ini
|
||||
[general]
|
||||
enabled = yes
|
||||
|
||||
[dograh]
|
||||
type = user
|
||||
read_only = no
|
||||
password = your_secure_password
|
||||
```
|
||||
|
||||
<Note>
|
||||
The username (section name, e.g., `dograh`) and password here must match the **Stasis App Name** and **App Password** you configure in Dograh.
|
||||
</Note>
|
||||
|
||||
### Enable the HTTP Server (`http.conf`)
|
||||
|
||||
ARI requires the Asterisk HTTP server to be enabled:
|
||||
|
||||
```ini
|
||||
[general]
|
||||
enabled = yes
|
||||
bindaddr = 0.0.0.0
|
||||
bindport = 8088
|
||||
```
|
||||
|
||||
### Configure the Stasis Dialplan (`extensions.conf`)
|
||||
|
||||
Route incoming calls to your Stasis application so Dograh can handle them:
|
||||
|
||||
```ini
|
||||
[from-external]
|
||||
exten => _X.,1,NoOp(Incoming call to ${EXTEN})
|
||||
same => n,Stasis(dograh)
|
||||
same => n,Hangup()
|
||||
```
|
||||
|
||||
Replace `dograh` with the app name you configured in `ari.conf` and in Dograh.
|
||||
|
||||
### Configure External Media Streaming (`websocket_client.conf`)
|
||||
|
||||
Dograh uses Asterisk's external media streaming to send and receive audio over WebSocket. Configure a WebSocket client connection that points to your Dograh instance:
|
||||
|
||||
```ini
|
||||
[dograh_staging]
|
||||
type = websocket_client
|
||||
uri = ws://your-dograh-host:port/ws/audio
|
||||
protocols = audio
|
||||
```
|
||||
|
||||
<Note>
|
||||
The section name (e.g., `dograh_staging`) is the **WebSocket Client Name** you'll enter in the Dograh telephony configuration. This name tells Asterisk which WebSocket connection to use for external media streaming during calls.
|
||||
</Note>
|
||||
|
||||
Refer to the [Asterisk WebSocket documentation](https://docs.asterisk.org/) for additional `websocket_client.conf` options and TLS configuration.
|
||||
|
||||
## Configuration in Dograh
|
||||
|
||||
### Step 1: Navigate to Telephony Settings
|
||||
|
||||
1. Go to **Workflow** → **Phone Call** → **Configure Telephony**
|
||||
2. Select **Asterisk (ARI)** as your provider
|
||||
|
||||
### Step 2: Enter Your ARI Credentials
|
||||
|
||||
Configure the following fields:
|
||||
|
||||
| Field | Description | Example |
|
||||
|-------|-------------|---------|
|
||||
| **ARI Endpoint URL** | HTTP base URL of your Asterisk ARI server | `http://asterisk.example.com:8088` |
|
||||
| **Stasis App Name** | The ARI username configured in `ari.conf` | `dograh` |
|
||||
| **App Password** | The ARI password configured in `ari.conf` | `your_secure_password` |
|
||||
| **WebSocket Client Name** | The connection name from `websocket_client.conf` | `dograh_staging` |
|
||||
| **Inbound Workflow ID** | The workflow to activate for inbound calls (optional) | `42` |
|
||||
| **SIP Extensions / Numbers** | Optional SIP extensions or trunk numbers for outbound calls | `PJSIP/6001` or `6001` |
|
||||
|
||||
### Step 3: Save and Test
|
||||
|
||||
1. Click **Save Configuration**
|
||||
2. Create a test workflow
|
||||
3. Initiate a test call to verify the connection
|
||||
|
||||
## Inbound Calling
|
||||
|
||||
Unlike other telephony providers that use HTTP webhooks for inbound calls, ARI delivers inbound calls as **StasisStart events on the ARI WebSocket**. Dograh automatically detects these events and activates the configured workflow.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. An external call arrives at Asterisk and the dialplan routes it to `Stasis(dograh)`
|
||||
2. Asterisk fires a StasisStart event over the ARI WebSocket with the channel in `Ring` state
|
||||
3. Dograh identifies this as an inbound call, validates your quota, and creates a workflow run
|
||||
4. The call is answered, bridged to an external media channel, and your voice agent workflow begins
|
||||
|
||||
### Setting Up Inbound Calls
|
||||
|
||||
**Step 1: Configure the Asterisk dialplan**
|
||||
|
||||
Ensure your dialplan routes inbound calls to the Stasis application as shown in the [dialplan configuration above](#configure-the-stasis-dialplan-extensionsconf).
|
||||
|
||||
**Step 2: Set the Inbound Workflow ID in Dograh**
|
||||
|
||||
1. Go to **Workflow** → **Phone Call** → **Configure Telephony**
|
||||
2. In the ARI configuration, enter the **Inbound Workflow ID** — this is the ID of the workflow you want to activate when an inbound call arrives
|
||||
3. Click **Save Configuration**
|
||||
|
||||
You can find a workflow's ID in the URL when viewing it (e.g., `/workflows/42` means the ID is `42`).
|
||||
|
||||
<Note>
|
||||
If no Inbound Workflow ID is configured, inbound calls will be hung up immediately. You must set this field for inbound calling to work.
|
||||
</Note>
|
||||
|
||||
**Step 3: Test an inbound call**
|
||||
|
||||
Place a call to a number or extension routed to your Stasis application. You should see the workflow activate and the voice agent respond.
|
||||
|
||||
### Inbound Call Context
|
||||
|
||||
When an inbound call activates a workflow, the following context is available to your workflow:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `caller_number` | The caller's phone number or extension |
|
||||
| `called_number` | The dialed number or extension |
|
||||
| `direction` | Always `inbound` |
|
||||
| `call_id` | The Asterisk channel ID |
|
||||
| `provider` | Always `ari` |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Cannot connect to ARI endpoint">
|
||||
- Verify the ARI endpoint URL is correct and reachable from your Dograh instance
|
||||
- Check that the Asterisk HTTP server is running (`http.conf` has `enabled = yes`)
|
||||
- Ensure firewall rules allow traffic on the ARI port (default: 8088)
|
||||
- Confirm the ARI module is loaded: run `module show like res_ari` in the Asterisk CLI
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Authentication failed">
|
||||
- Verify the Stasis App Name matches the ARI user section name in `ari.conf`
|
||||
- Check the App Password matches the password in `ari.conf`
|
||||
- Ensure there are no extra spaces in the credentials
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="No audio during calls">
|
||||
- Verify `chan_websocket` is loaded: run `module show like chan_websocket` in the Asterisk CLI
|
||||
- Check that `websocket_client.conf` is correctly configured with the right Dograh URI
|
||||
- Ensure the WebSocket Client Name in Dograh matches the section name in `websocket_client.conf`
|
||||
- Verify network connectivity and firewall rules allow WebSocket traffic between Asterisk and Dograh
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Calls not reaching Dograh">
|
||||
- Ensure the dialplan routes calls to `Stasis(your_app_name)`
|
||||
- Verify the app name in the dialplan matches the ARI user in `ari.conf`
|
||||
- Check Asterisk CLI for errors: `asterisk -rvvv`
|
||||
- Confirm the ARI WebSocket connection is active
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Inbound calls are immediately hung up">
|
||||
- Verify the **Inbound Workflow ID** is set in your ARI telephony configuration
|
||||
- Confirm the workflow ID exists and belongs to the same organization as the ARI config
|
||||
- Check that your organization has available quota
|
||||
- Review Dograh logs for warnings mentioning "no inbound_workflow_id configured"
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="WebSocket client connection issues">
|
||||
- Check the URI in `websocket_client.conf` points to the correct Dograh host and port
|
||||
- Verify the Dograh instance is running and accepting WebSocket connections
|
||||
- If using TLS, ensure certificates are correctly configured on both sides
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Keep your Asterisk instance on the same network or a low-latency connection to Dograh for optimal audio quality
|
||||
- Use strong passwords for ARI authentication
|
||||
- Restrict ARI access to known IP addresses using firewall rules
|
||||
- Monitor Asterisk logs alongside Dograh logs when debugging call issues
|
||||
- Keep Asterisk updated to the latest stable version for security and compatibility
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Asterisk Documentation](https://docs.asterisk.org/) -- official reference for all Asterisk configuration
|
||||
- [ARI Documentation](https://docs.asterisk.org/Configuration/Interfaces/Asterisk-REST-Interface-ARI/) -- detailed ARI configuration and API reference
|
||||
|
|
@ -19,6 +19,9 @@ Dograh AI supports inbound calling across all supported telephony providers. Whe
|
|||
<Card title="Vobiz" href="/integrations/telephony/vobiz">
|
||||
Cloud-based telephony with global reach and competitive pricing
|
||||
</Card>
|
||||
<Card title="Asterisk ARI" href="/integrations/telephony/asterisk-ari">
|
||||
Connect to your own Asterisk PBX via the Asterisk REST Interface
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Note>
|
||||
|
|
@ -46,6 +49,7 @@ The telephony configuration for inbound calling is **identical** to outbound cal
|
|||
- [Twilio Configuration](/integrations/telephony/twilio#configuration)
|
||||
- [Cloudonix Configuration](/integrations/telephony/cloudonix#configuration)
|
||||
- [Vobiz Configuration](/integrations/telephony/vobiz#configuration)
|
||||
- [Asterisk ARI Configuration](/integrations/telephony/asterisk-ari#configuration-in-dograh)
|
||||
|
||||
### Step 2: Get Your Workflow Webhook URL
|
||||
|
||||
|
|
@ -75,6 +79,7 @@ Each telephony provider requires additional configuration to route incoming call
|
|||
- [Vonage Inbound Setup](/integrations/telephony/vonage#inbound-calling-setup)
|
||||
- [Cloudonix Inbound Setup](/integrations/telephony/cloudonix#inbound-calling-setup)
|
||||
- [Vobiz Inbound Setup](/integrations/telephony/vobiz#inbound-calling-setup)
|
||||
- [Asterisk ARI Inbound Setup](/integrations/telephony/asterisk-ari#inbound-calling)
|
||||
|
||||
|
||||
## Testing Inbound Calls
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ interface TelephonyConfigForm {
|
|||
app_name?: string;
|
||||
app_password?: string;
|
||||
ws_client_name?: string;
|
||||
inbound_workflow_id?: number;
|
||||
// Common field - multiple phone numbers
|
||||
from_numbers: string[];
|
||||
}
|
||||
|
|
@ -155,6 +156,10 @@ export default function ConfigureTelephonyPage() {
|
|||
setValue("app_name", ariConfig.app_name);
|
||||
setValue("app_password", ariConfig.app_password);
|
||||
setValue("ws_client_name", ariConfig.ws_client_name);
|
||||
setValue(
|
||||
"inbound_workflow_id",
|
||||
typeof ariConfig.inbound_workflow_id === "number" ? ariConfig.inbound_workflow_id : undefined
|
||||
);
|
||||
setValue("from_numbers", ariConfig.from_numbers?.length > 0 ? ariConfig.from_numbers : [""]);
|
||||
}
|
||||
}
|
||||
|
|
@ -257,6 +262,7 @@ export default function ConfigureTelephonyPage() {
|
|||
app_name: data.app_name!,
|
||||
app_password: data.app_password!,
|
||||
ws_client_name: data.ws_client_name || "",
|
||||
inbound_workflow_id: data.inbound_workflow_id || undefined,
|
||||
} as AriConfigurationRequest;
|
||||
}
|
||||
|
||||
|
|
@ -913,6 +919,19 @@ export default function ConfigureTelephonyPage() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="inbound_workflow_id">Inbound Workflow ID (Optional)</Label>
|
||||
<Input
|
||||
id="inbound_workflow_id"
|
||||
type="number"
|
||||
placeholder="e.g. 42"
|
||||
{...register("inbound_workflow_id", { valueAsNumber: true })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Workflow to activate for inbound calls received via ARI
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>SIP Extensions / Numbers (Optional)</Label>
|
||||
{fromNumbers.map((number, index) => (
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ export type AriConfigurationRequest = {
|
|||
* websocket_client.conf connection name for externalMedia (e.g., dograh_staging)
|
||||
*/
|
||||
ws_client_name?: string;
|
||||
/**
|
||||
* Workflow ID for inbound calls
|
||||
*/
|
||||
inbound_workflow_id?: number | null;
|
||||
/**
|
||||
* List of SIP extensions/numbers for outbound calls (optional)
|
||||
*/
|
||||
|
|
@ -55,6 +59,7 @@ export type AriConfigurationResponse = {
|
|||
app_name: string;
|
||||
app_password: string;
|
||||
ws_client_name?: string;
|
||||
inbound_workflow_id?: number | null;
|
||||
from_numbers: Array<string>;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue