From 18c4466fba351a856c2f070141f282281c1fa249 Mon Sep 17 00:00:00 2001 From: Alvaro Navarro Date: Fri, 27 Feb 2026 10:55:31 +0100 Subject: [PATCH 1/2] feat(voice): add AnswerWebhook support --- voice/src/vonage_voice/models/__init__.py | 2 ++ voice/src/vonage_voice/models/webhooks.py | 33 +++++++++++++++++++++++ voice/tests/test_answer_webhook.py | 25 +++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 voice/src/vonage_voice/models/webhooks.py create mode 100644 voice/tests/test_answer_webhook.py diff --git a/voice/src/vonage_voice/models/__init__.py b/voice/src/vonage_voice/models/__init__.py index aeb86a7b..7f42cfe1 100644 --- a/voice/src/vonage_voice/models/__init__.py +++ b/voice/src/vonage_voice/models/__init__.py @@ -23,6 +23,7 @@ ToPhone, TtsStreamOptions, ) +from .webhooks import AnswerWebhook from .responses import ( CallInfo, CallList, @@ -36,6 +37,7 @@ 'AdvancedMachineDetection', 'AppEndpoint', 'AudioStreamOptions', + 'AnswerWebhook', 'CallInfo', 'CallList', 'CallMessage', diff --git a/voice/src/vonage_voice/models/webhooks.py b/voice/src/vonage_voice/models/webhooks.py new file mode 100644 index 00000000..1b4b7515 --- /dev/null +++ b/voice/src/vonage_voice/models/webhooks.py @@ -0,0 +1,33 @@ +from typing import Optional + +from pydantic import BaseModel, Field + + +class AnswerWebhook(BaseModel): + """Model for the Voice API Answer webhook payload. + + Args: + to (str, Optional): The number or endpoint that answered the call. + from_ (str, Optional): The number or endpoint that initiated the call. + from_user (str, Optional): The Client SDK user that initiated the call. + endpoint_type (str, Optional): The type of endpoint that answered the call. + uuid (str, Optional): The unique identifier for this call. + conversation_uuid (str, Optional): The unique identifier for this conversation. + region_url (str, Optional): Regional API endpoint to control the call. + custom_data (dict, Optional): Custom data object passed from the Client SDK. + sipheader_user_to_user (str, Optional): Content of the SIP User-to-User header, + received as the `SipHeader_User-to-User` parameter on the webhook. + """ + + to: Optional[str] = None + from_: Optional[str] = Field(None, alias='from') + from_user: Optional[str] = None + endpoint_type: Optional[str] = None + uuid: Optional[str] = None + conversation_uuid: Optional[str] = None + region_url: Optional[str] = None + custom_data: Optional[dict] = None + sipheader_user_to_user: Optional[str] = Field( + None, serialization_alias='SipHeader_User-to-User' + ) + diff --git a/voice/tests/test_answer_webhook.py b/voice/tests/test_answer_webhook.py new file mode 100644 index 00000000..2387db1e --- /dev/null +++ b/voice/tests/test_answer_webhook.py @@ -0,0 +1,25 @@ +from vonage_voice.models import AnswerWebhook + + +def test_answer_webhook_sipheader_user_to_user_alias(): + payload = { + 'to': '442079460000', + 'from': '447700900000', + 'uuid': 'aaaaaaaa-bbbb-cccc-dddd-0123456789ab', + 'conversation_uuid': 'CON-aaaaaaaa-bbbb-cccc-dddd-0123456789ab', + 'SipHeader_User-to-User': '1234567890abcdef;encoding=hex', + } + + hook = AnswerWebhook(**payload) + + assert hook.to == '442079460000' + assert hook.from_ == '447700900000' + assert ( + hook.sipheader_user_to_user == '1234567890abcdef;encoding=hex' + ), 'Field should be populated from SipHeader_User-to-User' + + dumped = hook.model_dump(by_alias=True, exclude_none=True) + assert ( + dumped['SipHeader_User-to-User'] == '1234567890abcdef;encoding=hex' + ), 'Field should serialize back with the SipHeader_User-to-User key' + From 92205689c9f08f2839b07b1f2ebacc2afb427d0d Mon Sep 17 00:00:00 2001 From: Alvaro Navarro Date: Fri, 27 Feb 2026 17:22:01 +0100 Subject: [PATCH 2/2] fix(voice): lint and test --- voice/src/vonage_voice/models/__init__.py | 2 +- voice/src/vonage_voice/models/webhooks.py | 9 ++++++--- voice/tests/test_answer_webhook.py | 1 - voice/tests/test_voice.py | 3 +-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/voice/src/vonage_voice/models/__init__.py b/voice/src/vonage_voice/models/__init__.py index 7f42cfe1..7e03cc52 100644 --- a/voice/src/vonage_voice/models/__init__.py +++ b/voice/src/vonage_voice/models/__init__.py @@ -23,7 +23,6 @@ ToPhone, TtsStreamOptions, ) -from .webhooks import AnswerWebhook from .responses import ( CallInfo, CallList, @@ -32,6 +31,7 @@ Embedded, HalLinks, ) +from .webhooks import AnswerWebhook __all__ = [ 'AdvancedMachineDetection', diff --git a/voice/src/vonage_voice/models/webhooks.py b/voice/src/vonage_voice/models/webhooks.py index 1b4b7515..6fb84a1b 100644 --- a/voice/src/vonage_voice/models/webhooks.py +++ b/voice/src/vonage_voice/models/webhooks.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field class AnswerWebhook(BaseModel): @@ -19,6 +19,8 @@ class AnswerWebhook(BaseModel): received as the `SipHeader_User-to-User` parameter on the webhook. """ + model_config = ConfigDict(populate_by_name=True) + to: Optional[str] = None from_: Optional[str] = Field(None, alias='from') from_user: Optional[str] = None @@ -28,6 +30,7 @@ class AnswerWebhook(BaseModel): region_url: Optional[str] = None custom_data: Optional[dict] = None sipheader_user_to_user: Optional[str] = Field( - None, serialization_alias='SipHeader_User-to-User' + None, + validation_alias='SipHeader_User-to-User', + serialization_alias='SipHeader_User-to-User', ) - diff --git a/voice/tests/test_answer_webhook.py b/voice/tests/test_answer_webhook.py index 2387db1e..ab56057a 100644 --- a/voice/tests/test_answer_webhook.py +++ b/voice/tests/test_answer_webhook.py @@ -22,4 +22,3 @@ def test_answer_webhook_sipheader_user_to_user_alias(): assert ( dumped['SipHeader_User-to-User'] == '1234567890abcdef;encoding=hex' ), 'Field should serialize back with the SipHeader_User-to-User key' - diff --git a/voice/tests/test_voice.py b/voice/tests/test_voice.py index cfcd459e..b1a7c0de 100644 --- a/voice/tests/test_voice.py +++ b/voice/tests/test_voice.py @@ -4,6 +4,7 @@ import responses from pytest import raises from responses.matchers import json_params_matcher +from testutils import build_response, get_mock_jwt_auth from vonage_http_client.http_client import HttpClient from vonage_voice import ( AudioStreamOptions, @@ -17,8 +18,6 @@ from vonage_voice.models.responses import CreateCallResponse from vonage_voice.voice import Voice -from testutils import build_response, get_mock_jwt_auth - path = abspath(__file__)