diff --git a/api/alembic/versions/b79f19f68157_add_call_type_column_to_workflow_runs.py b/api/alembic/versions/b79f19f68157_add_call_type_column_to_workflow_runs.py new file mode 100644 index 0000000..9a35271 --- /dev/null +++ b/api/alembic/versions/b79f19f68157_add_call_type_column_to_workflow_runs.py @@ -0,0 +1,45 @@ +"""add call_type column to workflow_runs + +Revision ID: b79f19f68157 +Revises: 488eb58e4e6e +Create Date: 2026-01-08 21:20:17.298334 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = "b79f19f68157" +down_revision: Union[str, None] = "488eb58e4e6e" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create the workflow_call_type enum + sa.Enum("inbound", "outbound", name="workflow_call_type").create(op.get_bind()) + + # Add call_type column to workflow_runs table + op.add_column( + "workflow_runs", + sa.Column( + "call_type", + postgresql.ENUM( + "inbound", "outbound", name="workflow_call_type", create_type=False + ), + server_default=sa.text("'outbound'::workflow_call_type"), + nullable=False, + ), + ) + + +def downgrade() -> None: + # Drop the call_type column + op.drop_column("workflow_runs", "call_type") + + # Drop the workflow_call_type enum + sa.Enum("inbound", "outbound", name="workflow_call_type").drop(op.get_bind()) diff --git a/api/constants.py b/api/constants.py index f3c8e50..366a256 100644 --- a/api/constants.py +++ b/api/constants.py @@ -51,3 +51,28 @@ SENTRY_DSN = os.getenv("SENTRY_DSN") ENABLE_ARI_STASIS = os.getenv("ENABLE_ARI_STASIS", "false").lower() == "true" SERIALIZE_LOG_OUTPUT = os.getenv("SERIALIZE_LOG_OUTPUT", "false").lower() == "true" ENABLE_TELEMETRY = os.getenv("ENABLE_TELEMETRY", "false").lower() == "true" + +# Country code mapping: ISO country code -> international dialing prefix +COUNTRY_CODES = { + "US": "1", # United States + "CA": "1", # Canada + "GB": "44", # United Kingdom + "IN": "91", # India + "AU": "61", # Australia + "DE": "49", # Germany + "FR": "33", # France + "BR": "55", # Brazil + "MX": "52", # Mexico + "IT": "39", # Italy + "ES": "34", # Spain + "NL": "31", # Netherlands + "SE": "46", # Sweden + "NO": "47", # Norway + "DK": "45", # Denmark + "FI": "358", # Finland + "CH": "41", # Switzerland + "AT": "43", # Austria + "BE": "32", # Belgium + "LU": "352", # Luxembourg + "IE": "353", # Ireland +} diff --git a/api/db/models.py b/api/db/models.py index 5bbce61..8de7651 100644 --- a/api/db/models.py +++ b/api/db/models.py @@ -21,6 +21,7 @@ from sqlalchemy import ( from sqlalchemy.orm import declarative_base, relationship from ..enums import ( + CallType, IntegrationAction, ToolCategory, ToolStatus, @@ -324,6 +325,12 @@ class WorkflowRunModel(Base): Enum(*[mode.value for mode in WorkflowRunMode], name="workflow_run_mode"), nullable=False, ) + call_type = Column( + Enum(*[call_type.value for call_type in CallType], name="workflow_call_type"), + nullable=False, + default=CallType.OUTBOUND.value, + server_default=text("'outbound'::workflow_call_type"), + ) state = Column( Enum(*[state.value for state in WorkflowRunState], name="workflow_run_state"), nullable=False, diff --git a/api/db/workflow_run_client.py b/api/db/workflow_run_client.py index 195f318..27cffac 100644 --- a/api/db/workflow_run_client.py +++ b/api/db/workflow_run_client.py @@ -14,7 +14,7 @@ from api.db.models import ( WorkflowModel, WorkflowRunModel, ) -from api.enums import StorageBackend +from api.enums import CallType, StorageBackend from api.schemas.workflow import WorkflowRunResponseSchema @@ -25,6 +25,7 @@ class WorkflowRunClient(BaseDBClient): workflow_id: int, mode: str, user_id: int, + call_type: CallType = CallType.OUTBOUND, initial_context: dict = None, campaign_id: int = None, queued_run_id: int = None, @@ -80,6 +81,7 @@ class WorkflowRunClient(BaseDBClient): campaign_id=campaign_id, queued_run_id=queued_run_id, storage_backend=current_backend.value, + call_type=call_type.value, ) session.add(new_run) try: @@ -288,6 +290,7 @@ class WorkflowRunClient(BaseDBClient): "definition_id": run.definition_id, "initial_context": run.initial_context, "gathered_context": run.gathered_context, + "call_type": run.call_type, } ) for run in result.scalars().all() diff --git a/api/enums.py b/api/enums.py index 4c0295d..74b46b7 100644 --- a/api/enums.py +++ b/api/enums.py @@ -12,6 +12,11 @@ class Environment(Enum): TEST = "test" +class CallType(Enum): + INBOUND = "inbound" + OUTBOUND = "outbound" + + class WorkflowRunMode(Enum): TWILIO = "twilio" VONAGE = "vonage" diff --git a/api/errors/__init__.py b/api/errors/__init__.py new file mode 100644 index 0000000..cd2511f --- /dev/null +++ b/api/errors/__init__.py @@ -0,0 +1,4 @@ +""" +Errors package for the Dograh API. +Contains centralized error definitions and messages for various domains. +""" diff --git a/api/errors/telephony_errors.py b/api/errors/telephony_errors.py new file mode 100644 index 0000000..b7d18e0 --- /dev/null +++ b/api/errors/telephony_errors.py @@ -0,0 +1,31 @@ +""" +Telephony error constants and messages for inbound call validation. +Centralizes error handling across all telephony providers. +""" + +from enum import Enum + + +class TelephonyError(Enum): + """Telephony validation error types""" + + PROVIDER_MISMATCH = "PROVIDER_MISMATCH" + WORKFLOW_NOT_FOUND = "WORKFLOW_NOT_FOUND" + ACCOUNT_VALIDATION_FAILED = "ACCOUNT_VALIDATION_FAILED" + PHONE_NUMBER_NOT_CONFIGURED = "PHONE_NUMBER_NOT_CONFIGURED" + SIGNATURE_VALIDATION_FAILED = "SIGNATURE_VALIDATION_FAILED" + QUOTA_EXCEEDED = "QUOTA_EXCEEDED" + GENERAL_AUTH_FAILED = "GENERAL_AUTH_FAILED" + VALID = "VALID" + + +# Error messages for organizations (debugging-focused) +TELEPHONY_ERROR_MESSAGES = { + TelephonyError.PROVIDER_MISMATCH: "Configuration error: This phone number is configured for a different telephony provider. Please check your dashboard settings and update your webhook URL configuration.", + TelephonyError.WORKFLOW_NOT_FOUND: "Workflow not found. Please verify the workflow ID in your webhook URL is correct and the workflow exists in your dashboard.", + TelephonyError.ACCOUNT_VALIDATION_FAILED: "Authentication error: Account credentials do not match. Please verify your account SID configuration in the dashboard matches your telephony provider settings.", + TelephonyError.PHONE_NUMBER_NOT_CONFIGURED: "Phone number not configured: This number is not set up for inbound calls in your account. Please add this number to your telephony configuration.", + TelephonyError.SIGNATURE_VALIDATION_FAILED: "Security error: Webhook signature validation failed. Please verify your auth token configuration and ensure requests are coming from your telephony provider.", + TelephonyError.QUOTA_EXCEEDED: "Service temporarily unavailable: Your account has exceeded usage limits. Please contact your administrator or upgrade your plan to continue receiving calls.", + TelephonyError.GENERAL_AUTH_FAILED: "Authentication failed: Please check your webhook URL configuration and ensure your telephony provider settings match your dashboard configuration.", +} diff --git a/api/routes/telephony.py b/api/routes/telephony.py index 53ca780..cb9f756 100644 --- a/api/routes/telephony.py +++ b/api/routes/telephony.py @@ -1,27 +1,42 @@ """ -Generic telephony routes that work with any telephony provider. +Telephony routes - handles all telephony-related endpoints. +Consolidated from split modules for easier maintenance. """ import json -import random +import uuid from datetime import UTC, datetime from typing import Optional from fastapi import APIRouter, Depends, Header, HTTPException, Request, WebSocket from loguru import logger +from pipecat.utils.context import set_current_run_id from pydantic import BaseModel +from sqlalchemy import text +from sqlalchemy.future import select from starlette.responses import HTMLResponse from api.db import db_client -from api.db.models import UserModel -from api.enums import WorkflowRunState +from api.db.models import OrganizationConfigurationModel, UserModel +from api.db.workflow_client import WorkflowClient +from api.db.workflow_run_client import WorkflowRunClient +from api.enums import CallType, OrganizationConfigurationKey, WorkflowRunState +from api.errors.telephony_errors import TelephonyError from api.services.auth.depends import get_user from api.services.campaign.call_dispatcher import campaign_call_dispatcher from api.services.campaign.campaign_event_publisher import get_campaign_event_publisher -from api.services.quota_service import check_dograh_quota -from api.services.telephony.factory import get_telephony_provider +from api.services.quota_service import check_dograh_quota, check_dograh_quota_by_user_id +from api.services.telephony.factory import ( + get_all_telephony_providers, + get_telephony_provider, +) +from api.utils.telephony_helper import ( + generic_hangup_response, + normalize_webhook_data, + numbers_match, + parse_webhook_request, +) from api.utils.tunnel import TunnelURLProvider -from pipecat.utils.context import set_current_run_id router = APIRouter(prefix="/telephony") @@ -122,15 +137,18 @@ async def initiate_call( workflow_run_id = request.workflow_run_id if not workflow_run_id: - workflow_run_name = f"WR-TEL-{random.randint(1000, 9999)}" + numeric_suffix = int(str(uuid.uuid4()).replace("-", "")[:8], 16) % 100000000 + workflow_run_name = f"WR-TEL-OUT-{numeric_suffix:08d}" workflow_run = await db_client.create_workflow_run( workflow_run_name, request.workflow_id, workflow_run_mode, + user_id=user.id, + call_type=CallType.OUTBOUND, initial_context={ "phone_number": phone_number, + "provider": provider.PROVIDER_NAME, }, - user_id=user.id, ) workflow_run_id = workflow_run.id else: @@ -174,6 +192,255 @@ async def initiate_call( return {"message": f"Call initiated successfully with run name {workflow_run_name}"} +async def _verify_organization_phone_number( + phone_number: str, + organization_id: int, + to_country: str = None, + from_country: str = None, +) -> bool: + """ + Verify that a phone number belongs to the specified organization. + + Args: + phone_number: The phone number to verify + organization_id: The organization ID to check against + to_country: ISO country code for the called number (e.g., "US", "IN") + from_country: ISO country code for the caller (e.g., "IN", "GB") + + Returns: + True if the phone number belongs to the organization, False otherwise + """ + try: + async with db_client.async_session() as session: + result = await session.execute( + select(OrganizationConfigurationModel).where( + OrganizationConfigurationModel.organization_id == organization_id, + OrganizationConfigurationModel.key + == OrganizationConfigurationKey.TELEPHONY_CONFIGURATION.value, + ) + ) + + config = result.scalars().first() + + if not config or not config.value: + logger.warning( + f"No telephony configuration found for organization {organization_id}" + ) + return False + + from_numbers = config.value.get("from_numbers", []) + logger.debug( + f"Organization {organization_id} has from_numbers: {from_numbers}" + ) + + for configured_number in from_numbers: + if numbers_match( + phone_number, configured_number, to_country, from_country + ): + logger.info( + f"Phone number {phone_number} verified for organization {organization_id} " + f"(matches {configured_number}, to_country={to_country}, from_country={from_country})" + ) + return True + + logger.warning( + f"Phone number {phone_number} not found in organization {organization_id} from_numbers: {from_numbers} " + f"(to_country={to_country}, from_country={from_country})" + ) + return False + + except Exception as e: + logger.error( + f"Error verifying phone number {phone_number} for organization {organization_id}: {e}" + ) + return False + + +async def _detect_provider(webhook_data: dict, headers: dict): + """Detect which telephony provider can handle this webhook""" + provider_classes = await get_all_telephony_providers() + + for provider_class in provider_classes: + if provider_class.can_handle_webhook(webhook_data, headers): + return provider_class + + logger.warning(f"No provider found for webhook data: {webhook_data.keys()}") + return None + + +async def _validate_inbound_request( + workflow_id: int, + provider_class, + normalized_data, + webhook_data: dict, + webhook_body: str = "", + x_twilio_signature: str = None, + x_vobiz_signature: str = None, + x_vobiz_timestamp: str = None, +) -> tuple[bool, TelephonyError, dict, object]: + """ + Validate all aspects of inbound request. + Returns: (is_valid, error_type, workflow_context, provider_instance) + """ + + workflow = await db_client.get_workflow(workflow_id) + if not workflow: + return False, TelephonyError.WORKFLOW_NOT_FOUND, {}, None + + organization_id = workflow.organization_id + user_id = workflow.user_id + provider = normalized_data.provider + + # Validate provider and account_id + validation_result = await _validate_organization_provider_config( + organization_id, provider_class, normalized_data.account_id + ) + if validation_result != TelephonyError.VALID: + return False, validation_result, {}, None + + # Verify phone number belongs to organization + is_valid = await _verify_organization_phone_number( + normalized_data.to_number, + organization_id, + normalized_data.to_country, + normalized_data.from_country, + ) + if not is_valid: + return False, TelephonyError.PHONE_NUMBER_NOT_CONFIGURED, {}, None + + # Verify webhook signature if provided + provider_instance = None + if x_twilio_signature or x_vobiz_signature: + backend_endpoint = await TunnelURLProvider.get_tunnel_url() + webhook_url = ( + f"https://{backend_endpoint}/api/v1/telephony/inbound/{workflow_id}" + ) + + # Get the real telephony provider with actual credentials for signature verification + provider_instance = await get_telephony_provider(organization_id) + + if provider_class.PROVIDER_NAME == "twilio" and x_twilio_signature: + logger.info(f"Verifying Twilio signature for URL: {webhook_url}") + signature_valid = await provider_instance.verify_inbound_signature( + webhook_url, webhook_data, x_twilio_signature + ) + elif provider_class.PROVIDER_NAME == "vobiz" and x_vobiz_signature: + logger.info(f"Verifying Vobiz signature for URL: {webhook_url}") + signature_valid = await provider_instance.verify_inbound_signature( + webhook_url, + webhook_data, + x_vobiz_signature, + x_vobiz_timestamp, + webhook_body, + ) + else: + logger.warning( + f"No signature validation for provider {provider_class.PROVIDER_NAME}" + ) + signature_valid = True + + logger.info(f"Signature validation result: {signature_valid}") + if not signature_valid: + return ( + False, + TelephonyError.SIGNATURE_VALIDATION_FAILED, + {}, + provider_instance, + ) + + # Return success with workflow context + workflow_context = { + "workflow": workflow, + "organization_id": organization_id, + "user_id": user_id, + "provider": provider, + } + return ( + True, + "", + workflow_context, + provider_instance, + ) # TODO: do we still need instance in the client code + + +async def _create_inbound_workflow_run( + workflow_id: int, user_id: int, provider: str, normalized_data, data_source: str +) -> int: + """Create workflow run for inbound call and return run ID""" + call_id = normalized_data.call_id + numeric_suffix = int(str(uuid.uuid4()).replace("-", "")[:8], 16) % 100000000 + workflow_run_name = f"WR-TEL-IN-{numeric_suffix:08d}" + + workflow_run = await db_client.create_workflow_run( + workflow_run_name, + workflow_id, + provider, # Use detected provider as mode + user_id=user_id, + call_type=CallType.INBOUND, + initial_context={ + "caller_number": normalized_data.from_number, + "called_number": normalized_data.to_number, + "direction": "inbound", + "call_id": call_id, + "account_id": normalized_data.account_id, + "provider": provider, + "data_source": data_source, + "from_country": normalized_data.from_country, + "to_country": normalized_data.to_country, + "raw_webhook_data": normalized_data.raw_data, + }, + ) + + logger.info( + f"Created inbound workflow run {workflow_run.id} for {provider} call {call_id}" + ) + return workflow_run.id + + +async def _validate_organization_provider_config( + organization_id: int, provider_class, account_id: str +) -> TelephonyError: + """Validate provider and account_id, returning specific error type""" + if not account_id: + logger.warning( + f"No account_id provided for provider {provider_class.PROVIDER_NAME}" + ) + return TelephonyError.ACCOUNT_VALIDATION_FAILED + + try: + config = await db_client.get_configuration( + organization_id, + OrganizationConfigurationKey.TELEPHONY_CONFIGURATION.value, + ) + + if not config or not config.value: + logger.warning( + f"No telephony configuration found for organization {organization_id}" + ) + return TelephonyError.ACCOUNT_VALIDATION_FAILED + + stored_provider = config.value.get("provider") + if stored_provider != provider_class.PROVIDER_NAME: + logger.warning( + f"Provider mismatch: webhook={provider_class.PROVIDER_NAME}, config={stored_provider}" + ) + return TelephonyError.PROVIDER_MISMATCH + + # Use provider-specific validation + is_valid = provider_class.validate_account_id(config.value, account_id) + if not is_valid: + logger.warning( + f"Account validation failed for {provider_class.PROVIDER_NAME}: webhook={account_id}" + ) + return TelephonyError.ACCOUNT_VALIDATION_FAILED + + return TelephonyError.VALID + + except Exception as e: + logger.error(f"Exception during account validation: {e}") + return TelephonyError.ACCOUNT_VALIDATION_FAILED + + @router.post("/twiml", include_in_schema=False) async def handle_twiml_webhook( workflow_id: int, user_id: int, workflow_run_id: int, organization_id: int @@ -250,8 +517,14 @@ async def websocket_endpoint( # Extract provider type from workflow run context provider_type = None - if workflow_run.gathered_context: - provider_type = workflow_run.gathered_context.get("provider") + logger.info( + f"Workflow run {workflow_run_id} gathered_context: {workflow_run.gathered_context}" + ) + logger.info(f"Workflow run {workflow_run_id} mode: {workflow_run.mode}") + + if workflow_run.initial_context: + provider_type = workflow_run.initial_context.get("provider") + logger.info(f"Extracted provider_type: {provider_type}") if not provider_type: logger.error( @@ -546,21 +819,78 @@ async def handle_vobiz_xml_webhook( async def handle_vobiz_hangup_callback( workflow_run_id: int, request: Request, + x_vobiz_signature: Optional[str] = Header(None), + x_vobiz_timestamp: Optional[str] = Header(None), ): """Handle Vobiz hangup callback (sent when call ends). Vobiz sends callbacks to hangup_url when the call terminates. This includes call duration, status, and billing information. """ + # TODO: Remove this debug logging after Vobiz team clarifies webhook authentication + # Logging all headers and body to understand what Vobiz actually sends + all_headers = dict(request.headers) + logger.info( + f"[run {workflow_run_id}] Vobiz hangup callback - Headers: {json.dumps(all_headers)}" + ) + # Parse the callback data (Vobiz sends form data or JSON) form_data = await request.form() callback_data = dict(form_data) + + # TODO: Remove this debug logging after Vobiz team clarifies webhook authentication + logger.info( + f"[run {workflow_run_id}] Vobiz hangup callback - Body: {json.dumps(callback_data)}" + ) logger.info( f"[run {workflow_run_id}] Received Vobiz hangup callback {json.dumps(callback_data)}" ) - # Get workflow run for processing - workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id) + # Verify signature if provided + if x_vobiz_signature: + # We need the workflow run to get organization for provider credentials + workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id) + if not workflow_run: + logger.warning( + f"[run {workflow_run_id}] Workflow run not found for signature verification" + ) + return {"status": "error", "reason": "workflow_run_not_found"} + + workflow = await db_client.get_workflow_by_id(workflow_run.workflow_id) + if not workflow: + logger.warning( + f"[run {workflow_run_id}] Workflow not found for signature verification" + ) + return {"status": "error", "reason": "workflow_not_found"} + + provider = await get_telephony_provider(workflow.organization_id) + + # Get raw body for signature verification + raw_body = await request.body() + webhook_body = raw_body.decode("utf-8") + + # Verify signature + backend_endpoint = await TunnelURLProvider.get_tunnel_url() + webhook_url = f"https://{backend_endpoint}/api/v1/telephony/vobiz/hangup-callback/{workflow_run_id}" + + is_valid = await provider.verify_webhook_signature( + webhook_url, + callback_data, + x_vobiz_signature, + x_vobiz_timestamp, + webhook_body, + ) + + if not is_valid: + logger.warning( + f"[run {workflow_run_id}] Invalid Vobiz hangup callback signature" + ) + return {"status": "error", "reason": "invalid_signature"} + + logger.info(f"[run {workflow_run_id}] Vobiz hangup callback signature verified") + else: + # Get workflow run for processing (signature verification already got it if needed) + workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id) if not workflow_run: logger.warning( f"[run {workflow_run_id}] Workflow run not found for Vobiz hangup callback" @@ -609,21 +939,79 @@ async def handle_vobiz_hangup_callback( async def handle_vobiz_ring_callback( workflow_run_id: int, request: Request, + x_vobiz_signature: Optional[str] = Header(None), + x_vobiz_timestamp: Optional[str] = Header(None), ): """Handle Vobiz ring callback (sent when call starts ringing). Vobiz can send callbacks to ring_url when the call starts ringing. This is optional and used for tracking ringing status. """ + # TODO: Remove this debug logging after Vobiz team clarifies webhook authentication + # Logging all headers and body to understand what Vobiz actually sends + all_headers = dict(request.headers) + logger.info( + f"[run {workflow_run_id}] Vobiz ring callback - Headers: {json.dumps(all_headers)}" + ) + # Parse the callback data form_data = await request.form() callback_data = dict(form_data) + + # TODO: Remove this debug logging after Vobiz team clarifies webhook authentication + logger.info( + f"[run {workflow_run_id}] Vobiz ring callback - Body: {json.dumps(callback_data)}" + ) + logger.info( f"[run {workflow_run_id}] Received Vobiz ring callback {json.dumps(callback_data)}" ) - # Get workflow run for processing - workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id) + # Verify signature if provided + if x_vobiz_signature: + # We need the workflow run to get organization for provider credentials + workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id) + if not workflow_run: + logger.warning( + f"[run {workflow_run_id}] Workflow run not found for signature verification" + ) + return {"status": "error", "reason": "workflow_run_not_found"} + + workflow = await db_client.get_workflow_by_id(workflow_run.workflow_id) + if not workflow: + logger.warning( + f"[run {workflow_run_id}] Workflow not found for signature verification" + ) + return {"status": "error", "reason": "workflow_not_found"} + + provider = await get_telephony_provider(workflow.organization_id) + + # Get raw body for signature verification + raw_body = await request.body() + webhook_body = raw_body.decode("utf-8") + + # Verify signature + backend_endpoint = await TunnelURLProvider.get_tunnel_url() + webhook_url = f"https://{backend_endpoint}/api/v1/telephony/vobiz/ring-callback/{workflow_run_id}" + + is_valid = await provider.verify_webhook_signature( + webhook_url, + callback_data, + x_vobiz_signature, + x_vobiz_timestamp, + webhook_body, + ) + + if not is_valid: + logger.warning( + f"[run {workflow_run_id}] Invalid Vobiz ring callback signature" + ) + return {"status": "error", "reason": "invalid_signature"} + + logger.info(f"[run {workflow_run_id}] Vobiz ring callback signature verified") + else: + # Get workflow run for processing (signature verification already got it if needed) + workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id) if not workflow_run: logger.warning( f"[run {workflow_run_id}] Workflow run not found for Vobiz ring callback" @@ -707,3 +1095,272 @@ async def handle_cloudonix_status_callback( await _process_status_update(workflow_run_id, status_update, workflow_run) return {"status": "success"} + + +@router.post("/vobiz/hangup-callback/workflow/{workflow_id}") +async def handle_vobiz_hangup_callback_by_workflow( + workflow_id: int, + request: Request, + x_vobiz_signature: Optional[str] = Header(None), + x_vobiz_timestamp: Optional[str] = Header(None), +): + """Handle Vobiz hangup callback with workflow_id - finds workflow run by call_id.""" + + all_headers = dict(request.headers) + logger.info( + f"[workflow {workflow_id}] Vobiz hangup callback - Headers: {json.dumps(all_headers)}" + ) + + try: + callback_data, _ = await parse_webhook_request(request) + except ValueError: + callback_data = {} + + call_uuid = callback_data.get("CallUUID") or callback_data.get("call_uuid") + logger.info( + f"[workflow {workflow_id}] Received Vobiz hangup callback for call {call_uuid}: {json.dumps(callback_data)}" + ) + + if not call_uuid: + logger.warning( + f"[workflow {workflow_id}] No call_uuid found in Vobiz hangup callback" + ) + return {"status": "error", "message": "No call_uuid found"} + + workflow_client = WorkflowClient() + workflow = await workflow_client.get_workflow_by_id(workflow_id) + if not workflow: + logger.warning(f"[workflow {workflow_id}] Workflow not found") + return {"status": "error", "message": "workflow_not_found"} + + provider = await get_telephony_provider(workflow.organization_id) + + if x_vobiz_signature: + raw_body = await request.body() + webhook_body = raw_body.decode("utf-8") + backend_endpoint = await TunnelURLProvider.get_tunnel_url() + webhook_url = f"https://{backend_endpoint}/api/v1/telephony/vobiz/hangup-callback/workflow/{workflow_id}" + + is_valid = await provider.verify_webhook_signature( + webhook_url, + callback_data, + x_vobiz_signature, + x_vobiz_timestamp, + webhook_body, + ) + + if not is_valid: + logger.warning( + f"[workflow {workflow_id}] Invalid Vobiz hangup callback signature" + ) + return {"status": "error", "message": "invalid_signature"} + + logger.info( + f"[workflow {workflow_id}] Vobiz hangup callback signature verified" + ) + + try: + db_client = WorkflowRunClient() + async with db_client.async_session() as session: + # Fetch workflow run with matching call_id in initial_context + query = text(""" + SELECT id FROM workflow_runs + WHERE workflow_id = :workflow_id + AND CAST(initial_context AS jsonb) @> CAST(:call_id_json AS jsonb) + ORDER BY created_at DESC + LIMIT 1 + """) + + result = await session.execute( + query, + { + "workflow_id": workflow_id, + "call_id_json": json.dumps({"call_id": call_uuid}), + }, + ) + workflow_run_row = result.fetchone() + + if not workflow_run_row: + logger.warning( + f"[workflow {workflow_id}] No workflow run found for call {call_uuid}" + ) + return {"status": "ignored", "reason": "workflow_run_not_found"} + + workflow_run_id = workflow_run_row[0] + logger.info( + f"[workflow {workflow_id}] Found workflow run {workflow_run_id} for call {call_uuid}" + ) + + except Exception as e: + logger.error( + f"[workflow {workflow_id}] Error finding workflow run for call {call_uuid}: {e}" + ) + return {"status": "error", "message": str(e)} + + try: + workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id) + if not workflow_run: + logger.warning(f"[run {workflow_run_id}] Workflow run not found") + return {"status": "ignored", "reason": "workflow_run_not_found"} + + parsed_data = provider.parse_status_callback(callback_data) + + status = StatusCallbackRequest( + call_id=parsed_data["call_id"], + status=parsed_data["status"], + from_number=parsed_data.get("from_number"), + to_number=parsed_data.get("to_number"), + direction=parsed_data.get("direction"), + duration=parsed_data.get("duration"), + extra=parsed_data.get("extra", {}), + ) + + await _process_status_update(workflow_run_id, status, workflow_run) + + logger.info( + f"[run {workflow_run_id}] Vobiz hangup callback processed successfully" + ) + return {"status": "success"} + + except Exception as e: + logger.error( + f"[run {workflow_run_id}] Error processing Vobiz hangup callback: {e}" + ) + return {"status": "error", "message": str(e)} + + +@router.post("/inbound/{workflow_id}") +async def handle_inbound_telephony( + workflow_id: int, + request: Request, + x_twilio_signature: Optional[str] = Header(None), + x_vobiz_signature: Optional[str] = Header(None), + x_vobiz_timestamp: Optional[str] = Header(None), +): + """Handle inbound telephony calls from any supported provider with common processing""" + logger.info(f"Inbound call received for workflow_id: {workflow_id}") + + try: + webhook_data, data_source = await parse_webhook_request(request) + headers = dict(request.headers) + + # Detect provider and normalize data + provider_class = await _detect_provider(webhook_data, headers) + if not provider_class: + logger.error("Unable to detect provider for webhook") + return generic_hangup_response() + + normalized_data = normalize_webhook_data(provider_class, webhook_data) + + logger.info( + f"Inbound call - Provider: {normalized_data.provider}, Data source: {data_source}" + ) + logger.info(f"Normalized data: {normalized_data}") + + # Validate inbound direction + if normalized_data.direction != "inbound": + logger.warning(f"Non-inbound call received: {normalized_data.direction}") + return generic_hangup_response() + + logger.info(f"Inbound call headers: {dict(request.headers)}") + logger.info(f"Twilio signature header: {x_twilio_signature}") + logger.info(f"Vobiz signature header: {x_vobiz_signature}") + logger.info(f"Vobiz timestamp header: {x_vobiz_timestamp}") + + webhook_body = "" + if provider_class.PROVIDER_NAME == "vobiz": + webhook_body = data_source + logger.info(f"Vobiz inbound call - Body: {json.dumps(webhook_data)}") + + ( + is_valid, + error_type, + workflow_context, + provider_instance, + ) = await _validate_inbound_request( + workflow_id, + provider_class, + normalized_data, + webhook_data, + webhook_body, + x_twilio_signature, + x_vobiz_signature, + x_vobiz_timestamp, + ) + + if not is_valid: + logger.error(f"Request validation failed: {error_type}") + return provider_class.generate_validation_error_response(error_type) + + # Check quota before processing + user_id = workflow_context["user_id"] + quota_result = await check_dograh_quota_by_user_id(user_id) + if not quota_result.has_quota: + logger.warning( + f"User {user_id} has exceeded quota for inbound calls: {quota_result.error_message}" + ) + return provider_class.generate_validation_error_response( + TelephonyError.QUOTA_EXCEEDED + ) + + # Create workflow run + workflow_run_id = await _create_inbound_workflow_run( + workflow_id, + workflow_context["user_id"], + workflow_context["provider"], + normalized_data, + data_source, + ) + + # Generate response URLs + backend_endpoint = await TunnelURLProvider.get_tunnel_url() + websocket_url = f"wss://{backend_endpoint}/api/v1/telephony/ws/{workflow_id}/{workflow_context['user_id']}/{workflow_run_id}" + response = await provider_class.generate_inbound_response( + websocket_url, workflow_run_id + ) + + logger.info( + f"Generated {normalized_data.provider} response for call {normalized_data.call_id}" + ) + return response + + except ValueError as e: + logger.error(f"Request parsing error: {e}") + return generic_hangup_response() + except Exception as e: + logger.error(f"Error processing inbound call: {e}") + return generic_hangup_response() + + +@router.post("/inbound/fallback") +async def handle_inbound_fallback(request: Request): + """Fallback endpoint that returns audio message when calls cannot be processed.""" + + webhook_data, _ = await parse_webhook_request(request) + headers = dict(request.headers) + + # Detect provider + provider_class = await _detect_provider(webhook_data, headers) + + if provider_class: + # Use provider-specific error response + call_id = ( + webhook_data.get("CallSid") + or webhook_data.get("CallUUID") + or webhook_data.get("call_uuid") + ) + logger.info( + f"[fallback] Received {provider_class.PROVIDER_NAME} callback for call {call_id}: {json.dumps(webhook_data)}" + ) + + return provider_class.generate_error_response( + "SYSTEM_UNAVAILABLE", + "Our system is temporarily unavailable. Please try again later.", + ) + else: + # Unknown provider - return generic XML + logger.info( + f"[fallback] Received unknown provider callback: {json.dumps(webhook_data)} and request headers: {json.dumps(headers)}" + ) + + return generic_hangup_response() diff --git a/api/routes/workflow.py b/api/routes/workflow.py index e6ebde9..6ce98a1 100644 --- a/api/routes/workflow.py +++ b/api/routes/workflow.py @@ -12,6 +12,7 @@ from api.constants import DEPLOYMENT_MODE from api.db import db_client from api.db.models import UserModel from api.db.workflow_template_client import WorkflowTemplateClient +from api.enums import CallType from api.schemas.workflow import WorkflowRunResponseSchema from api.services.auth.depends import get_user from api.services.mps_service_key_client import mps_service_key_client @@ -141,7 +142,7 @@ class CreateWorkflowRunResponse(BaseModel): class CreateWorkflowTemplateRequest(BaseModel): - call_type: Literal["INBOUND", "OUTBOUND"] + call_type: Literal[CallType.INBOUND.value, CallType.OUTBOUND.value] use_case: str activity_description: str @@ -289,7 +290,7 @@ async def create_workflow_from_template( # Call MPS API to generate workflow using the client if DEPLOYMENT_MODE == "oss": workflow_data = await mps_service_key_client.call_workflow_api( - call_type=request.call_type, + call_type=request.call_type.upper(), use_case=request.use_case, activity_description=request.activity_description, created_by=str(user.provider_id), @@ -299,7 +300,7 @@ async def create_workflow_from_template( raise HTTPException(status_code=400, detail="No organization selected") workflow_data = await mps_service_key_client.call_workflow_api( - call_type=request.call_type, + call_type=request.call_type.upper(), use_case=request.use_case, activity_description=request.activity_description, organization_id=user.selected_organization_id, @@ -609,6 +610,7 @@ async def get_workflow_run( "definition_id": run.definition_id, "initial_context": run.initial_context, "gathered_context": run.gathered_context, + "call_type": run.call_type, } diff --git a/api/schemas/workflow.py b/api/schemas/workflow.py index 5ea1cec..5696e8b 100644 --- a/api/schemas/workflow.py +++ b/api/schemas/workflow.py @@ -3,6 +3,8 @@ from typing import Any, Dict from pydantic import BaseModel +from api.enums import CallType + class WorkflowRunResponseSchema(BaseModel): id: int @@ -17,3 +19,4 @@ class WorkflowRunResponseSchema(BaseModel): definition_id: int | None # This is for backward compatibility initial_context: dict | None = None gathered_context: dict | None = None + call_type: CallType diff --git a/api/services/telephony/base.py b/api/services/telephony/base.py index 8668872..8e9c32c 100644 --- a/api/services/telephony/base.py +++ b/api/services/telephony/base.py @@ -26,6 +26,22 @@ class CallInitiationResult: ) # Full provider response for debugging +@dataclass +class NormalizedInboundData: + """Standardized inbound call data across all providers.""" + + provider: str # Provider name (twilio, vobiz, etc.) + call_id: str # Provider's call identifier + from_number: str # Caller phone number (E.164 format) + to_number: str # Called phone number (E.164 format) + direction: str # Call direction (should be "inbound") + call_status: str # Call status (ringing, answered, etc.) + account_id: Optional[str] = None # Provider account ID + from_country: Optional[str] = None # Country code of caller + to_country: Optional[str] = None # Country code of called number + raw_data: Dict[str, Any] = field(default_factory=dict) # Original webhook data + + class TelephonyProvider(ABC): """ Abstract base class for telephony providers. @@ -181,3 +197,109 @@ class TelephonyProvider(ABC): workflow_run_id: The workflow run ID """ pass + + # ======== INBOUND CALL METHODS ======== + + @classmethod + @abstractmethod + def can_handle_webhook( + cls, webhook_data: Dict[str, Any], headers: Dict[str, str] + ) -> bool: + """ + Determine if this provider can handle the incoming webhook. + + Args: + webhook_data: The parsed webhook payload + headers: HTTP headers from the webhook request + + Returns: + True if this provider should handle this webhook, False otherwise + """ + pass + + @staticmethod + @abstractmethod + def parse_inbound_webhook(webhook_data: Dict[str, Any]) -> NormalizedInboundData: + """ + Parse provider-specific inbound webhook data into normalized format. + + Args: + webhook_data: Raw webhook data from the provider + + Returns: + NormalizedInboundData with standardized fields + """ + pass + + @staticmethod + @abstractmethod + def validate_account_id(config_data: dict, webhook_account_id: str) -> bool: + """ + Validate that the account_id from webhook matches the provider configuration. + + Args: + config_data: Provider configuration data from organization + webhook_account_id: Account ID from the webhook + + Returns: + True if account_id matches, False otherwise + """ + pass + + @abstractmethod + def normalize_phone_number(self, phone_number: str) -> str: + """ + Normalize a phone number to E.164 format for this provider. + + Args: + phone_number: Raw phone number from webhook + + Returns: + Phone number in E.164 format (+country_code_number) + """ + pass + + @abstractmethod + async def verify_inbound_signature( + self, url: str, webhook_data: Dict[str, Any], signature: str + ) -> bool: + """ + Verify the signature of an inbound webhook for security. + + Args: + url: The full webhook URL + webhook_data: The webhook payload + signature: The signature header from the provider + + Returns: + True if signature is valid, False otherwise + """ + pass + + @abstractmethod + def generate_inbound_response(self, websocket_url: str) -> tuple: + """ + Generate the appropriate response for an inbound webhook. + + Args: + websocket_url: WebSocket URL for audio streaming + + Returns: + Tuple of (Response, media_type) - Response object and content type + """ + pass + + @staticmethod + @abstractmethod + def generate_error_response(error_type: str, message: str) -> tuple: + """ + Generate a provider-specific error response. + + Args: + error_type: Type of error (auth_failed, not_configured, etc.) + message: Error message + + Returns: + Tuple of (Response, media_type) - Response object and content type + """ + pass diff --git a/api/services/telephony/factory.py b/api/services/telephony/factory.py index eb56316..ce3abaf 100644 --- a/api/services/telephony/factory.py +++ b/api/services/telephony/factory.py @@ -4,7 +4,7 @@ Handles configuration loading from environment (OSS) or database (SaaS). The providers themselves don't know or care where config comes from. """ -from typing import Any, Dict +from typing import Any, Dict, List, Type from loguru import logger @@ -116,3 +116,13 @@ async def get_telephony_provider(organization_id: int) -> TelephonyProvider: else: raise ValueError(f"Unknown telephony provider: {provider_type}") + + +async def get_all_telephony_providers() -> List[Type[TelephonyProvider]]: + """ + Get all available telephony provider classes for webhook detection. + + Returns: + List of provider classes that can be used for webhook detection + """ + return [TwilioProvider, VobizProvider, VonageProvider] diff --git a/api/services/telephony/providers/cloudonix_provider.py b/api/services/telephony/providers/cloudonix_provider.py index 42645d4..bbcc7b6 100644 --- a/api/services/telephony/providers/cloudonix_provider.py +++ b/api/services/telephony/providers/cloudonix_provider.py @@ -10,7 +10,11 @@ import aiohttp from loguru import logger from api.enums import WorkflowRunMode -from api.services.telephony.base import CallInitiationResult, TelephonyProvider +from api.services.telephony.base import ( + CallInitiationResult, + NormalizedInboundData, + TelephonyProvider, +) from api.utils.tunnel import TunnelURLProvider if TYPE_CHECKING: @@ -416,3 +420,170 @@ class CloudonixProvider(TelephonyProvider): except Exception as e: logger.error(f"Error in Cloudonix WebSocket handler: {e}") raise + + # ======== INBOUND CALL METHODS ======== + + @classmethod + def can_handle_webhook( + cls, webhook_data: Dict[str, Any], headers: Dict[str, str] + ) -> bool: + """ + Determine if this provider can handle the incoming webhook. + + Cloudonix uses TwiML-compatible format, so look for Twilio-like identifiers + but also check for Cloudonix-specific headers or fields if they exist. + """ + # Check for Cloudonix-specific headers + if headers.get("User-Agent", "").lower().startswith("cloudonix"): + return True + + # Check for session token (Cloudonix equivalent of CallSid) + if "token" in webhook_data or "session_token" in webhook_data: + return True + + # If it looks like TwiML format but no other providers claimed it, + # it could be Cloudonix (TwiML-compatible) + if "CallSid" in webhook_data and "AccountSid" in webhook_data: + # Let Twilio provider handle this first, only handle if unclaimed + return False + + return False + + @staticmethod + def parse_inbound_webhook(webhook_data: Dict[str, Any]) -> NormalizedInboundData: + """ + Parse Cloudonix-specific inbound webhook data into normalized format. + + Cloudonix is TwiML-compatible so the webhook format should be similar to Twilio. + """ + return NormalizedInboundData( + provider=CloudonixProvider.PROVIDER_NAME, + call_id=webhook_data.get("token") or webhook_data.get("CallSid", ""), + from_number=webhook_data.get("From", ""), + to_number=webhook_data.get("To", ""), + direction="inbound", # This is an inbound webhook + call_status=webhook_data.get("CallStatus", "ringing"), + account_id=webhook_data.get("AccountSid") or webhook_data.get("domain_id"), + from_country=webhook_data.get("FromCountry"), + to_country=webhook_data.get("ToCountry"), + raw_data=webhook_data, + ) + + @staticmethod + def validate_account_id(config_data: dict, webhook_account_id: str) -> bool: + """ + Validate that the account_id from webhook matches the Cloudonix configuration. + """ + if not webhook_account_id: + return False + + # Cloudonix uses domain_id as the account identifier + stored_domain_id = config_data.get("domain_id") + if not stored_domain_id: + return False + + return webhook_account_id == stored_domain_id + + def normalize_phone_number(self, phone_number: str) -> str: + """ + Normalize a phone number to E.164 format for Cloudonix. + + Cloudonix typically provides numbers in E.164 format already, + but we'll ensure proper formatting. + """ + if not phone_number: + return "" + + # Remove any spaces or formatting + clean_number = ( + phone_number.replace(" ", "") + .replace("-", "") + .replace("(", "") + .replace(")", "") + ) + + # If already in E.164 format (+...), return as-is + if clean_number.startswith("+"): + return clean_number + + # If starts with country code but no +, add it + if len(clean_number) >= 10: + return f"+{clean_number}" + + return clean_number + + async def verify_inbound_signature( + self, url: str, webhook_data: Dict[str, Any], signature: str + ) -> bool: + """ + Verify the signature of an inbound Cloudonix webhook for security. + + Note: Cloudonix signature verification details need to be implemented + based on their specific authentication method. For now, we'll log + and return True (similar to current webhook verification). + """ + logger.info( + f"Cloudonix inbound signature verification not fully implemented. " + f"Webhook URL: {url}, Signature present: {bool(signature)}" + ) + + # TODO: Implement actual Cloudonix signature verification + # This would depend on Cloudonix's specific signing method + return True + + def generate_inbound_response(self, websocket_url: str) -> tuple: + """ + Generate the appropriate TwiML response for an inbound Cloudonix webhook. + + Since Cloudonix is TwiML-compatible, we generate TwiML response. + """ + from fastapi import Response + + # Generate TwiML response to connect to WebSocket + twiml = f""" + + + + + +""" + + return Response(content=twiml, media_type="application/xml"), "application/xml" + + @staticmethod + def generate_error_response(error_type: str, message: str) -> tuple: + """ + Generate a Cloudonix-specific error response. + + Since Cloudonix is TwiML-compatible, we use TwiML format. + """ + from fastapi import Response + + # Map error types to appropriate TwiML responses + if error_type == "auth_failed": + twiml = """ + + Authentication failed. This call cannot be processed. + +""" + elif error_type == "not_configured": + twiml = """ + + Service not configured. Please contact support. + +""" + elif error_type == "invalid_number": + twiml = """ + + Invalid phone number. This call cannot be processed. + +""" + else: + # Generic error + twiml = f""" + + An error occurred: {message} + +""" + + return Response(content=twiml, media_type="application/xml"), "application/xml" diff --git a/api/services/telephony/providers/twilio_provider.py b/api/services/telephony/providers/twilio_provider.py index 5e42ecf..4b510a3 100644 --- a/api/services/telephony/providers/twilio_provider.py +++ b/api/services/telephony/providers/twilio_provider.py @@ -11,7 +11,11 @@ from loguru import logger from twilio.request_validator import RequestValidator from api.enums import WorkflowRunMode -from api.services.telephony.base import CallInitiationResult, TelephonyProvider +from api.services.telephony.base import ( + CallInitiationResult, + NormalizedInboundData, + TelephonyProvider, +) from api.utils.tunnel import TunnelURLProvider if TYPE_CHECKING: @@ -284,3 +288,139 @@ class TwilioProvider(TelephonyProvider): except Exception as e: logger.error(f"Error in Twilio WebSocket handler: {e}") raise + + # ======== INBOUND CALL METHODS ======== + + @classmethod + def can_handle_webhook( + cls, webhook_data: Dict[str, Any], headers: Dict[str, str] + ) -> bool: + """ + Determine if this provider can handle the incoming webhook. + Twilio webhooks contain CallSid field. + """ + return "CallSid" in webhook_data + + @staticmethod + def parse_inbound_webhook(webhook_data: Dict[str, Any]) -> NormalizedInboundData: + """ + Parse Twilio-specific inbound webhook data into normalized format. + """ + return NormalizedInboundData( + provider=TwilioProvider.PROVIDER_NAME, + call_id=webhook_data.get("CallSid", ""), + from_number=TwilioProvider.normalize_phone_number( + webhook_data.get("From", "") + ), + to_number=TwilioProvider.normalize_phone_number(webhook_data.get("To", "")), + direction=webhook_data.get("Direction", ""), + call_status=webhook_data.get("CallStatus", ""), + account_id=webhook_data.get("AccountSid"), + from_country=webhook_data.get("FromCountry") + or webhook_data.get("CallerCountry"), + to_country=webhook_data.get("ToCountry") + or webhook_data.get("CalledCountry"), + raw_data=webhook_data, + ) + + @staticmethod + def normalize_phone_number(phone_number: str) -> str: + """ + Normalize a phone number to E.164 format for Twilio. + Twilio already provides numbers in E.164 format. + """ + if not phone_number: + return "" + + # Twilio numbers are already in E.164 format (+1234567890) + if phone_number.startswith("+"): + return phone_number + + # If for some reason it doesn't have +, assume US and add +1 + if phone_number.startswith("1") and len(phone_number) == 11: + return f"+{phone_number}" + elif len(phone_number) == 10: + return f"+1{phone_number}" + + return phone_number + + @staticmethod + def validate_account_id(config_data: dict, webhook_account_id: str) -> bool: + """Validate Twilio account_sid from webhook matches configuration""" + if not webhook_account_id: + return False + + stored_account_sid = config_data.get("account_sid") + return stored_account_sid == webhook_account_id + + async def verify_inbound_signature( + self, url: str, webhook_data: Dict[str, Any], signature: str + ) -> bool: + """ + Verify the signature of an inbound Twilio webhook for security. + """ + return await self.verify_webhook_signature(url, webhook_data, signature) + + @staticmethod + async def generate_inbound_response( + websocket_url: str, workflow_run_id: int = None + ) -> tuple: + """ + Generate TwiML response for an inbound Twilio webhook. + + Uses the same StatusCallback URL pattern as outbound calls for consistency. + """ + from fastapi import Response + + # Generate StatusCallback URL using same pattern as outbound calls + status_callback_attr = "" + if workflow_run_id: + backend_endpoint = await TunnelURLProvider.get_tunnel_url() + status_callback_url = f"https://{backend_endpoint}/api/v1/telephony/twilio/status-callback/{workflow_run_id}" + status_callback_attr = f' statusCallback="{status_callback_url}"' + + twiml_content = f""" + + + + + +""" + + return Response(content=twiml_content, media_type="application/xml") + + @staticmethod + def generate_error_response(error_type: str, message: str) -> tuple: + """ + Generate a Twilio-specific error response. + """ + from fastapi import Response + + twiml_content = f""" + + Sorry, there was an error processing your call. {message} + +""" + + return Response(content=twiml_content, media_type="application/xml") + + @staticmethod + def generate_validation_error_response(error_type) -> tuple: + """ + Generate Twilio-specific error response for validation failures with organizational debugging info. + """ + from fastapi import Response + + from api.errors.telephony_errors import TELEPHONY_ERROR_MESSAGES, TelephonyError + + message = TELEPHONY_ERROR_MESSAGES.get( + error_type, TELEPHONY_ERROR_MESSAGES[TelephonyError.GENERAL_AUTH_FAILED] + ) + + twiml_content = f""" + + {message} + +""" + + return Response(content=twiml_content, media_type="application/xml") diff --git a/api/services/telephony/providers/vobiz_provider.py b/api/services/telephony/providers/vobiz_provider.py index 894389e..596c02f 100644 --- a/api/services/telephony/providers/vobiz_provider.py +++ b/api/services/telephony/providers/vobiz_provider.py @@ -10,7 +10,11 @@ import aiohttp from loguru import logger from api.enums import WorkflowRunMode -from api.services.telephony.base import CallInitiationResult, TelephonyProvider +from api.services.telephony.base import ( + CallInitiationResult, + NormalizedInboundData, + TelephonyProvider, +) from api.utils.tunnel import TunnelURLProvider if TYPE_CHECKING: @@ -179,20 +183,65 @@ class VobizProvider(TelephonyProvider): return bool(self.auth_id and self.auth_token and self.from_numbers) async def verify_webhook_signature( - self, url: str, params: Dict[str, Any], signature: str + self, + url: str, + params: Dict[str, Any], + signature: str, + timestamp: str = None, + body: str = "", ) -> bool: """ Verify Vobiz webhook signature for security. - Vobiz uses Plivo-compatible signature verification (HMAC-SHA256). - For now, returning True to allow testing. - TODO: Implement proper signature verification based on Vobiz docs. + Vobiz uses HMAC-SHA256 signature verification with timestamp validation: + - Header: x-vobiz-signature (HMAC-SHA256 hash) + - Header: x-vobiz-timestamp (timestamp for replay protection) + - Signature = HMAC-SHA256(auth_token, timestamp + '.' + body) """ - # Plivo/Vobiz signature verification would go here - # For development, we can skip signature verification - # In production, implement HMAC-SHA256 verification - logger.warning("Vobiz webhook signature verification not yet implemented") - return True + import hashlib + import hmac + from datetime import datetime, timezone + + if not signature or not timestamp: + logger.warning("Missing signature or timestamp headers for Vobiz webhook") + return False + + if not self.auth_token: + logger.error( + "No auth_token available for Vobiz webhook signature verification" + ) + return False + + try: + # 1. Timestamp validation (within 5 minutes) + webhook_timestamp = int(timestamp) + current_timestamp = int(datetime.now(timezone.utc).timestamp()) + time_diff = abs(current_timestamp - webhook_timestamp) + + if time_diff > 300: # 5 minutes = 300 seconds + logger.warning(f"Vobiz webhook timestamp too old: {time_diff}s > 300s") + return False + + # 2. Signature verification + # Create expected signature: HMAC-SHA256(auth_token, timestamp + '.' + body) + payload = f"{timestamp}.{body}" + expected_signature = hmac.new( + self.auth_token.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256 + ).hexdigest() + + # 3. Compare signatures (timing-safe comparison) + is_valid = hmac.compare_digest(expected_signature, signature) + + if not is_valid: + logger.warning( + f"Vobiz webhook signature mismatch. Expected: {expected_signature[:8]}..., Got: {signature[:8]}..." + ) + + return is_valid + + except Exception as e: + logger.error(f"Error verifying Vobiz webhook signature: {e}") + return False async def get_webhook_response( self, workflow_id: int, user_id: int, workflow_run_id: int @@ -339,3 +388,140 @@ class VobizProvider(TelephonyProvider): f"[run {workflow_run_id}] Error in Vobiz WebSocket handler: {e}" ) raise + + # ======== INBOUND CALL METHODS ======== + + @classmethod + def can_handle_webhook( + cls, webhook_data: Dict[str, Any], headers: Dict[str, str] + ) -> bool: + """ + Determine if this provider can handle the incoming webhook. + Vobiz webhooks contain CallUUID field. + """ + return "CallUUID" in webhook_data + + @staticmethod + def parse_inbound_webhook(webhook_data: Dict[str, Any]) -> NormalizedInboundData: + """ + Parse Vobiz-specific inbound webhook data into normalized format. + """ + return NormalizedInboundData( + provider=VobizProvider.PROVIDER_NAME, + call_id=webhook_data.get("CallUUID", ""), + from_number=VobizProvider.normalize_phone_number( + webhook_data.get("From", "") + ), + to_number=VobizProvider.normalize_phone_number(webhook_data.get("To", "")), + direction=webhook_data.get("Direction", ""), + call_status=webhook_data.get("CallStatus", ""), + account_id=webhook_data.get("ParentAuthID"), + from_country=None, # Vobiz doesn't provide country information + to_country=None, # Vobiz doesn't provide country information + raw_data=webhook_data, + ) + + @staticmethod + def normalize_phone_number(phone_number: str) -> str: + """ + Normalize a phone number to E.164 format for Vobiz. + Vobiz sends numbers in various formats - normalize to E.164 with +. + """ + if not phone_number: + return "" + + # Remove any existing + prefix + clean_number = phone_number.lstrip("+") + + # If it starts with 1 and has 11 digits, it's a US number + if clean_number.startswith("1") and len(clean_number) == 11: + return f"+{clean_number}" + elif len(clean_number) == 10: + # Assume US number if 10 digits + return f"+1{clean_number}" + elif len(clean_number) > 10: + # International number without country code detection + return f"+{clean_number}" + + return phone_number + + @staticmethod + def validate_account_id(config_data: dict, webhook_account_id: str) -> bool: + """Validate Vobiz auth_id from webhook matches configuration""" + if not webhook_account_id: + return False + + stored_auth_id = config_data.get("auth_id") + return stored_auth_id == webhook_account_id + + async def verify_inbound_signature( + self, + url: str, + webhook_data: Dict[str, Any], + signature: str, + timestamp: str = None, + body: str = "", + ) -> bool: + """ + Verify the signature of an inbound Vobiz webhook for security. + Uses the same HMAC-SHA256 verification as other Vobiz webhooks. + """ + return await self.verify_webhook_signature( + url, webhook_data, signature, timestamp, body + ) + + @staticmethod + async def generate_inbound_response( + websocket_url: str, workflow_run_id: int = None + ) -> tuple: + """ + Generate Vobiz XML response for an inbound webhook. + + Note: For hangup callbacks, configure the hangup_url manually in Vobiz dashboard + to point to: /api/v1/telephony/vobiz/hangup-callback/workflow/{workflow_id} + """ + from fastapi import Response + + vobiz_xml = f""" + + {websocket_url} +""" + + return Response(content=vobiz_xml, media_type="application/xml") + + @staticmethod + def generate_error_response(error_type: str, message: str) -> tuple: + """ + Generate a Vobiz-specific error response. + """ + from fastapi import Response + + # Vobiz error responses should be valid XML like Plivo + vobiz_xml = f""" + + Sorry, there was an error processing your call. {message} + +""" + + return Response(content=vobiz_xml, media_type="application/xml") + + @staticmethod + def generate_validation_error_response(error_type) -> tuple: + """ + Generate Vobiz-specific error response for validation failures with organizational debugging info. + """ + from fastapi import Response + + from api.errors.telephony_errors import TELEPHONY_ERROR_MESSAGES, TelephonyError + + message = TELEPHONY_ERROR_MESSAGES.get( + error_type, TELEPHONY_ERROR_MESSAGES[TelephonyError.GENERAL_AUTH_FAILED] + ) + + vobiz_xml_content = f""" + + {message} + +""" + + return Response(content=vobiz_xml_content, media_type="application/xml") diff --git a/api/services/telephony/providers/vonage_provider.py b/api/services/telephony/providers/vonage_provider.py index a69e0a0..e3b40fa 100644 --- a/api/services/telephony/providers/vonage_provider.py +++ b/api/services/telephony/providers/vonage_provider.py @@ -9,10 +9,15 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional import aiohttp import jwt +from fastapi import Response from loguru import logger from api.enums import WorkflowRunMode -from api.services.telephony.base import CallInitiationResult, TelephonyProvider +from api.services.telephony.base import ( + CallInitiationResult, + NormalizedInboundData, + TelephonyProvider, +) from api.utils.tunnel import TunnelURLProvider if TYPE_CHECKING: @@ -378,3 +383,99 @@ class VonageProvider(TelephonyProvider): except Exception as e: logger.error(f"Error in Vonage WebSocket handler: {e}") raise + + # ======== INBOUND CALL METHODS ======== + + @classmethod + def can_handle_webhook( + cls, webhook_data: Dict[str, Any], headers: Dict[str, str] + ) -> bool: + """ + Determine if this provider can handle the incoming webhook. + """ + return False + + @staticmethod + def parse_inbound_webhook(webhook_data: Dict[str, Any]) -> NormalizedInboundData: + """ + Parse Vonage-specific inbound webhook data into normalized format. + """ + return NormalizedInboundData( + provider=VonageProvider.PROVIDER_NAME, + call_id=webhook_data.get("uuid", ""), + from_number=webhook_data.get("from", ""), + to_number=webhook_data.get("to", ""), + direction=webhook_data.get("direction", ""), + call_status=webhook_data.get("status", ""), + account_id=webhook_data.get("account_id"), + from_country=None, + to_country=None, + raw_data=webhook_data, + ) + + @staticmethod + def normalize_phone_number(phone_number: str) -> str: + """ + Normalize a phone number to E.164 format for Vonage. + """ + if not phone_number: + return "" + + if phone_number.startswith("+"): + return phone_number + + return f"+{phone_number}" + + @staticmethod + def validate_account_id(config_data: dict, webhook_account_id: str) -> bool: + """Validate Vonage account_id from webhook matches configuration""" + if not webhook_account_id: + return False + + stored_api_key = config_data.get("api_key") + return stored_api_key == webhook_account_id + + async def verify_inbound_signature( + self, url: str, webhook_data: Dict[str, Any], signature: str + ) -> bool: + """ + Vonage inbound signature verification - minimalist implementation. + """ + return True + + @staticmethod + async def generate_inbound_response( + websocket_url: str, workflow_run_id: int = None + ) -> tuple: + """ + Generate NCCO response for inbound Vonage webhook. + """ + # Minimalist NCCO response for interface compliance + ncco_response = [ + { + "action": "talk", + "text": "Vonage inbound calls are not currently supported.", + }, + {"action": "hangup"}, + ] + + return Response( + content=json.dumps(ncco_response), media_type="application/json" + ) + + @staticmethod + def generate_error_response(error_type: str, message: str) -> tuple: + """ + Generate a Vonage-specific error response. + """ + from fastapi import Response + + error_ncco = [ + { + "action": "talk", + "text": f"Sorry, there was an error processing your call. {message}", + }, + {"action": "hangup"}, + ] + + return Response(content=json.dumps(error_ncco), media_type="application/json") diff --git a/api/utils/telephony_helper.py b/api/utils/telephony_helper.py new file mode 100644 index 0000000..6ad917e --- /dev/null +++ b/api/utils/telephony_helper.py @@ -0,0 +1,232 @@ +""" +Telephony helper utilities. +Common functions used across telephony operations. +""" + +from dograh.api.constants import COUNTRY_CODES +from fastapi import Request +from loguru import logger +from starlette.responses import HTMLResponse + + +def numbers_match( + incoming_number: str, + configured_number: str, + to_country: str = None, + from_country: str = None, +) -> bool: + """ + Check if two phone numbers match, handling different formats with country context. + + Args: + incoming_number: Phone number from webhook + configured_number: Phone number from organization config + to_country: ISO country code for the called number (e.g., "US", "IN") + from_country: ISO country code for the caller (e.g., "IN", "GB") + + Examples: + - incoming: "+08043071383", configured: "918043071383", to_country="IN" -> True + - incoming: "+918043071383", configured: "918043071383" -> True + - incoming: "+19781899185", configured: "+19781899185" -> True + """ + if not incoming_number or not configured_number: + return False + + # Remove spaces and normalize + incoming_clean = incoming_number.replace(" ", "").replace("-", "") + configured_clean = configured_number.replace(" ", "").replace("-", "") + + # Direct match + if incoming_clean == configured_clean: + return True + + # Remove + from both and compare + incoming_no_plus = incoming_clean.lstrip("+") + configured_no_plus = configured_clean.lstrip("+") + + if incoming_no_plus == configured_no_plus: + return True + + if to_country: + country_code = get_country_code(to_country) + if country_code: + if _test_number_formats_with_country_code( + incoming_no_plus, configured_no_plus, country_code + ): + return True + + # Fallback to caller country if available + if from_country and from_country != to_country: + country_code = get_country_code(from_country) + if country_code: + if _test_number_formats_with_country_code( + incoming_no_plus, configured_no_plus, country_code + ): + return True + + # Legacy fallback for common country codes (when no country info available) + if not to_country and not from_country: + common_codes = ["91", "1", "44"] # India, US/Canada, UK + for code in common_codes: + if _test_number_formats_with_country_code( + incoming_no_plus, configured_no_plus, code + ): + return True + + return False + + +def _test_number_formats_with_country_code( + incoming_no_plus: str, configured_no_plus: str, country_code: str +) -> bool: + """ + Test different phone number format variations with the given country code to find matches. + + This function handles various international phone number formatting scenarios: + - Numbers with/without country codes + - Numbers with leading zeros vs country codes + - Different representations of the same number across formats + + Args: + incoming_no_plus: Incoming number without + prefix + configured_no_plus: Configured number without + prefix + country_code: International dialing code (e.g., "91", "1") + + Returns: + True if any format variation produces a match + """ + # Case 1: Incoming has no country code, configured has it + if f"{country_code}{incoming_no_plus}" == configured_no_plus: + return True + + # Case 2: Incoming has leading 0, need to replace with country code + if incoming_no_plus.startswith("0"): + local_part = incoming_no_plus[1:] # Remove leading 0 + if f"{country_code}{local_part}" == configured_no_plus: + return True + + # Case 3: Configured has no country code, incoming has it + if f"{country_code}{configured_no_plus}" == incoming_no_plus: + return True + + # Case 4: Configured has leading 0, need to replace with country code + if configured_no_plus.startswith("0"): + local_part = configured_no_plus[1:] # Remove leading 0 + if f"{country_code}{local_part}" == incoming_no_plus: + return True + + return False + + +def normalize_phone_number(phone_number: str, country_code: str = None) -> str: + """ + Normalize a phone number to E.164 format using country context. + + Args: + phone_number: Phone number to normalize + country_code: ISO country code (e.g., "US", "IN") for context + + Returns: + Phone number in E.164 format (e.g., "+14155552671", "+919876543210") + """ + if not phone_number: + return "" + + # Remove spaces, hyphens, and other formatting + clean_number = ( + phone_number.replace(" ", "").replace("-", "").replace("(", "").replace(")", "") + ) + + # Already in E.164 format + if clean_number.startswith("+"): + return clean_number + + # Get dialing code for the country + if country_code: + dialing_code = get_country_code(country_code) + if dialing_code: + # Remove leading 0 if present (common in many countries) + if clean_number.startswith("0"): + clean_number = clean_number[1:] + + # Add country code if not already present + if not clean_number.startswith(dialing_code): + return f"+{dialing_code}{clean_number}" + else: + return f"+{clean_number}" + + # Fallback: try to guess common formats + if clean_number.startswith("0") and len(clean_number) == 11: + # Without country context, prefer India for now + return f"+91{clean_number[1:]}" + elif len(clean_number) == 10: + # Without context, this is ambiguous - return as-is with + prefix + return f"+{clean_number}" + elif not clean_number.startswith("+"): + # Add + prefix if missing + return f"+{clean_number}" + + return clean_number + + +def normalize_webhook_data(provider_class, webhook_data): + """Normalize webhook data using the provider's parse method""" + return provider_class.parse_inbound_webhook(webhook_data) + + +def generic_hangup_response(): + """Return a generic hangup response for unknown/error cases""" + return HTMLResponse( + content="", media_type="application/xml" + ) + + +async def parse_webhook_request(request: Request) -> tuple[dict, str]: + """Parse webhook request data from either JSON or form""" + try: + # Try JSON first + webhook_data = await request.json() + data_source = "JSON" + except Exception: + try: + # Fallback to form data + form_data = await request.form() + webhook_data = dict(form_data) + data_source = "FORM" + except Exception as e: + logger.error(f"Failed to parse webhook data: {e}") + raise ValueError("Unable to parse webhook data") + + return webhook_data, data_source + + +def get_country_code(country_iso: str) -> str: + """ + Get the international dialing code for a country. + + Args: + country_iso: ISO 3166-1 alpha-2 country code (e.g., "US", "IN", "GB") + + Returns: + International dialing code (e.g., "1", "91", "44") or empty string if not found + """ + if not country_iso: + return "" + + return COUNTRY_CODES.get(country_iso.upper(), "") + + +def get_countries_for_code(dialing_code: str) -> list[str]: + """ + Get all countries that use a specific dialing code. + + Args: + dialing_code: International dialing code (e.g., "1", "91") + + Returns: + List of ISO country codes that use this dialing code + """ + if not dialing_code: + return [] + + return [country for country, code in COUNTRY_CODES.items() if code == dialing_code] diff --git a/ui/package.json b/ui/package.json index 767dbe7..6ea06f6 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,7 +7,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - "fix-lint": "npx eslint --fix . --ignore-pattern '.next/*' --ignore-pattern 'node_modules/*'", + "fix-lint": "npx eslint --fix . --ignore-pattern '.next/*' --ignore-pattern 'node_modules/*' --ignore-pattern 'next-env.d.ts'", "generate-client": "openapi-ts" }, "dependencies": { diff --git a/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx b/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx index 09f3c1c..20ba69a 100644 --- a/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx +++ b/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx @@ -216,6 +216,7 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti ID Status Created At + Call Type Duration Disposition Dograh Token @@ -236,6 +237,11 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti {formatDate(run.created_at)} + + + {run.call_type === 'inbound' ? 'Inbound' : 'Outbound'} + + {typeof run.cost_info?.call_duration_seconds === 'number' ? `${run.cost_info.call_duration_seconds.toFixed(1)}s` diff --git a/ui/src/app/workflow/create/page.tsx b/ui/src/app/workflow/create/page.tsx index 287fbbb..5f194e9 100644 --- a/ui/src/app/workflow/create/page.tsx +++ b/ui/src/app/workflow/create/page.tsx @@ -31,7 +31,7 @@ export default function CreateWorkflowPage() { const [showSuccessModal, setShowSuccessModal] = useState(false); const [workflowId, setWorkflowId] = useState(null); - const [callType, setCallType] = useState<'INBOUND' | 'OUTBOUND'>('INBOUND'); + const [callType, setCallType] = useState<'inbound' | 'outbound'>('inbound'); const [useCase, setUseCase] = useState(''); const [activityDescription, setActivityDescription] = useState(''); @@ -128,15 +128,15 @@ export default function CreateWorkflowPage() { Call Type - setCallType(value as 'INBOUND' | 'OUTBOUND')}> + setCallType(value as 'inbound' | 'outbound')}> - + Inbound (Users call AI) - + Outbound (AI calls users) diff --git a/ui/src/client/sdk.gen.ts b/ui/src/client/sdk.gen.ts index c3fcda6..5a8c6c6 100644 --- a/ui/src/client/sdk.gen.ts +++ b/ui/src/client/sdk.gen.ts @@ -3,7 +3,7 @@ import type { Client,Options as ClientOptions, TDataShape } from '@hey-api/client-fetch'; import { client as _heyApiClient } from './client.gen'; -import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, OfferApiV1PipecatRtcOfferPostData, OfferApiV1PipecatRtcOfferPostError, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen'; +import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, OfferApiV1PipecatRtcOfferPostData, OfferApiV1PipecatRtcOfferPostError, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen'; export type Options = ClientOptions & { /** @@ -100,6 +100,39 @@ export const handleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackW }); }; +/** + * Handle Vobiz Hangup Callback By Workflow + * Handle Vobiz hangup callback with workflow_id - finds workflow run by call_id. + */ +export const handleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPost = (options: Options) => { + return (options.client ?? _heyApiClient).post({ + url: '/api/v1/telephony/vobiz/hangup-callback/workflow/{workflow_id}', + ...options + }); +}; + +/** + * Handle Inbound Telephony + * Handle inbound telephony calls from any supported provider with common processing + */ +export const handleInboundTelephonyApiV1TelephonyInboundWorkflowIdPost = (options: Options) => { + return (options.client ?? _heyApiClient).post({ + url: '/api/v1/telephony/inbound/{workflow_id}', + ...options + }); +}; + +/** + * Handle Inbound Fallback + * Fallback endpoint that returns audio message when calls cannot be processed. + */ +export const handleInboundFallbackApiV1TelephonyInboundFallbackPost = (options?: Options) => { + return (options?.client ?? _heyApiClient).post({ + url: '/api/v1/telephony/inbound/fallback', + ...options + }); +}; + /** * Offer */ diff --git a/ui/src/client/types.gen.ts b/ui/src/client/types.gen.ts index cbfabd6..dbcc044 100644 --- a/ui/src/client/types.gen.ts +++ b/ui/src/client/types.gen.ts @@ -41,6 +41,8 @@ export type AuthUserResponse = { is_superuser: boolean; }; +export type CallType = 'inbound' | 'outbound'; + export type CampaignProgressResponse = { campaign_id: number; state: string; @@ -210,7 +212,7 @@ export type CreateWorkflowRunResponse = { }; export type CreateWorkflowTemplateRequest = { - call_type: 'INBOUND' | 'OUTBOUND'; + call_type: 'inbound' | 'outbound'; use_case: string; activity_description: string; }; @@ -933,6 +935,7 @@ export type WorkflowRunResponseSchema = { gathered_context?: { [key: string]: unknown; } | null; + call_type: CallType; }; export type WorkflowRunUsageResponse = { @@ -986,6 +989,7 @@ export type InitiateCallApiV1TelephonyInitiateCallPostData = { body: InitiateCallRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -1075,6 +1079,10 @@ export type HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostRespons export type HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData = { body?: never; + headers?: { + 'x-vobiz-signature'?: string | null; + 'x-vobiz-timestamp'?: string | null; + }; path: { workflow_run_id: number; }; @@ -1104,6 +1112,10 @@ export type HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRu export type HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData = { body?: never; + headers?: { + 'x-vobiz-signature'?: string | null; + 'x-vobiz-timestamp'?: string | null; + }; path: { workflow_run_id: number; }; @@ -1160,10 +1172,99 @@ export type HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWo 200: unknown; }; +export type HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData = { + body?: never; + headers?: { + 'x-vobiz-signature'?: string | null; + 'x-vobiz-timestamp'?: string | null; + }; + path: { + workflow_id: number; + }; + query?: never; + url: '/api/v1/telephony/vobiz/hangup-callback/workflow/{workflow_id}'; +}; + +export type HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostErrors = { + /** + * Not found + */ + 404: unknown; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError = HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostErrors[keyof HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostErrors]; + +export type HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + +export type HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData = { + body?: never; + headers?: { + 'x-twilio-signature'?: string | null; + 'x-vobiz-signature'?: string | null; + 'x-vobiz-timestamp'?: string | null; + }; + path: { + workflow_id: number; + }; + query?: never; + url: '/api/v1/telephony/inbound/{workflow_id}'; +}; + +export type HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostErrors = { + /** + * Not found + */ + 404: unknown; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError = HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostErrors[keyof HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostErrors]; + +export type HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + +export type HandleInboundFallbackApiV1TelephonyInboundFallbackPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/telephony/inbound/fallback'; +}; + +export type HandleInboundFallbackApiV1TelephonyInboundFallbackPostErrors = { + /** + * Not found + */ + 404: unknown; +}; + +export type HandleInboundFallbackApiV1TelephonyInboundFallbackPostResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + export type OfferApiV1PipecatRtcOfferPostData = { body: RtcOfferRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -1194,6 +1295,7 @@ export type ImpersonateApiV1SuperuserImpersonatePostData = { body: ImpersonateRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -1226,6 +1328,7 @@ export type GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: { @@ -1271,6 +1374,7 @@ export type SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData = { body: AdminCommentRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { run_id: number; @@ -1305,6 +1409,7 @@ export type ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { workflow_id: number; @@ -1339,6 +1444,7 @@ export type CreateWorkflowApiV1WorkflowCreateDefinitionPostData = { body: CreateWorkflowRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -1371,6 +1477,7 @@ export type CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData = { body: CreateWorkflowTemplateRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -1403,6 +1510,7 @@ export type GetWorkflowsApiV1WorkflowFetchGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: { @@ -1440,6 +1548,7 @@ export type GetWorkflowApiV1WorkflowFetchWorkflowIdGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { workflow_id: number; @@ -1474,6 +1583,7 @@ export type GetWorkflowsSummaryApiV1WorkflowSummaryGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -1506,6 +1616,7 @@ export type UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData = { body: UpdateWorkflowStatusRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { workflow_id: number; @@ -1540,6 +1651,7 @@ export type UpdateWorkflowApiV1WorkflowWorkflowIdPutData = { body: UpdateWorkflowRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { workflow_id: number; @@ -1574,6 +1686,7 @@ export type GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { workflow_id: number; @@ -1615,6 +1728,7 @@ export type CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData = { body: CreateWorkflowRunRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { workflow_id: number; @@ -1649,6 +1763,7 @@ export type GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { workflow_id: number; @@ -1707,6 +1822,7 @@ export type DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData = { body: DuplicateTemplateRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -1762,6 +1878,7 @@ export type GetAuthUserApiV1UserAuthUserGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -1794,6 +1911,7 @@ export type GetUserConfigurationsApiV1UserConfigurationsUserGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -1826,6 +1944,7 @@ export type UpdateUserConfigurationsApiV1UserConfigurationsUserPutData = { body: UserConfigurationRequestResponseSchema; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -1858,6 +1977,7 @@ export type ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: { @@ -1892,6 +2012,7 @@ export type GetApiKeysApiV1UserApiKeysGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: { @@ -1926,6 +2047,7 @@ export type CreateApiKeyApiV1UserApiKeysPostData = { body: CreateApiKeyRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -1958,6 +2080,7 @@ export type ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { api_key_id: number; @@ -1994,6 +2117,7 @@ export type ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { api_key_id: number; @@ -2030,6 +2154,7 @@ export type GetVoicesApiV1UserConfigurationsVoicesProviderGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { provider: 'elevenlabs' | 'deepgram' | 'sarvam' | 'cartesia' | 'dograh'; @@ -2064,6 +2189,7 @@ export type CreateCampaignApiV1CampaignCreatePostData = { body: CreateCampaignRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -2096,6 +2222,7 @@ export type GetCampaignsApiV1CampaignGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -2128,6 +2255,7 @@ export type GetCampaignApiV1CampaignCampaignIdGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { campaign_id: number; @@ -2162,6 +2290,7 @@ export type StartCampaignApiV1CampaignCampaignIdStartPostData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { campaign_id: number; @@ -2196,6 +2325,7 @@ export type PauseCampaignApiV1CampaignCampaignIdPausePostData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { campaign_id: number; @@ -2230,6 +2360,7 @@ export type GetCampaignRunsApiV1CampaignCampaignIdRunsGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { campaign_id: number; @@ -2264,6 +2395,7 @@ export type ResumeCampaignApiV1CampaignCampaignIdResumePostData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { campaign_id: number; @@ -2298,6 +2430,7 @@ export type GetCampaignProgressApiV1CampaignCampaignIdProgressGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { campaign_id: number; @@ -2332,6 +2465,7 @@ export type GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrl body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { campaign_id: number; @@ -2366,6 +2500,7 @@ export type ListCredentialsApiV1CredentialsGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -2398,6 +2533,7 @@ export type CreateCredentialApiV1CredentialsPostData = { body: CreateCredentialRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -2430,6 +2566,7 @@ export type DeleteCredentialApiV1CredentialsCredentialUuidDeleteData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { credential_uuid: string; @@ -2466,6 +2603,7 @@ export type GetCredentialApiV1CredentialsCredentialUuidGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { credential_uuid: string; @@ -2500,6 +2638,7 @@ export type UpdateCredentialApiV1CredentialsCredentialUuidPutData = { body: UpdateCredentialRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { credential_uuid: string; @@ -2534,6 +2673,7 @@ export type ListToolsApiV1ToolsGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: { @@ -2569,6 +2709,7 @@ export type CreateToolApiV1ToolsPostData = { body: CreateToolRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -2601,6 +2742,7 @@ export type DeleteToolApiV1ToolsToolUuidDeleteData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { tool_uuid: string; @@ -2637,6 +2779,7 @@ export type GetToolApiV1ToolsToolUuidGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { tool_uuid: string; @@ -2671,6 +2814,7 @@ export type UpdateToolApiV1ToolsToolUuidPutData = { body: UpdateToolRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { tool_uuid: string; @@ -2705,6 +2849,7 @@ export type GetIntegrationsApiV1IntegrationGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -2737,6 +2882,7 @@ export type CreateSessionApiV1IntegrationSessionPostData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -2769,6 +2915,7 @@ export type UpdateIntegrationApiV1IntegrationIntegrationIdPutData = { body: UpdateIntegrationRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { integration_id: number; @@ -2803,6 +2950,7 @@ export type GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGet body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { integration_id: number; @@ -2837,6 +2985,7 @@ export type GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData = body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -2869,6 +3018,7 @@ export type SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData body: TwilioConfigurationRequest | VonageConfigurationRequest | VobizConfigurationRequest | CloudonixConfigurationRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -2899,6 +3049,7 @@ export type GetSignedUrlApiV1S3SignedUrlGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query: { @@ -2938,6 +3089,7 @@ export type GetFileMetadataApiV1S3FileMetadataGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query: { @@ -2975,6 +3127,7 @@ export type GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData = { body: PresignedUploadUrlRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -3007,6 +3160,7 @@ export type GetServiceKeysApiV1UserServiceKeysGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: { @@ -3041,6 +3195,7 @@ export type CreateServiceKeyApiV1UserServiceKeysPostData = { body: CreateServiceKeyRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -3073,6 +3228,7 @@ export type ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { service_key_id: string; @@ -3105,6 +3261,7 @@ export type ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutDat body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { service_key_id: string; @@ -3137,6 +3294,7 @@ export type ListTestSessionsApiV1LooptalkTestSessionsGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: { @@ -3174,6 +3332,7 @@ export type CreateTestSessionApiV1LooptalkTestSessionsPostData = { body: CreateTestSessionRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -3206,6 +3365,7 @@ export type GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { test_session_id: number; @@ -3240,6 +3400,7 @@ export type StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { test_session_id: number; @@ -3272,6 +3433,7 @@ export type StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData = body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { test_session_id: number; @@ -3304,6 +3466,7 @@ export type GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConv body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { test_session_id: number; @@ -3336,6 +3499,7 @@ export type CreateLoadTestApiV1LooptalkLoadTestsPostData = { body: CreateLoadTestRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -3370,6 +3534,7 @@ export type GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData = body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { load_test_group_id: string; @@ -3404,6 +3569,7 @@ export type GetActiveTestsApiV1LooptalkActiveTestsGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -3434,6 +3600,7 @@ export type GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -3466,6 +3633,7 @@ export type GetUsageHistoryApiV1OrganizationsUsageRunsGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: { @@ -3513,6 +3681,7 @@ export type GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData = body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: { @@ -3550,6 +3719,7 @@ export type GetDailyReportApiV1OrganizationsReportsDailyGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query: { @@ -3595,6 +3765,7 @@ export type GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query?: never; @@ -3627,6 +3798,7 @@ export type GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path?: never; query: { @@ -3816,6 +3988,7 @@ export type DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { workflow_id: number; @@ -3852,6 +4025,7 @@ export type GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData = { body?: never; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { workflow_id: number; @@ -3886,6 +4060,7 @@ export type CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData = body: EmbedTokenRequest; headers?: { authorization?: string | null; + 'X-API-Key'?: string | null; }; path: { workflow_id: number;