feat: integrate Telnyx telephony for outbound and inbound calling (#206)

* feat: integrate Telnyx telephony for outbound and inbound calling

* chore: remove redundant code

---------

Co-authored-by: Abhishek <abhishek@a6k.me>
This commit is contained in:
Sabiha Khan 2026-03-25 18:01:41 +05:30 committed by GitHub
parent dc800bdd63
commit 5b820cb0ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1050 additions and 12 deletions

View file

@ -13,6 +13,8 @@ import type {
CloudonixConfigurationRequest,
CloudonixConfigurationResponse,
TelephonyConfigurationResponse,
TelnyxConfigurationRequest,
TelnyxConfigurationResponse,
TwilioConfigurationRequest,
VobizConfigurationRequest,
VonageConfigurationRequest
@ -50,6 +52,9 @@ interface TelephonyConfigForm {
// Vobiz fields
auth_id?: string;
vobiz_auth_token?: string;
// Telnyx fields
telnyx_api_key?: string;
connection_id?: string;
// Cloudonix fields
bearer_token?: string;
domain_id?: string;
@ -161,6 +166,13 @@ export default function ConfigureTelephonyPage() {
typeof ariConfig.inbound_workflow_id === "number" ? ariConfig.inbound_workflow_id : undefined
);
setValue("from_numbers", ariConfig.from_numbers?.length > 0 ? ariConfig.from_numbers : [""]);
} else if ((response.data as TelephonyConfigurationResponse)?.telnyx) {
const telnyxConfig = (response.data as TelephonyConfigurationResponse).telnyx as TelnyxConfigurationResponse;
setHasExistingConfig(true);
setValue("provider", "telnyx");
setValue("telnyx_api_key", telnyxConfig.api_key);
setValue("connection_id", telnyxConfig.connection_id);
setValue("from_numbers", telnyxConfig.from_numbers?.length > 0 ? telnyxConfig.from_numbers : [""]);
}
}
} catch (error) {
@ -183,7 +195,8 @@ export default function ConfigureTelephonyPage() {
| VonageConfigurationRequest
| VobizConfigurationRequest
| CloudonixConfigurationRequest
| AriConfigurationRequest;
| AriConfigurationRequest
| TelnyxConfigurationRequest;
const filteredNumbers = data.from_numbers.filter(n => n.trim() !== "");
@ -201,7 +214,7 @@ export default function ConfigureTelephonyPage() {
let pattern: RegExp;
let formatMessage: string;
if (data.provider === "twilio") {
if (data.provider === "twilio" || data.provider === "telnyx") {
pattern = twilioPattern;
formatMessage = "with + prefix (e.g., +1234567890)";
} else if (data.provider === "cloudonix") {
@ -246,6 +259,13 @@ export default function ConfigureTelephonyPage() {
auth_id: data.auth_id,
auth_token: data.vobiz_auth_token,
} as VobizConfigurationRequest;
} else if (data.provider === "telnyx") {
requestBody = {
provider: data.provider,
from_numbers: filteredNumbers,
api_key: data.telnyx_api_key!,
connection_id: data.connection_id!,
} as TelnyxConfigurationRequest;
} else if (data.provider === "cloudonix") {
requestBody = {
provider: data.provider,
@ -312,13 +332,21 @@ export default function ConfigureTelephonyPage() {
? "Vonage"
: selectedProvider === "vobiz"
? "Vobiz"
: selectedProvider === "telnyx"
? "Telnyx"
: selectedProvider === "ari"
? "Asterisk ARI"
: "Cloudonix"}{" "}
Setup Guide
</CardTitle>
<CardDescription>
{selectedProvider === "ari" ? (
{selectedProvider === "telnyx" ? (
<>
Telnyx is a cloud communications platform providing programmable voice, messaging,
and networking services. Use the Call Control API to build voice applications
with real-time WebSocket audio streaming.
</>
) : selectedProvider === "ari" ? (
<>
Connect Dograh to your Asterisk PBX using the Asterisk REST Interface (ARI).
ARI provides a WebSocket-based event model for controlling calls via Stasis applications.
@ -368,7 +396,26 @@ export default function ConfigureTelephonyPage() {
</CardDescription>
</CardHeader>
<CardContent>
{selectedProvider === "ari" ? (
{selectedProvider === "telnyx" ? (
<div className="space-y-4 text-sm">
<div>
<h4 className="font-semibold mb-2">Getting Started with Telnyx:</h4>
<ol className="list-decimal list-inside space-y-1 text-muted-foreground">
<li>Sign up at <a href="https://telnyx.com" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">telnyx.com</a> and create an API Key in the Mission Control Portal</li>
<li>Create a Call Control Application under Voice &gt; Programmable Voice</li>
<li>Note the Connection ID (Application ID) from your Call Control App</li>
<li>Purchase a phone number and assign it to your Call Control Application</li>
<li>Enter your API Key, Connection ID, and phone numbers below</li>
</ol>
</div>
<div className="bg-muted border border-border rounded p-3">
<p className="text-sm">
<strong>Note:</strong> Telnyx uses the Call Control API with WebSocket-based
bidirectional audio streaming. Phone numbers must be in E.164 format (e.g., +1234567890).
</p>
</div>
</div>
) : selectedProvider === "ari" ? (
<div className="space-y-4 text-sm">
<div>
<h4 className="font-semibold mb-2">Getting Started with Asterisk ARI:</h4>
@ -469,6 +516,7 @@ export default function ConfigureTelephonyPage() {
<SelectItem value="twilio">Twilio</SelectItem>
<SelectItem value="vonage">Vonage</SelectItem>
<SelectItem value="vobiz">Vobiz</SelectItem>
<SelectItem value="telnyx">Telnyx</SelectItem>
<SelectItem value="cloudonix">Cloudonix</SelectItem>
<SelectItem value="ari">Asterisk (ARI)</SelectItem>
</SelectContent>
@ -744,6 +792,97 @@ export default function ConfigureTelephonyPage() {
</>
)}
{/* Telnyx-specific fields */}
{selectedProvider === "telnyx" && (
<>
<div className="space-y-2">
<Label htmlFor="telnyx_api_key">API Key</Label>
<Input
id="telnyx_api_key"
type="password"
autoComplete="current-password"
placeholder={
hasExistingConfig
? "Leave masked to keep existing"
: "Enter your Telnyx API key"
}
{...register("telnyx_api_key", {
required: selectedProvider === "telnyx" && !hasExistingConfig
? "API Key is required"
: false,
})}
/>
{errors.telnyx_api_key && (
<p className="text-sm text-red-500">
{errors.telnyx_api_key.message}
</p>
)}
<p className="text-xs text-muted-foreground">
Found in the Telnyx Mission Control Portal under Auth &gt; API Keys
</p>
</div>
<div className="space-y-2">
<Label htmlFor="connection_id">Connection ID (Application ID)</Label>
<Input
id="connection_id"
placeholder="1234567890"
{...register("connection_id", {
required: selectedProvider === "telnyx"
? "Connection ID is required"
: false,
})}
/>
{errors.connection_id && (
<p className="text-sm text-red-500">
{errors.connection_id.message}
</p>
)}
<p className="text-xs text-muted-foreground">
The ID of your Call Control Application in Telnyx Mission Control
</p>
</div>
<div className="space-y-2">
<Label>CLI Phone Numbers</Label>
{fromNumbers.map((number, index) => (
<div key={index} className="flex gap-2">
<Input
autoComplete="tel"
placeholder="+1234567890"
value={number}
onChange={(e) => updatePhoneNumber(index, e.target.value)}
/>
{fromNumbers.length > 1 && (
<Button
type="button"
variant="outline"
size="icon"
onClick={() => removePhoneNumber(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={addPhoneNumber}
>
<Plus className="h-4 w-4 mr-2" />
Add Phone Number
</Button>
{fromNumbers.some(n => n.trim() !== "" && !/^\+[1-9]\d{1,14}$/.test(n)) && (
<p className="text-sm text-red-500">
Enter valid phone numbers with country code (e.g., +1234567890)
</p>
)}
</div>
</>
)}
{/* Cloudonix-specific fields */}
{selectedProvider === "cloudonix" && (
<>

View file

@ -57,7 +57,7 @@ export const PhoneCallDialog = ({
try {
const configResponse = await getTelephonyConfigurationApiV1OrganizationsTelephonyConfigGet({});
if (configResponse.error || (!configResponse.data?.twilio && !configResponse.data?.vonage && !configResponse.data?.vobiz && !configResponse.data?.cloudonix && !configResponse.data?.ari)) {
if (configResponse.error || (!configResponse.data?.twilio && !configResponse.data?.vonage && !configResponse.data?.vobiz && !configResponse.data?.cloudonix && !configResponse.data?.ari && !configResponse.data?.telnyx)) {
setNeedsConfiguration(true);
} else {
setNeedsConfiguration(false);

File diff suppressed because one or more lines are too long

View file

@ -1030,6 +1030,36 @@ export type TelephonyConfigurationResponse = {
vobiz?: VobizConfigurationResponse | null;
cloudonix?: CloudonixConfigurationResponse | null;
ari?: AriConfigurationResponse | null;
telnyx?: TelnyxConfigurationResponse | null;
};
/**
* Request schema for Telnyx configuration.
*/
export type TelnyxConfigurationRequest = {
provider?: string;
/**
* Telnyx API Key
*/
api_key: string;
/**
* Telnyx Call Control Application ID (connection_id)
*/
connection_id: string;
/**
* List of Telnyx phone numbers (E.164 format)
*/
from_numbers: Array<string>;
};
/**
* Response schema for Telnyx configuration with masked sensitive fields.
*/
export type TelnyxConfigurationResponse = {
provider: string;
api_key: string;
connection_id: string;
from_numbers: Array<string>;
};
export type TestSessionResponse = {
@ -1611,6 +1641,35 @@ export type HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflow
200: unknown;
};
export type HandleTelnyxEventsApiV1TelephonyTelnyxEventsWorkflowRunIdPostData = {
body?: never;
path: {
workflow_run_id: number;
};
query?: never;
url: '/api/v1/telephony/telnyx/events/{workflow_run_id}';
};
export type HandleTelnyxEventsApiV1TelephonyTelnyxEventsWorkflowRunIdPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type HandleTelnyxEventsApiV1TelephonyTelnyxEventsWorkflowRunIdPostError = HandleTelnyxEventsApiV1TelephonyTelnyxEventsWorkflowRunIdPostErrors[keyof HandleTelnyxEventsApiV1TelephonyTelnyxEventsWorkflowRunIdPostErrors];
export type HandleTelnyxEventsApiV1TelephonyTelnyxEventsWorkflowRunIdPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData = {
body?: never;
path: {
@ -1775,6 +1834,7 @@ export type HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData = {
'x-vobiz-signature'?: string | null;
'x-vobiz-timestamp'?: string | null;
'x-cx-apikey'?: string | null;
'telnyx-signature-ed25519'?: string | null;
};
path: {
workflow_id: number;
@ -3792,7 +3852,7 @@ export type GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetRespons
export type GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse = GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponses[keyof GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponses];
export type SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData = {
body: TwilioConfigurationRequest | VonageConfigurationRequest | VobizConfigurationRequest | CloudonixConfigurationRequest | AriConfigurationRequest;
body: TwilioConfigurationRequest | VonageConfigurationRequest | VobizConfigurationRequest | CloudonixConfigurationRequest | AriConfigurationRequest | TelnyxConfigurationRequest;
headers?: {
authorization?: string | null;
'X-API-Key'?: string | null;

View file

@ -9,7 +9,8 @@ export const WORKFLOW_RUN_MODES = {
CLOUDONIX: 'cloudonix',
WEBRTC: 'webrtc',
SMALL_WEBRTC: 'smallwebrtc',
ARI: 'ari'
ARI: 'ari',
TELNYX: 'telnyx'
} as const;
export type WorkflowRunMode = typeof WORKFLOW_RUN_MODES[keyof typeof WORKFLOW_RUN_MODES];