From 54bdeee7f015b5c49cd561d3d1a5ce0ea547e13b Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Tue, 17 Mar 2026 21:31:49 +0100 Subject: [PATCH 01/10] DEVEXP-1310: Redesign Webhooks - SMS --- examples/webhooks/sms_api/controller.py | 2 +- .../webhooks/sms_api/server_business_logic.py | 4 +- sinch/domains/sms/api/v1/batches_apis.py | 96 +++++++++---------- .../v1/internal/update_binary_request.py | 3 +- .../v1/internal/update_media_request.py | 3 +- .../models/v1/internal/update_text_request.py | 3 +- .../sms/models/v1/shared/binary_request.py | 3 +- .../sms/models/v1/shared/binary_response.py | 6 +- .../sms/models/v1/shared/media_request.py | 3 +- .../sms/models/v1/shared/media_response.py | 3 +- .../sms/models/v1/shared/text_request.py | 3 +- .../sms/models/v1/shared/text_response.py | 3 +- sinch/domains/sms/sinch_events/v1/__init__.py | 5 + .../v1/events/__init__.py | 2 +- .../v1/events/sms_sinch_event.py} | 2 +- .../sms/sinch_events/v1/internal/__init__.py | 5 + .../v1/internal/webhook_event.py | 0 .../v1/sms_sinch_event.py} | 4 +- sinch/domains/sms/sms.py | 12 +-- .../sms/webhooks/v1/internal/__init__.py | 5 - .../e2e/sms/features/steps/webhooks.steps.py | 6 +- .../batches/test_send_batches_endpoint.py | 15 +++ .../internal/test_dry_run_request_model.py | 12 +-- .../test_replace_binary_request_model.py | 4 +- .../test_replace_media_request_model.py | 4 +- ...date_binary_request_with_batch_id_model.py | 4 +- ...pdate_media_request_with_batch_id_model.py | 4 +- ...update_text_request_with_batch_id_model.py | 4 +- tests/unit/domains/sms/v1/test_batches.py | 6 +- 29 files changed, 128 insertions(+), 98 deletions(-) create mode 100644 sinch/domains/sms/sinch_events/v1/__init__.py rename sinch/domains/sms/{webhooks => sinch_events}/v1/events/__init__.py (80%) rename sinch/domains/sms/{webhooks/v1/events/sms_webhooks_event.py => sinch_events/v1/events/sms_sinch_event.py} (98%) create mode 100644 sinch/domains/sms/sinch_events/v1/internal/__init__.py rename sinch/domains/sms/{webhooks => sinch_events}/v1/internal/webhook_event.py (100%) rename sinch/domains/sms/{webhooks/v1/sms_webhooks.py => sinch_events/v1/sms_sinch_event.py} (98%) delete mode 100644 sinch/domains/sms/webhooks/v1/internal/__init__.py diff --git a/examples/webhooks/sms_api/controller.py b/examples/webhooks/sms_api/controller.py index b5bd50f5..f3418e26 100644 --- a/examples/webhooks/sms_api/controller.py +++ b/examples/webhooks/sms_api/controller.py @@ -14,7 +14,7 @@ def sms_event(self): headers = dict(request.headers) raw_body = request.raw_body if request.raw_body else b"" - webhooks_service = self.sinch_client.sms.webhooks(self.webhooks_secret) + webhooks_service = self.sinch_client.sms.sinch_events(self.webhooks_secret) # Signature headers may be absent unless your account manager enables them # (see README: Configuration -> Controller Settings -> SMS controller); diff --git a/examples/webhooks/sms_api/server_business_logic.py b/examples/webhooks/sms_api/server_business_logic.py index aa47e470..d13e6fc2 100644 --- a/examples/webhooks/sms_api/server_business_logic.py +++ b/examples/webhooks/sms_api/server_business_logic.py @@ -1,11 +1,11 @@ -from sinch.domains.sms.webhooks.v1.events.sms_webhooks_event import IncomingSMSWebhookEvent +from sinch.domains.sms.sinch_events.v1.events.sms_sinch_event import IncomingSMSWebhookEvent def handle_sms_event(sms_event: IncomingSMSWebhookEvent, logger): """ This method handles an SMS event. Args: - sms_event (SmsWebhooksEvent): The SMS event data. + sms_event (IncomingSMSWebhookEvent): The SMS event data. logger (logging.Logger, optional): Logger instance for logging. Defaults to None. """ logger.info(f'Handling SMS event:\n{sms_event.model_dump_json(indent=2)}') diff --git a/sinch/domains/sms/api/v1/batches_apis.py b/sinch/domains/sms/api/v1/batches_apis.py index 843ad482..e4a34576 100644 --- a/sinch/domains/sms/api/v1/batches_apis.py +++ b/sinch/domains/sms/api/v1/batches_apis.py @@ -143,7 +143,7 @@ def dry_run_sms( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, flash_message: Optional[bool] = None, @@ -175,8 +175,8 @@ def dry_run_sms( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -209,7 +209,7 @@ def dry_run_sms( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, flash_message=flash_message, @@ -232,7 +232,7 @@ def dry_run_binary( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, from_ton: Optional[int] = None, @@ -261,8 +261,8 @@ def dry_run_binary( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -289,7 +289,7 @@ def dry_run_binary( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, from_ton=from_ton, @@ -309,7 +309,7 @@ def dry_run_mms( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, strict_validation: Optional[bool] = None, @@ -337,8 +337,8 @@ def dry_run_mms( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -363,7 +363,7 @@ def dry_run_mms( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, strict_validation=strict_validation, @@ -487,7 +487,7 @@ def replace_sms( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, flash_message: Optional[bool] = None, @@ -516,8 +516,8 @@ def replace_sms( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -550,7 +550,7 @@ def replace_sms( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, flash_message=flash_message, @@ -573,7 +573,7 @@ def replace_binary( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, from_ton: Optional[int] = None, @@ -600,8 +600,8 @@ def replace_binary( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -627,7 +627,7 @@ def replace_binary( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, from_ton=from_ton, @@ -645,7 +645,7 @@ def replace_mms( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, strict_validation: Optional[bool] = None, @@ -670,8 +670,8 @@ def replace_mms( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -696,7 +696,7 @@ def replace_mms( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, strict_validation=strict_validation, @@ -751,7 +751,7 @@ def send_sms( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, flash_message: Optional[bool] = None, @@ -784,8 +784,8 @@ def send_sms( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -817,7 +817,7 @@ def send_sms( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, flash_message=flash_message, @@ -839,7 +839,7 @@ def send_binary( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, from_ton: Optional[int] = None, @@ -870,8 +870,8 @@ def send_binary( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -896,7 +896,7 @@ def send_binary( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, from_ton=from_ton, @@ -913,7 +913,7 @@ def send_mms( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, strict_validation: Optional[bool] = None, @@ -942,8 +942,8 @@ def send_mms( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -967,7 +967,7 @@ def send_mms( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, strict_validation=strict_validation, @@ -1057,7 +1057,7 @@ def update_sms( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, parameters: Optional[Dict[str, Dict[str, str]]] = None, @@ -1087,8 +1087,8 @@ def update_sms( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -1122,7 +1122,7 @@ def update_sms( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, parameters=parameters, @@ -1146,7 +1146,7 @@ def update_binary( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, from_ton: Optional[int] = None, @@ -1174,8 +1174,8 @@ def update_binary( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -1202,7 +1202,7 @@ def update_binary( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, from_ton=from_ton, @@ -1221,7 +1221,7 @@ def update_mms( delivery_report: Optional[DeliveryReportType] = None, send_at: Optional[datetime] = None, expire_at: Optional[datetime] = None, - callback_url: Optional[str] = None, + event_destination_target: Optional[str] = None, client_reference: Optional[str] = None, feedback_enabled: Optional[bool] = None, parameters: Optional[Dict[str, Dict[str, str]]] = None, @@ -1247,8 +1247,8 @@ def update_mms( :type send_at: Optional[datetime] :param expire_at: The time to expire the message at. (optional) :type expire_at: Optional[datetime] - :param callback_url: The callback URL to receive the delivery report. (optional) - :type callback_url: Optional[str] + :param event_destination_target: The callback URL to receive the delivery report. (optional) + :type event_destination_target: Optional[str] :param client_reference: The client reference to identify the message. (optional) :type client_reference: Optional[str] :param feedback_enabled: Whether to enable feedback. (optional) @@ -1274,7 +1274,7 @@ def update_mms( delivery_report=delivery_report, send_at=send_at, expire_at=expire_at, - callback_url=callback_url, + event_destination_target=event_destination_target, client_reference=client_reference, feedback_enabled=feedback_enabled, parameters=parameters, diff --git a/sinch/domains/sms/models/v1/internal/update_binary_request.py b/sinch/domains/sms/models/v1/internal/update_binary_request.py index a73f090f..f23c5182 100644 --- a/sinch/domains/sms/models/v1/internal/update_binary_request.py +++ b/sinch/domains/sms/models/v1/internal/update_binary_request.py @@ -34,8 +34,9 @@ class UpdateBinaryRequest(BaseModelConfigurationRequest): default=None, description="If set, the system will stop trying to deliver the message at this point. Constraints: Must be after `send_at` Default: 3 days after `send_at` ", ) - callback_url: Optional[StrictStr] = Field( + event_destination_target: Optional[StrictStr] = Field( default=None, + alias="callback_url", description="Override the default callback URL for this batch. Constraints: Must be valid URL. ", ) client_reference: Optional[StrictStr] = Field( diff --git a/sinch/domains/sms/models/v1/internal/update_media_request.py b/sinch/domains/sms/models/v1/internal/update_media_request.py index 1aa5f6dc..ed347d3d 100644 --- a/sinch/domains/sms/models/v1/internal/update_media_request.py +++ b/sinch/domains/sms/models/v1/internal/update_media_request.py @@ -32,8 +32,9 @@ class UpdateMediaRequest(BaseModelConfigurationRequest): default=None, description="If set, the system will stop trying to deliver the message at this point. Constraints: Must be after `send_at` Default: 3 days after `send_at` ", ) - callback_url: Optional[StrictStr] = Field( + event_destination_target: Optional[StrictStr] = Field( default=None, + alias="callback_url", description="Override the default callback URL for this batch. Constraints: Must be valid URL. ", ) client_reference: Optional[StrictStr] = Field( diff --git a/sinch/domains/sms/models/v1/internal/update_text_request.py b/sinch/domains/sms/models/v1/internal/update_text_request.py index 81e4e2ea..d43ee83f 100644 --- a/sinch/domains/sms/models/v1/internal/update_text_request.py +++ b/sinch/domains/sms/models/v1/internal/update_text_request.py @@ -39,8 +39,9 @@ class UpdateTextRequest(BaseModelConfigurationRequest): default=None, description="If set, the system will stop trying to deliver the message at this point. Constraints: Must be after `send_at` Default: 3 days after `send_at` ", ) - callback_url: Optional[StrictStr] = Field( + event_destination_target: Optional[StrictStr] = Field( default=None, + alias="callback_url", description="Override the default callback URL for this batch. Constraints: Must be valid URL. ", ) client_reference: Optional[StrictStr] = Field( diff --git a/sinch/domains/sms/models/v1/shared/binary_request.py b/sinch/domains/sms/models/v1/shared/binary_request.py index 027d98e9..35fc4146 100644 --- a/sinch/domains/sms/models/v1/shared/binary_request.py +++ b/sinch/domains/sms/models/v1/shared/binary_request.py @@ -40,8 +40,9 @@ class BinaryRequest(BaseModelConfigurationRequest): default=None, description="If set, the system will stop trying to deliver the message at this point. Must be after `send_at`. Default and max is 3 days after `send_at`. Formatted as [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601). For example: `YYYY-MM-DDThh:mm:ss.SSSZ`.", ) - callback_url: Optional[StrictStr] = Field( + event_destination_target: Optional[StrictStr] = Field( default=None, + alias="callback_url", description="Override the *default* callback URL for this batch. Must be a valid URL. Learn how to set a default callback URL [here](https://community.sinch.com/t5/SMS/How-do-I-assign-a-callback-URL-to-an-SMS-service-plan/ta-p/8414).", ) client_reference: Optional[StrictStr] = Field( diff --git a/sinch/domains/sms/models/v1/shared/binary_response.py b/sinch/domains/sms/models/v1/shared/binary_response.py index 171b2fd2..96309913 100644 --- a/sinch/domains/sms/models/v1/shared/binary_response.py +++ b/sinch/domains/sms/models/v1/shared/binary_response.py @@ -61,8 +61,10 @@ class BinaryResponse(BaseModelConfigurationResponse): default=None, description="If set, the date and time the message will expire. Formatted as [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601). For example: `YYYY-MM-DDThh:mm:ss.SSSZ`.", ) - callback_url: Optional[StrictStr] = Field( - default=None, description="The callback URL provided in the request." + event_destination_target: Optional[StrictStr] = Field( + default=None, + alias="callback_url", + description="The callback URL provided in the request.", ) client_reference: Optional[StrictStr] = Field( default=None, diff --git a/sinch/domains/sms/models/v1/shared/media_request.py b/sinch/domains/sms/models/v1/shared/media_request.py index 54ef9203..57d5311a 100644 --- a/sinch/domains/sms/models/v1/shared/media_request.py +++ b/sinch/domains/sms/models/v1/shared/media_request.py @@ -35,8 +35,9 @@ class MediaRequest(BaseModelConfigurationRequest): default=None, description="If set, the system will stop trying to deliver the message at this point. Must be after `send_at`. Default and max is 3 days after `send_at`. Formatted as [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601): `YYYY-MM-DDThh:mm:ss.SSSZ`. ", ) - callback_url: Optional[StrictStr] = Field( + event_destination_target: Optional[StrictStr] = Field( default=None, + alias="callback_url", description="Override the default callback URL for this batch. Must be valid URL.", ) client_reference: Optional[StrictStr] = Field( diff --git a/sinch/domains/sms/models/v1/shared/media_response.py b/sinch/domains/sms/models/v1/shared/media_response.py index 7da3a82f..5971208a 100644 --- a/sinch/domains/sms/models/v1/shared/media_response.py +++ b/sinch/domains/sms/models/v1/shared/media_response.py @@ -50,8 +50,9 @@ class MediaResponse(BaseModelConfigurationResponse): default=None, description="If set the system will stop trying to deliver the message at this point. Must be after `send_at`. Default and max is 3 days after send_at. YYYY-MM-DDThh:mm:ss.SSSZ format", ) - callback_url: Optional[StrictStr] = Field( + event_destination_target: Optional[StrictStr] = Field( default=None, + alias="callback_url", description="Override the default callback URL for this batch. Must be valid URL.", ) client_reference: Optional[StrictStr] = Field( diff --git a/sinch/domains/sms/models/v1/shared/text_request.py b/sinch/domains/sms/models/v1/shared/text_request.py index 2416633b..e5ecce0f 100644 --- a/sinch/domains/sms/models/v1/shared/text_request.py +++ b/sinch/domains/sms/models/v1/shared/text_request.py @@ -42,8 +42,9 @@ class TextRequest(BaseModelConfigurationRequest): default=None, description="If set, the system will stop trying to deliver the message at this point. Must be after `send_at`. Default and max is 3 days after `send_at`. Formatted as [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601): `YYYY-MM-DDThh:mm:ss.SSSZ`.", ) - callback_url: Optional[StrictStr] = Field( + event_destination_target: Optional[StrictStr] = Field( default=None, + alias="callback_url", description="Override the *default* callback URL for this batch. Must be a valid URL. Learn how to set a default callback URL [here](https://community.sinch.com/t5/SMS/How-do-I-assign-a-callback-URL-to-an-SMS-service-plan/ta-p/8414).", ) client_reference: Optional[StrictStr] = Field( diff --git a/sinch/domains/sms/models/v1/shared/text_response.py b/sinch/domains/sms/models/v1/shared/text_response.py index 650906e5..f5fc5975 100644 --- a/sinch/domains/sms/models/v1/shared/text_response.py +++ b/sinch/domains/sms/models/v1/shared/text_response.py @@ -51,8 +51,9 @@ class TextResponse(BaseModelConfigurationResponse): default=None, description="If set, the system will stop trying to deliver the message at this point. Must be after `send_at`. Default and max is 3 days after `send_at`. Formatted as [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601): `YYYY-MM-DDThh:mm:ss.SSSZ`.", ) - callback_url: Optional[StrictStr] = Field( + event_destination_target: Optional[StrictStr] = Field( default=None, + alias="callback_url", description="Override the default callback URL for this batch. Must be valid URL.", ) client_reference: Optional[StrictStr] = Field( diff --git a/sinch/domains/sms/sinch_events/v1/__init__.py b/sinch/domains/sms/sinch_events/v1/__init__.py new file mode 100644 index 00000000..522d374f --- /dev/null +++ b/sinch/domains/sms/sinch_events/v1/__init__.py @@ -0,0 +1,5 @@ +from sinch.domains.sms.sinch_events.v1.sms_sinch_event import ( + SmsSinchEvent, +) + +__all__ = ["SmsSinchEvent"] diff --git a/sinch/domains/sms/webhooks/v1/events/__init__.py b/sinch/domains/sms/sinch_events/v1/events/__init__.py similarity index 80% rename from sinch/domains/sms/webhooks/v1/events/__init__.py rename to sinch/domains/sms/sinch_events/v1/events/__init__.py index bb5a10da..66598629 100644 --- a/sinch/domains/sms/webhooks/v1/events/__init__.py +++ b/sinch/domains/sms/sinch_events/v1/events/__init__.py @@ -1,4 +1,4 @@ -from sinch.domains.sms.webhooks.v1.events.sms_webhooks_event import ( +from sinch.domains.sms.sinch_events.v1.events.sms_sinch_event import ( IncomingSMSWebhookEvent, MOTextWebhookEvent, MOBinaryWebhookEvent, diff --git a/sinch/domains/sms/webhooks/v1/events/sms_webhooks_event.py b/sinch/domains/sms/sinch_events/v1/events/sms_sinch_event.py similarity index 98% rename from sinch/domains/sms/webhooks/v1/events/sms_webhooks_event.py rename to sinch/domains/sms/sinch_events/v1/events/sms_sinch_event.py index f8abe3cb..330738a1 100644 --- a/sinch/domains/sms/webhooks/v1/events/sms_webhooks_event.py +++ b/sinch/domains/sms/sinch_events/v1/events/sms_sinch_event.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Optional, Union, Literal, Annotated from pydantic import Field, StrictStr, StrictInt, conlist -from sinch.domains.sms.webhooks.v1.internal import WebhookEvent +from sinch.domains.sms.sinch_events.v1.internal import WebhookEvent class MediaItem(WebhookEvent): diff --git a/sinch/domains/sms/sinch_events/v1/internal/__init__.py b/sinch/domains/sms/sinch_events/v1/internal/__init__.py new file mode 100644 index 00000000..b1bd904d --- /dev/null +++ b/sinch/domains/sms/sinch_events/v1/internal/__init__.py @@ -0,0 +1,5 @@ +from sinch.domains.sms.sinch_events.v1.internal.webhook_event import ( + WebhookEvent, +) + +__all__ = ["WebhookEvent"] diff --git a/sinch/domains/sms/webhooks/v1/internal/webhook_event.py b/sinch/domains/sms/sinch_events/v1/internal/webhook_event.py similarity index 100% rename from sinch/domains/sms/webhooks/v1/internal/webhook_event.py rename to sinch/domains/sms/sinch_events/v1/internal/webhook_event.py diff --git a/sinch/domains/sms/webhooks/v1/sms_webhooks.py b/sinch/domains/sms/sinch_events/v1/sms_sinch_event.py similarity index 98% rename from sinch/domains/sms/webhooks/v1/sms_webhooks.py rename to sinch/domains/sms/sinch_events/v1/sms_sinch_event.py index c64dde6e..8a4d5dbf 100644 --- a/sinch/domains/sms/webhooks/v1/sms_webhooks.py +++ b/sinch/domains/sms/sinch_events/v1/sms_sinch_event.py @@ -9,7 +9,7 @@ parse_json, normalize_iso_timestamp, ) -from sinch.domains.sms.webhooks.v1.events import ( +from sinch.domains.sms.sinch_events.v1.events import ( IncomingSMSWebhookEvent, MOTextWebhookEvent, MOBinaryWebhookEvent, @@ -30,7 +30,7 @@ ] -class SmsWebhooks: +class SmsSinchEvent: def __init__(self, app_secret: Optional[str] = None): self.app_secret = app_secret diff --git a/sinch/domains/sms/sms.py b/sinch/domains/sms/sms.py index 3c5c3ff3..2a15ac7e 100644 --- a/sinch/domains/sms/sms.py +++ b/sinch/domains/sms/sms.py @@ -2,7 +2,7 @@ Batches, DeliveryReports, ) -from sinch.domains.sms.webhooks.v1.sms_webhooks import SmsWebhooks +from sinch.domains.sms.sinch_events.v1.sms_sinch_event import SmsSinchEvent class SMS: @@ -17,13 +17,13 @@ def __init__(self, sinch): self.batches = Batches(self._sinch) self.delivery_reports = DeliveryReports(self._sinch) - def webhooks(self, callback_secret: str) -> SmsWebhooks: + def sinch_events(self, callback_secret: str) -> SmsSinchEvent: """ - Create an SMS webhooks handler with the specified callback secret. + Create an SMS Sinch Events handler with the specified callback secret. :param callback_secret: Secret used for webhook validation. :type callback_secret: str - :returns: A configured webhooks handler - :rtype: SmsWebhooks + :returns: A configured Sinch Events handler + :rtype: SmsSinchEvent """ - return SmsWebhooks(callback_secret) + return SmsSinchEvent(callback_secret) diff --git a/sinch/domains/sms/webhooks/v1/internal/__init__.py b/sinch/domains/sms/webhooks/v1/internal/__init__.py deleted file mode 100644 index 329bdf65..00000000 --- a/sinch/domains/sms/webhooks/v1/internal/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from sinch.domains.sms.webhooks.v1.internal.webhook_event import ( - WebhookEvent, -) - -__all__ = ["WebhookEvent"] diff --git a/tests/e2e/sms/features/steps/webhooks.steps.py b/tests/e2e/sms/features/steps/webhooks.steps.py index 096ae9cd..d153720f 100644 --- a/tests/e2e/sms/features/steps/webhooks.steps.py +++ b/tests/e2e/sms/features/steps/webhooks.steps.py @@ -1,8 +1,8 @@ import requests from datetime import datetime, timezone from behave import given, when, then -from sinch.domains.sms.webhooks.v1.sms_webhooks import SmsWebhooks -from sinch.domains.sms.webhooks.v1.events import ( +from sinch.domains.sms.sinch_events.v1.sms_sinch_event import SmsSinchEvent +from sinch.domains.sms.sinch_events.v1.events import ( MOTextWebhookEvent, ) from sinch.domains.sms.models.v1.response import ( @@ -16,7 +16,7 @@ @given('the SMS Webhooks handler is available') def step_webhook_handler_is_available(context): - context.sms_webhook = SmsWebhooks(SINCH_SMS_CALLBACK_SECRET) + context.sms_webhook = SmsSinchEvent(SINCH_SMS_CALLBACK_SECRET) @when('I send a request to trigger an "incoming SMS" event') diff --git a/tests/unit/domains/sms/v1/endpoints/batches/test_send_batches_endpoint.py b/tests/unit/domains/sms/v1/endpoints/batches/test_send_batches_endpoint.py index f20303b1..41aee1f7 100644 --- a/tests/unit/domains/sms/v1/endpoints/batches/test_send_batches_endpoint.py +++ b/tests/unit/domains/sms/v1/endpoints/batches/test_send_batches_endpoint.py @@ -92,6 +92,21 @@ def test_request_body_expects_text_request_data(text_request_data): assert body["body"] == "Your verification code is 123456" +def test_request_body_uses_callback_url_alias_for_event_destination_target(): + """Ensure event_destination_target is serialized as backend callback_url.""" + request = TextRequest( + to=["+46701234567"], + from_="+46701111111", + body="Hello", + event_destination_target="https://example.com/callback", + ) + endpoint = SendSMSEndpoint("test_project_id", request) + body = json.loads(endpoint.request_body()) + + assert body["callback_url"] == "https://example.com/callback" + assert "event_destination_target" not in body + + def test_request_body_expects_binary_request_data(binary_request_data): """Test that binary request body contains correct fields.""" endpoint = SendSMSEndpoint("test_project_id", binary_request_data) diff --git a/tests/unit/domains/sms/v1/models/internal/test_dry_run_request_model.py b/tests/unit/domains/sms/v1/models/internal/test_dry_run_request_model.py index c308af5a..a3560f01 100644 --- a/tests/unit/domains/sms/v1/models/internal/test_dry_run_request_model.py +++ b/tests/unit/domains/sms/v1/models/internal/test_dry_run_request_model.py @@ -119,7 +119,7 @@ def test_dry_run_text_request_expects_valid_inputs_and_all_fields( delivery_report="summary", send_at=send_at, expire_at=expire_at, - callback_url="https://capybara.com/callback", + event_destination_target="https://capybara.com/callback", client_reference="test-ref", feedback_enabled=True, flash_message=False, @@ -134,7 +134,7 @@ def test_dry_run_text_request_expects_valid_inputs_and_all_fields( assert request.delivery_report == "summary" assert request.send_at == send_at assert request.expire_at == expire_at - assert request.callback_url == "https://capybara.com/callback" + assert request.event_destination_target == "https://capybara.com/callback" assert request.client_reference == "test-ref" assert request.feedback_enabled is True assert request.flash_message is False @@ -191,7 +191,7 @@ def test_dry_run_binary_request_expects_valid_inputs_and_all_fields( delivery_report="full", send_at=send_at, expire_at=expire_at, - callback_url="https://capybara.com/callback", + event_destination_target="https://capybara.com/callback", client_reference="binary-ref", feedback_enabled=False, from_ton=0, @@ -203,7 +203,7 @@ def test_dry_run_binary_request_expects_valid_inputs_and_all_fields( assert request.delivery_report == "full" assert request.send_at == send_at assert request.expire_at == expire_at - assert request.callback_url == "https://capybara.com/callback" + assert request.event_destination_target == "https://capybara.com/callback" assert request.client_reference == "binary-ref" assert request.feedback_enabled is False assert request.from_ton == 0 @@ -256,7 +256,7 @@ def test_dry_run_media_request_expects_valid_inputs_and_all_fields( delivery_report="summary", send_at=send_at, expire_at=expire_at, - callback_url="https://capybara.com/callback", + event_destination_target="https://capybara.com/callback", client_reference="media-ref", feedback_enabled=True, ) @@ -266,7 +266,7 @@ def test_dry_run_media_request_expects_valid_inputs_and_all_fields( assert request.delivery_report == "summary" assert request.send_at == send_at assert request.expire_at == expire_at - assert request.callback_url == "https://capybara.com/callback" + assert request.event_destination_target == "https://capybara.com/callback" assert request.client_reference == "media-ref" assert request.feedback_enabled is True diff --git a/tests/unit/domains/sms/v1/models/internal/test_replace_binary_request_model.py b/tests/unit/domains/sms/v1/models/internal/test_replace_binary_request_model.py index 7e632deb..10eded9c 100644 --- a/tests/unit/domains/sms/v1/models/internal/test_replace_binary_request_model.py +++ b/tests/unit/domains/sms/v1/models/internal/test_replace_binary_request_model.py @@ -39,7 +39,7 @@ def test_replace_binary_request_expects_valid_inputs_and_all_fields( delivery_report="summary", send_at=send_at, expire_at=expire_at, - callback_url="https://capybara.com/callback", + event_destination_target="https://capybara.com/callback", client_reference="test-ref", feedback_enabled=True, from_ton=1, @@ -49,7 +49,7 @@ def test_replace_binary_request_expects_valid_inputs_and_all_fields( assert request.delivery_report == "summary" assert request.send_at == send_at assert request.expire_at == expire_at - assert request.callback_url == "https://capybara.com/callback" + assert request.event_destination_target == "https://capybara.com/callback" assert request.client_reference == "test-ref" assert request.feedback_enabled is True assert request.from_ton == 1 diff --git a/tests/unit/domains/sms/v1/models/internal/test_replace_media_request_model.py b/tests/unit/domains/sms/v1/models/internal/test_replace_media_request_model.py index 49b3b9a5..6b8b30e7 100644 --- a/tests/unit/domains/sms/v1/models/internal/test_replace_media_request_model.py +++ b/tests/unit/domains/sms/v1/models/internal/test_replace_media_request_model.py @@ -49,7 +49,7 @@ def test_replace_media_request_expects_valid_inputs_and_all_fields( delivery_report="full", send_at=send_at, expire_at=expire_at, - callback_url="https://capybara.com/webhook", + event_destination_target="https://capybara.com/webhook", client_reference="capybara-media-batch-123", feedback_enabled=True, strict_validation=True, @@ -62,7 +62,7 @@ def test_replace_media_request_expects_valid_inputs_and_all_fields( assert request.delivery_report == "full" assert request.send_at == send_at assert request.expire_at == expire_at - assert request.callback_url == "https://capybara.com/webhook" + assert request.event_destination_target == "https://capybara.com/webhook" assert request.client_reference == "capybara-media-batch-123" assert request.feedback_enabled is True assert request.strict_validation is True diff --git a/tests/unit/domains/sms/v1/models/internal/test_update_binary_request_with_batch_id_model.py b/tests/unit/domains/sms/v1/models/internal/test_update_binary_request_with_batch_id_model.py index 5587ee7b..90d95c38 100644 --- a/tests/unit/domains/sms/v1/models/internal/test_update_binary_request_with_batch_id_model.py +++ b/tests/unit/domains/sms/v1/models/internal/test_update_binary_request_with_batch_id_model.py @@ -37,7 +37,7 @@ def test_update_binary_request_expects_valid_inputs_and_all_fields( delivery_report="full", send_at=send_at, expire_at=expire_at, - callback_url="https://capybara.com/binary-callback", + event_destination_target="https://capybara.com/binary-callback", client_reference="binary-update-456", feedback_enabled=True, from_ton=3, @@ -52,7 +52,7 @@ def test_update_binary_request_expects_valid_inputs_and_all_fields( assert request.delivery_report == "full" assert request.send_at == send_at assert request.expire_at == expire_at - assert request.callback_url == "https://capybara.com/binary-callback" + assert request.event_destination_target == "https://capybara.com/binary-callback" assert request.client_reference == "binary-update-456" assert request.feedback_enabled is True assert request.from_ton == 3 diff --git a/tests/unit/domains/sms/v1/models/internal/test_update_media_request_with_batch_id_model.py b/tests/unit/domains/sms/v1/models/internal/test_update_media_request_with_batch_id_model.py index fe6b3700..06dda0de 100644 --- a/tests/unit/domains/sms/v1/models/internal/test_update_media_request_with_batch_id_model.py +++ b/tests/unit/domains/sms/v1/models/internal/test_update_media_request_with_batch_id_model.py @@ -46,7 +46,7 @@ def test_update_media_request_expects_valid_inputs_and_all_fields( delivery_report="none", send_at=send_at, expire_at=expire_at, - callback_url="https://capybara.com/media-callback", + event_destination_target="https://capybara.com/media-callback", client_reference="media-update-789", feedback_enabled=True, strict_validation=True, @@ -62,7 +62,7 @@ def test_update_media_request_expects_valid_inputs_and_all_fields( assert request.delivery_report == "none" assert request.send_at == send_at assert request.expire_at == expire_at - assert request.callback_url == "https://capybara.com/media-callback" + assert request.event_destination_target == "https://capybara.com/media-callback" assert request.client_reference == "media-update-789" assert request.feedback_enabled is True assert request.strict_validation is True diff --git a/tests/unit/domains/sms/v1/models/internal/test_update_text_request_with_batch_id_model.py b/tests/unit/domains/sms/v1/models/internal/test_update_text_request_with_batch_id_model.py index 8737571c..23df2edb 100644 --- a/tests/unit/domains/sms/v1/models/internal/test_update_text_request_with_batch_id_model.py +++ b/tests/unit/domains/sms/v1/models/internal/test_update_text_request_with_batch_id_model.py @@ -35,7 +35,7 @@ def test_update_text_request_expects_valid_inputs_and_all_fields( delivery_report="summary", send_at=send_at, expire_at=expire_at, - callback_url="https://capybara.com/webhook", + event_destination_target="https://capybara.com/webhook", client_reference="update-ref-123", feedback_enabled=True, flash_message=True, @@ -54,7 +54,7 @@ def test_update_text_request_expects_valid_inputs_and_all_fields( assert request.delivery_report == "summary" assert request.send_at == send_at assert request.expire_at == expire_at - assert request.callback_url == "https://capybara.com/webhook" + assert request.event_destination_target == "https://capybara.com/webhook" assert request.client_reference == "update-ref-123" assert request.feedback_enabled is True assert request.flash_message is True diff --git a/tests/unit/domains/sms/v1/test_batches.py b/tests/unit/domains/sms/v1/test_batches.py index 23064769..a95c3d64 100644 --- a/tests/unit/domains/sms/v1/test_batches.py +++ b/tests/unit/domains/sms/v1/test_batches.py @@ -193,7 +193,7 @@ def test_batches_send_sms_expects_correct_request( delivery_report="full", send_at=datetime(2024, 6, 6, 9, 25, 0, tzinfo=timezone.utc), expire_at=datetime(2024, 6, 10, 9, 25, 0, tzinfo=timezone.utc), - callback_url="https://example.com/callback", + event_destination_target="https://example.com/callback", client_reference="test-ref", feedback_enabled=True, flash_message=False, @@ -262,7 +262,7 @@ def test_batches_send_binary_expects_correct_request( delivery_report="summary", send_at=datetime(2024, 6, 6, 9, 25, 0, tzinfo=timezone.utc), expire_at=datetime(2024, 6, 10, 9, 25, 0, tzinfo=timezone.utc), - callback_url="https://example.com/callback", + event_destination_target="https://example.com/callback", client_reference="test-ref", feedback_enabled=True, from_ton=1, @@ -330,7 +330,7 @@ def test_batches_send_mms_expects_correct_request( delivery_report="full", send_at=datetime(2024, 6, 6, 9, 25, 0, tzinfo=timezone.utc), expire_at=datetime(2024, 6, 10, 9, 25, 0, tzinfo=timezone.utc), - callback_url="https://example.com/callback", + event_destination_target="https://example.com/callback", client_reference="test-ref", feedback_enabled=True, strict_validation=True, From 45a990850a32d74ce5a12a8bd06301aca2a1bfac Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Wed, 18 Mar 2026 17:18:35 +0100 Subject: [PATCH 02/10] rename folder --- examples/{webhooks => sinch_events}/.env.example | 0 examples/{webhooks => sinch_events}/README.md | 6 +++--- .../conversation_api/__init__.py | 0 .../conversation_api/controller.py | 2 +- .../conversation_api/server_business_logic.py | 0 .../{webhooks => sinch_events}/numbers_api/__init__.py | 0 .../numbers_api/controller.py | 2 +- .../numbers_api/server_business_logic.py | 0 examples/{webhooks => sinch_events}/pyproject.toml | 0 examples/{webhooks => sinch_events}/server.py | 10 +++++----- .../{webhooks => sinch_events}/sinch_client_helper.py | 4 ++-- .../{webhooks => sinch_events}/sms_api/__init__.py | 0 .../{webhooks => sinch_events}/sms_api/controller.py | 2 +- .../sms_api/server_business_logic.py | 0 14 files changed, 13 insertions(+), 13 deletions(-) rename examples/{webhooks => sinch_events}/.env.example (100%) rename examples/{webhooks => sinch_events}/README.md (96%) rename examples/{webhooks => sinch_events}/conversation_api/__init__.py (100%) rename examples/{webhooks => sinch_events}/conversation_api/controller.py (92%) rename examples/{webhooks => sinch_events}/conversation_api/server_business_logic.py (100%) rename examples/{webhooks => sinch_events}/numbers_api/__init__.py (100%) rename examples/{webhooks => sinch_events}/numbers_api/controller.py (92%) rename examples/{webhooks => sinch_events}/numbers_api/server_business_logic.py (100%) rename examples/{webhooks => sinch_events}/pyproject.toml (100%) rename examples/{webhooks => sinch_events}/server.py (79%) rename examples/{webhooks => sinch_events}/sinch_client_helper.py (81%) rename examples/{webhooks => sinch_events}/sms_api/__init__.py (100%) rename examples/{webhooks => sinch_events}/sms_api/controller.py (95%) rename examples/{webhooks => sinch_events}/sms_api/server_business_logic.py (100%) diff --git a/examples/webhooks/.env.example b/examples/sinch_events/.env.example similarity index 100% rename from examples/webhooks/.env.example rename to examples/sinch_events/.env.example diff --git a/examples/webhooks/README.md b/examples/sinch_events/README.md similarity index 96% rename from examples/webhooks/README.md rename to examples/sinch_events/README.md index c2f88a45..96036d01 100644 --- a/examples/webhooks/README.md +++ b/examples/sinch_events/README.md @@ -21,7 +21,7 @@ This directory contains both the webhook handlers and the server application (`s ## Configuration 1. **Environment Variables**: - Rename [.env.example](.env.example) to `.env` in this directory (`examples/webhooks/`), then add your credentials from the Sinch dashboard under the Access Keys section. + Rename [.env.example](.env.example) to `.env` in this directory (`examples/sinch_events/`), then add your credentials from the Sinch dashboard under the Access Keys section. - Server Port: Define the port your server will listen to on (default: 3001): @@ -49,9 +49,9 @@ This directory contains both the webhook handlers and the server application (`s ### Running the server application -1. Navigate to the webhooks' directory: +1. Navigate to the sinch_events directory: ``` - cd examples/webhooks + cd examples/sinch_events ``` 2. Install the project dependencies: diff --git a/examples/webhooks/conversation_api/__init__.py b/examples/sinch_events/conversation_api/__init__.py similarity index 100% rename from examples/webhooks/conversation_api/__init__.py rename to examples/sinch_events/conversation_api/__init__.py diff --git a/examples/webhooks/conversation_api/controller.py b/examples/sinch_events/conversation_api/controller.py similarity index 92% rename from examples/webhooks/conversation_api/controller.py rename to examples/sinch_events/conversation_api/controller.py index 74e47888..03bdbae9 100644 --- a/examples/webhooks/conversation_api/controller.py +++ b/examples/sinch_events/conversation_api/controller.py @@ -1,5 +1,5 @@ from flask import request, Response -from webhooks.conversation_api.server_business_logic import handle_conversation_event +from sinch_events.conversation_api.server_business_logic import handle_conversation_event class ConversationController: diff --git a/examples/webhooks/conversation_api/server_business_logic.py b/examples/sinch_events/conversation_api/server_business_logic.py similarity index 100% rename from examples/webhooks/conversation_api/server_business_logic.py rename to examples/sinch_events/conversation_api/server_business_logic.py diff --git a/examples/webhooks/numbers_api/__init__.py b/examples/sinch_events/numbers_api/__init__.py similarity index 100% rename from examples/webhooks/numbers_api/__init__.py rename to examples/sinch_events/numbers_api/__init__.py diff --git a/examples/webhooks/numbers_api/controller.py b/examples/sinch_events/numbers_api/controller.py similarity index 92% rename from examples/webhooks/numbers_api/controller.py rename to examples/sinch_events/numbers_api/controller.py index 2380eda4..e4f41c01 100644 --- a/examples/webhooks/numbers_api/controller.py +++ b/examples/sinch_events/numbers_api/controller.py @@ -1,5 +1,5 @@ from flask import request, Response -from webhooks.numbers_api.server_business_logic import handle_numbers_event +from sinch_events.numbers_api.server_business_logic import handle_numbers_event class NumbersController: diff --git a/examples/webhooks/numbers_api/server_business_logic.py b/examples/sinch_events/numbers_api/server_business_logic.py similarity index 100% rename from examples/webhooks/numbers_api/server_business_logic.py rename to examples/sinch_events/numbers_api/server_business_logic.py diff --git a/examples/webhooks/pyproject.toml b/examples/sinch_events/pyproject.toml similarity index 100% rename from examples/webhooks/pyproject.toml rename to examples/sinch_events/pyproject.toml diff --git a/examples/webhooks/server.py b/examples/sinch_events/server.py similarity index 79% rename from examples/webhooks/server.py rename to examples/sinch_events/server.py index 98caa89a..15ca86b0 100644 --- a/examples/webhooks/server.py +++ b/examples/sinch_events/server.py @@ -2,16 +2,16 @@ import sys from pathlib import Path -# Add examples directory to Python path to allow importing webhooks +# Add examples directory to Python path to allow importing sinch_events examples_dir = Path(__file__).resolve().parent.parent if str(examples_dir) not in sys.path: sys.path.insert(0, str(examples_dir)) from flask import Flask, request -from webhooks.numbers_api.controller import NumbersController -from webhooks.sms_api.controller import SmsController -from webhooks.conversation_api.controller import ConversationController -from webhooks.sinch_client_helper import get_sinch_client, load_config +from sinch_events.numbers_api.controller import NumbersController +from sinch_events.sms_api.controller import SmsController +from sinch_events.conversation_api.controller import ConversationController +from sinch_events.sinch_client_helper import get_sinch_client, load_config app = Flask(__name__) diff --git a/examples/webhooks/sinch_client_helper.py b/examples/sinch_events/sinch_client_helper.py similarity index 81% rename from examples/webhooks/sinch_client_helper.py rename to examples/sinch_events/sinch_client_helper.py index 109fce1c..fdc662ab 100644 --- a/examples/webhooks/sinch_client_helper.py +++ b/examples/sinch_events/sinch_client_helper.py @@ -5,7 +5,7 @@ def load_config() -> dict[str, str]: """ - Load configuration from the .env file in the webhooks directory. + Load configuration from the .env file in the sinch_events directory. Returns: dict[str, str]: Dictionary containing configuration values @@ -15,7 +15,7 @@ def load_config() -> dict[str, str]: env_file = current_dir / '.env' if not env_file.exists(): - raise FileNotFoundError(f"Could not find .env file in webhooks directory: {env_file}") + raise FileNotFoundError(f"Could not find .env file in sinch_events directory: {env_file}") config_dict = dotenv_values(env_file) diff --git a/examples/webhooks/sms_api/__init__.py b/examples/sinch_events/sms_api/__init__.py similarity index 100% rename from examples/webhooks/sms_api/__init__.py rename to examples/sinch_events/sms_api/__init__.py diff --git a/examples/webhooks/sms_api/controller.py b/examples/sinch_events/sms_api/controller.py similarity index 95% rename from examples/webhooks/sms_api/controller.py rename to examples/sinch_events/sms_api/controller.py index f3418e26..64a9e09d 100644 --- a/examples/webhooks/sms_api/controller.py +++ b/examples/sinch_events/sms_api/controller.py @@ -1,5 +1,5 @@ from flask import request, Response -from webhooks.sms_api.server_business_logic import ( +from sinch_events.sms_api.server_business_logic import ( handle_sms_event, ) diff --git a/examples/webhooks/sms_api/server_business_logic.py b/examples/sinch_events/sms_api/server_business_logic.py similarity index 100% rename from examples/webhooks/sms_api/server_business_logic.py rename to examples/sinch_events/sms_api/server_business_logic.py From ff1d7500da92bce90ccd6a4f11fbca0616ead17b Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Wed, 18 Mar 2026 18:38:44 +0100 Subject: [PATCH 03/10] rename to sinch_events --- examples/sinch_events/sms_api/controller.py | 6 +++--- .../sms_api/server_business_logic.py | 8 +++++--- .../sms/sinch_events/v1/events/__init__.py | 16 ++++++++-------- .../sinch_events/v1/events/sms_sinch_event.py | 18 +++++++++--------- .../sms/sinch_events/v1/internal/__init__.py | 6 +++--- .../{webhook_event.py => sinch_event.py} | 2 +- .../sms/sinch_events/v1/sms_sinch_event.py | 16 ++++++++-------- tests/e2e/sms/features/steps/webhooks.steps.py | 4 ++-- 8 files changed, 39 insertions(+), 37 deletions(-) rename sinch/domains/sms/sinch_events/v1/internal/{webhook_event.py => sinch_event.py} (66%) diff --git a/examples/sinch_events/sms_api/controller.py b/examples/sinch_events/sms_api/controller.py index 64a9e09d..5b345332 100644 --- a/examples/sinch_events/sms_api/controller.py +++ b/examples/sinch_events/sms_api/controller.py @@ -14,14 +14,14 @@ def sms_event(self): headers = dict(request.headers) raw_body = request.raw_body if request.raw_body else b"" - webhooks_service = self.sinch_client.sms.sinch_events(self.webhooks_secret) + sinch_events_service = self.sinch_client.sms.sinch_events(self.webhooks_secret) # Signature headers may be absent unless your account manager enables them # (see README: Configuration -> Controller Settings -> SMS controller); # leave auth disabled here unless SMS callbacks are configured. ensure_valid_authentication = False if ensure_valid_authentication: - valid_auth = webhooks_service.validate_authentication_header( + valid_auth = sinch_events_service.validate_authentication_header( headers=headers, json_payload=raw_body, ) @@ -29,7 +29,7 @@ def sms_event(self): if not valid_auth: return Response(status=401) - event = webhooks_service.parse_event(raw_body, headers) + event = sinch_events_service.parse_event(raw_body, headers) handle_sms_event(sms_event=event, logger=self.logger) diff --git a/examples/sinch_events/sms_api/server_business_logic.py b/examples/sinch_events/sms_api/server_business_logic.py index d13e6fc2..7061394d 100644 --- a/examples/sinch_events/sms_api/server_business_logic.py +++ b/examples/sinch_events/sms_api/server_business_logic.py @@ -1,11 +1,13 @@ -from sinch.domains.sms.sinch_events.v1.events.sms_sinch_event import IncomingSMSWebhookEvent +from sinch.domains.sms.sinch_events.v1.events.sms_sinch_event import ( + IncomingSMSSinchEvent, +) -def handle_sms_event(sms_event: IncomingSMSWebhookEvent, logger): +def handle_sms_event(sms_event: IncomingSMSSinchEvent, logger): """ This method handles an SMS event. Args: - sms_event (IncomingSMSWebhookEvent): The SMS event data. + sms_event (IncomingSMSSinchEvent): The SMS event data. logger (logging.Logger, optional): Logger instance for logging. Defaults to None. """ logger.info(f'Handling SMS event:\n{sms_event.model_dump_json(indent=2)}') diff --git a/sinch/domains/sms/sinch_events/v1/events/__init__.py b/sinch/domains/sms/sinch_events/v1/events/__init__.py index 66598629..00aba842 100644 --- a/sinch/domains/sms/sinch_events/v1/events/__init__.py +++ b/sinch/domains/sms/sinch_events/v1/events/__init__.py @@ -1,17 +1,17 @@ from sinch.domains.sms.sinch_events.v1.events.sms_sinch_event import ( - IncomingSMSWebhookEvent, - MOTextWebhookEvent, - MOBinaryWebhookEvent, - MOMediaWebhookEvent, + IncomingSMSSinchEvent, + MOTextSinchEvent, + MOBinarySinchEvent, + MOMediaSinchEvent, MediaBody, MediaItem, ) __all__ = [ - "IncomingSMSWebhookEvent", - "MOTextWebhookEvent", - "MOBinaryWebhookEvent", - "MOMediaWebhookEvent", + "IncomingSMSSinchEvent", + "MOTextSinchEvent", + "MOBinarySinchEvent", + "MOMediaSinchEvent", "MediaBody", "MediaItem", ] diff --git a/sinch/domains/sms/sinch_events/v1/events/sms_sinch_event.py b/sinch/domains/sms/sinch_events/v1/events/sms_sinch_event.py index 330738a1..a6aeed05 100644 --- a/sinch/domains/sms/sinch_events/v1/events/sms_sinch_event.py +++ b/sinch/domains/sms/sinch_events/v1/events/sms_sinch_event.py @@ -1,10 +1,10 @@ from datetime import datetime from typing import Optional, Union, Literal, Annotated from pydantic import Field, StrictStr, StrictInt, conlist -from sinch.domains.sms.sinch_events.v1.internal import WebhookEvent +from sinch.domains.sms.sinch_events.v1.internal import SinchEvent -class MediaItem(WebhookEvent): +class MediaItem(SinchEvent): url: StrictStr = Field(..., description="URL to the media file") content_type: StrictStr = Field( ..., description="Content type of the media file" @@ -15,7 +15,7 @@ class MediaItem(WebhookEvent): code: StrictInt = Field(..., description="Status code") -class MediaBody(WebhookEvent): +class MediaBody(SinchEvent): subject: Optional[StrictStr] = Field( default=None, description="The subject text" ) @@ -25,7 +25,7 @@ class MediaBody(WebhookEvent): media: conlist(MediaItem) = Field(..., description="Array of media items") -class BaseIncomingSMSWebhookEvent(WebhookEvent): +class BaseIncomingSMSWebhookEvent(SinchEvent): from_: StrictStr = Field( ..., alias="from", @@ -54,7 +54,7 @@ class BaseIncomingSMSWebhookEvent(WebhookEvent): ) -class MOTextWebhookEvent(BaseIncomingSMSWebhookEvent): +class MOTextSinchEvent(BaseIncomingSMSWebhookEvent): body: StrictStr = Field( ..., description="The incoming message body. Maximum 2000 characters.", @@ -64,7 +64,7 @@ class MOTextWebhookEvent(BaseIncomingSMSWebhookEvent): ) -class MOBinaryWebhookEvent(BaseIncomingSMSWebhookEvent): +class MOBinarySinchEvent(BaseIncomingSMSWebhookEvent): body: StrictStr = Field( ..., description="The incoming message body (Base64 encoded)." ) @@ -76,7 +76,7 @@ class MOBinaryWebhookEvent(BaseIncomingSMSWebhookEvent): ) -class MOMediaWebhookEvent(BaseIncomingSMSWebhookEvent): +class MOMediaSinchEvent(BaseIncomingSMSWebhookEvent): body: MediaBody = Field( ..., description="The media message body containing subject, message, and media items.", @@ -88,10 +88,10 @@ class MOMediaWebhookEvent(BaseIncomingSMSWebhookEvent): # Union type for isinstance checks _IncomingSMSWebhookEventUnion = Union[ - MOTextWebhookEvent, MOBinaryWebhookEvent, MOMediaWebhookEvent + MOTextSinchEvent, MOBinarySinchEvent, MOMediaSinchEvent ] # Discriminated union for validation -IncomingSMSWebhookEvent = Annotated[ +IncomingSMSSinchEvent = Annotated[ _IncomingSMSWebhookEventUnion, Field(discriminator="type") ] diff --git a/sinch/domains/sms/sinch_events/v1/internal/__init__.py b/sinch/domains/sms/sinch_events/v1/internal/__init__.py index b1bd904d..43b3a8dd 100644 --- a/sinch/domains/sms/sinch_events/v1/internal/__init__.py +++ b/sinch/domains/sms/sinch_events/v1/internal/__init__.py @@ -1,5 +1,5 @@ -from sinch.domains.sms.sinch_events.v1.internal.webhook_event import ( - WebhookEvent, +from sinch.domains.sms.sinch_events.v1.internal.sinch_event import ( + SinchEvent, ) -__all__ = ["WebhookEvent"] +__all__ = ["SinchEvent"] diff --git a/sinch/domains/sms/sinch_events/v1/internal/webhook_event.py b/sinch/domains/sms/sinch_events/v1/internal/sinch_event.py similarity index 66% rename from sinch/domains/sms/sinch_events/v1/internal/webhook_event.py rename to sinch/domains/sms/sinch_events/v1/internal/sinch_event.py index 0d2857ed..184012f9 100644 --- a/sinch/domains/sms/sinch_events/v1/internal/webhook_event.py +++ b/sinch/domains/sms/sinch_events/v1/internal/sinch_event.py @@ -3,5 +3,5 @@ ) -class WebhookEvent(BaseModelConfigurationResponse): +class SinchEvent(BaseModelConfigurationResponse): pass diff --git a/sinch/domains/sms/sinch_events/v1/sms_sinch_event.py b/sinch/domains/sms/sinch_events/v1/sms_sinch_event.py index 8a4d5dbf..8f0fd37d 100644 --- a/sinch/domains/sms/sinch_events/v1/sms_sinch_event.py +++ b/sinch/domains/sms/sinch_events/v1/sms_sinch_event.py @@ -10,10 +10,10 @@ normalize_iso_timestamp, ) from sinch.domains.sms.sinch_events.v1.events import ( - IncomingSMSWebhookEvent, - MOTextWebhookEvent, - MOBinaryWebhookEvent, - MOMediaWebhookEvent, + IncomingSMSSinchEvent, + MOTextSinchEvent, + MOBinarySinchEvent, + MOMediaSinchEvent, ) from sinch.domains.sms.models.v1.response import ( BatchDeliveryReport, @@ -24,9 +24,9 @@ SmsCallback = Union[ BatchDeliveryReport, RecipientDeliveryReport, - MOTextWebhookEvent, - MOBinaryWebhookEvent, - MOMediaWebhookEvent, + MOTextSinchEvent, + MOBinarySinchEvent, + MOMediaSinchEvent, ] @@ -114,7 +114,7 @@ def parse_event( event_body["sent_at"] ) - adapter = TypeAdapter(IncomingSMSWebhookEvent) + adapter = TypeAdapter(IncomingSMSSinchEvent) return adapter.validate_python(event_body) raise ValueError(f"Unknown SMS event type: {event_type}") diff --git a/tests/e2e/sms/features/steps/webhooks.steps.py b/tests/e2e/sms/features/steps/webhooks.steps.py index d153720f..361c9b5c 100644 --- a/tests/e2e/sms/features/steps/webhooks.steps.py +++ b/tests/e2e/sms/features/steps/webhooks.steps.py @@ -3,7 +3,7 @@ from behave import given, when, then from sinch.domains.sms.sinch_events.v1.sms_sinch_event import SmsSinchEvent from sinch.domains.sms.sinch_events.v1.events import ( - MOTextWebhookEvent, + MOTextSinchEvent, ) from sinch.domains.sms.models.v1.response import ( BatchDeliveryReport, @@ -36,7 +36,7 @@ def step_check_valid_signature(context, event_type, status=None): @then('the SMS event describes an "incoming SMS" event') def step_check_incoming_sms_event(context): - incoming_sms_event: MOTextWebhookEvent = context.event + incoming_sms_event: MOTextSinchEvent = context.event assert incoming_sms_event.id == '01W4FFL35P4NC4K35SMSBATCH8' assert incoming_sms_event.from_ == '12015555555' assert incoming_sms_event.to == '12017777777' From 1c1ccbf68b0e364171f04a9df185b1721a20142c Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Thu, 19 Mar 2026 10:32:06 +0100 Subject: [PATCH 04/10] update README --- examples/sinch_events/README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/sinch_events/README.md b/examples/sinch_events/README.md index 7b406a19..ab73e213 100644 --- a/examples/sinch_events/README.md +++ b/examples/sinch_events/README.md @@ -1,14 +1,14 @@ -# Webhook Handlers for Sinch Python SDK +# Sinch Events Handlers for Sinch Python SDK This directory contains a server application built with [Sinch Python SDK](https://github.com/sinch/sinch-sdk-python) to process incoming webhooks from Sinch services. -The webhook handlers are organized by service: -- **SMS**: Handlers for SMS webhook events (`sms_api/`) -- **Numbers**: Handlers for Numbers API webhook events (`numbers_api/`) -- **Conversation**: Handlers for Conversation API webhook events (`conversation_api/`) +The Sinch Events Handlers are organized by service: +- **SMS**: Handlers for SMS events (`sms_api/`) +- **Numbers**: Handlers for Numbers API events (`numbers_api/`) +- **Conversation**: Handlers for Conversation API events (`conversation_api/`) -This directory contains both the webhook handlers and the server application (`server.py`) that uses them. +This directory contains both the Event handlers and the server application (`server.py`) that uses them. ## Requirements @@ -49,7 +49,7 @@ This directory contains both the webhook handlers and the server application (`s ### Running the server application -1. Navigate to the webhooks' directory: +1. Navigate to the examples events directory: ``` cd examples/sinch_events ``` @@ -96,18 +96,18 @@ ngrok ... Forwarding https://adbd-79-148-170-158.ngrok-free.app -> http://localhost:3001 ``` -Use the `https` forwarding URL in your callback configuration. For example: +Use the `https` forwarding URL in your event destination configuration. For example: - Numbers: https://adbd-79-148-170-158.ngrok-free.app/NumbersEvent - SMS: https://adbd-79-148-170-158.ngrok-free.app/SmsEvent - Conversation: https://adbd-79-148-170-158.ngrok-free.app/ConversationEvent -Use this value to configure the callback URLs: -- **Numbers**: Set the `callback_url` parameter when renting or updating a number via the SDK (e.g., `available_numbers_apis` rent/update flow: [rent](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/numbers/api/v1/available_numbers_apis.py#L69), [update](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/numbers/api/v1/available_numbers_apis.py#L89)); you can also update active numbers via `active_numbers_apis` ([example](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/numbers/api/v1/active_numbers_apis.py#L64)). -- **SMS**: Set the `callback_url` parameter when configuring your SMS service plan via the SDK (see `batches_apis` examples: [send/dry-run callbacks](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/sms/api/v1/batches_apis.py#L146), [update/replace callbacks](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/sms/api/v1/batches_apis.py#L491)); you can also set it directly via the SMS API. +Use this value to configure the Sinch Events URLs: +- **Numbers**: Set the `event_destination_target` parameter when renting or updating a number via the SDK (e.g., `available_numbers_apis` rent/update flow: [rent](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/numbers/api/v1/available_numbers_apis.py#L69), [update](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/numbers/api/v1/available_numbers_apis.py#L89)); you can also update active numbers via `active_numbers_apis` ([example](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/numbers/api/v1/active_numbers_apis.py#L64)). +- **SMS**: Set the `event_destination_target` parameter when configuring your SMS service plan via the SDK (see `batches_apis` examples: [send/dry-run callbacks](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/sms/api/v1/batches_apis.py#L146), [update/replace callbacks](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/sms/api/v1/batches_apis.py#L491)); you can also set it directly via the SMS API. - **Conversation**: Set the `callback_url` parameter when sending a message via the SDK (see `messages_apis` example: [send_text_message](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/conversation/api/v1/messages_apis.py#L420)). -You can also set these callback URLs in the Sinch dashboard; the API parameters above override the default values configured there. +You can also set these Sinch Events URLs in the Sinch dashboard; the API parameters above override the default values configured there. -> **Note**: If you have set a webhook secret (e.g., `SMS_WEBHOOKS_SECRET`), the webhook URL must be configured in the Sinch dashboard +> **Note**: If you have set a webhook secret (e.g., `SMS_WEBHOOKS_SECRET`), the Sinch Event URL must be configured in the Sinch dashboard > and cannot be overridden via API parameters. The webhook secret is used to validate incoming webhook requests, > and the URL associated with it must be set in the dashboard. From e24e982a3ff164ee96955ec2dce7db3186727b05 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Thu, 19 Mar 2026 17:57:33 +0100 Subject: [PATCH 05/10] code review --- examples/sinch_events/README.md | 6 +++--- examples/sinch_events/server.py | 4 ++-- examples/sinch_events/sms_api/controller.py | 6 +++--- .../sinch_events/v1/events/sms_sinch_event.py | 12 ++++++------ .../sms/sinch_events/v1/sms_sinch_event.py | 8 ++++---- sinch/domains/sms/sms.py | 10 +++++----- tests/e2e/sms/features/steps/webhooks.steps.py | 16 ++++++++-------- 7 files changed, 31 insertions(+), 31 deletions(-) diff --git a/examples/sinch_events/README.md b/examples/sinch_events/README.md index ab73e213..ce9ada5b 100644 --- a/examples/sinch_events/README.md +++ b/examples/sinch_events/README.md @@ -34,7 +34,7 @@ This directory contains both the Event handlers and the server application (`ser ``` NUMBERS_WEBHOOKS_SECRET=Your Sinch Numbers Webhook Secret ``` - - SMS controller: To configure the `sms` webhooks secret, contact your account manager to enable authentication for SMS callbacks. For more details, refer to + - SMS controller: To configure the `sms` Sinch Event secret, contact your account manager to enable authentication for SMS callbacks. For more details, refer to [SMS API](https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/#tag/Webhooks/section/Callbacks), ``` @@ -108,6 +108,6 @@ Use this value to configure the Sinch Events URLs: You can also set these Sinch Events URLs in the Sinch dashboard; the API parameters above override the default values configured there. -> **Note**: If you have set a webhook secret (e.g., `SMS_WEBHOOKS_SECRET`), the Sinch Event URL must be configured in the Sinch dashboard -> and cannot be overridden via API parameters. The webhook secret is used to validate incoming webhook requests, +> **Note**: If you have set a Sinch Event secret (e.g., `SMS_WEBHOOKS_SECRET`), the Sinch Event URL must be configured in the Sinch dashboard +> and cannot be overridden via API parameters. The Sinch Event secret is used to validate incoming webhook requests, > and the URL associated with it must be set in the dashboard. diff --git a/examples/sinch_events/server.py b/examples/sinch_events/server.py index 15ca86b0..67876863 100644 --- a/examples/sinch_events/server.py +++ b/examples/sinch_events/server.py @@ -18,7 +18,7 @@ config = load_config() port = int(config.get('SERVER_PORT') or 3001) numbers_webhooks_secret = config.get('NUMBERS_WEBHOOKS_SECRET') -sms_webhooks_secret = config.get('SMS_WEBHOOKS_SECRET') +sms_sinch_event_secret = config.get('SMS_WEBHOOKS_SECRET') conversation_webhooks_secret = config.get('CONVERSATION_WEBHOOKS_SECRET') sinch_client = get_sinch_client(config) @@ -27,7 +27,7 @@ sinch_client.configuration.logger.setLevel(logging.INFO) numbers_controller = NumbersController(sinch_client, numbers_webhooks_secret) -sms_controller = SmsController(sinch_client, sms_webhooks_secret) +sms_controller = SmsController(sinch_client, sms_sinch_event_secret) conversation_controller = ConversationController(sinch_client, conversation_webhooks_secret or '') diff --git a/examples/sinch_events/sms_api/controller.py b/examples/sinch_events/sms_api/controller.py index 5b345332..2ebfda6c 100644 --- a/examples/sinch_events/sms_api/controller.py +++ b/examples/sinch_events/sms_api/controller.py @@ -5,16 +5,16 @@ class SmsController: - def __init__(self, sinch_client, webhooks_secret): + def __init__(self, sinch_client, sinch_event_secret): self.sinch_client = sinch_client - self.webhooks_secret = webhooks_secret + self.sinch_event_secret = sinch_event_secret self.logger = self.sinch_client.configuration.logger def sms_event(self): headers = dict(request.headers) raw_body = request.raw_body if request.raw_body else b"" - sinch_events_service = self.sinch_client.sms.sinch_events(self.webhooks_secret) + sinch_events_service = self.sinch_client.sms.sinch_events(self.sinch_event_secret) # Signature headers may be absent unless your account manager enables them # (see README: Configuration -> Controller Settings -> SMS controller); diff --git a/sinch/domains/sms/sinch_events/v1/events/sms_sinch_event.py b/sinch/domains/sms/sinch_events/v1/events/sms_sinch_event.py index a6aeed05..fc87e608 100644 --- a/sinch/domains/sms/sinch_events/v1/events/sms_sinch_event.py +++ b/sinch/domains/sms/sinch_events/v1/events/sms_sinch_event.py @@ -25,7 +25,7 @@ class MediaBody(SinchEvent): media: conlist(MediaItem) = Field(..., description="Array of media items") -class BaseIncomingSMSWebhookEvent(SinchEvent): +class BaseIncomingSMSSinchEvent(SinchEvent): from_: StrictStr = Field( ..., alias="from", @@ -54,7 +54,7 @@ class BaseIncomingSMSWebhookEvent(SinchEvent): ) -class MOTextSinchEvent(BaseIncomingSMSWebhookEvent): +class MOTextSinchEvent(BaseIncomingSMSSinchEvent): body: StrictStr = Field( ..., description="The incoming message body. Maximum 2000 characters.", @@ -64,7 +64,7 @@ class MOTextSinchEvent(BaseIncomingSMSWebhookEvent): ) -class MOBinarySinchEvent(BaseIncomingSMSWebhookEvent): +class MOBinarySinchEvent(BaseIncomingSMSSinchEvent): body: StrictStr = Field( ..., description="The incoming message body (Base64 encoded)." ) @@ -76,7 +76,7 @@ class MOBinarySinchEvent(BaseIncomingSMSWebhookEvent): ) -class MOMediaSinchEvent(BaseIncomingSMSWebhookEvent): +class MOMediaSinchEvent(BaseIncomingSMSSinchEvent): body: MediaBody = Field( ..., description="The media message body containing subject, message, and media items.", @@ -87,11 +87,11 @@ class MOMediaSinchEvent(BaseIncomingSMSWebhookEvent): # Union type for isinstance checks -_IncomingSMSWebhookEventUnion = Union[ +_IncomingSMSSinchEventUnion = Union[ MOTextSinchEvent, MOBinarySinchEvent, MOMediaSinchEvent ] # Discriminated union for validation IncomingSMSSinchEvent = Annotated[ - _IncomingSMSWebhookEventUnion, Field(discriminator="type") + _IncomingSMSSinchEventUnion, Field(discriminator="type") ] diff --git a/sinch/domains/sms/sinch_events/v1/sms_sinch_event.py b/sinch/domains/sms/sinch_events/v1/sms_sinch_event.py index 8f0fd37d..8f8daee9 100644 --- a/sinch/domains/sms/sinch_events/v1/sms_sinch_event.py +++ b/sinch/domains/sms/sinch_events/v1/sms_sinch_event.py @@ -21,7 +21,7 @@ ) -SmsCallback = Union[ +SmsSinchEventPayload = Union[ BatchDeliveryReport, RecipientDeliveryReport, MOTextSinchEvent, @@ -64,7 +64,7 @@ def parse_event( self, event_body: Union[str, bytes, Dict[str, Any]], headers: Optional[Dict[str, str]] = None, - ) -> SmsCallback: + ) -> SmsSinchEventPayload: """ Parse the event payload into an SMS callback object. @@ -75,8 +75,8 @@ def parse_event( :type event_body: Union[str, bytes, Dict[str, Any]] :param headers: Request headers (used to decode charset when event_body is bytes). :type headers: Optional[Dict[str, str]] - :returns: A parsed SMS callback object. - :rtype: SmsCallback + :returns: A parsed SMS Sinch Event payload object. + :rtype: SmsSinchEventPayload :raises ValueError: If the event type is unknown or parsing fails. """ if isinstance(event_body, bytes): diff --git a/sinch/domains/sms/sms.py b/sinch/domains/sms/sms.py index 2a15ac7e..c312c2de 100644 --- a/sinch/domains/sms/sms.py +++ b/sinch/domains/sms/sms.py @@ -17,13 +17,13 @@ def __init__(self, sinch): self.batches = Batches(self._sinch) self.delivery_reports = DeliveryReports(self._sinch) - def sinch_events(self, callback_secret: str) -> SmsSinchEvent: + def sinch_events(self, sinch_event_secret: str) -> SmsSinchEvent: """ - Create an SMS Sinch Events handler with the specified callback secret. + Create an SMS Sinch Events handler with the specified Sinch Event secret. - :param callback_secret: Secret used for webhook validation. - :type callback_secret: str + :param sinch_event_secret: Secret used for Sinch Event validation. + :type sinch_event_secret: str :returns: A configured Sinch Events handler :rtype: SmsSinchEvent """ - return SmsSinchEvent(callback_secret) + return SmsSinchEvent(sinch_event_secret) diff --git a/tests/e2e/sms/features/steps/webhooks.steps.py b/tests/e2e/sms/features/steps/webhooks.steps.py index 361c9b5c..e2e04bd1 100644 --- a/tests/e2e/sms/features/steps/webhooks.steps.py +++ b/tests/e2e/sms/features/steps/webhooks.steps.py @@ -11,25 +11,25 @@ ) from tests.e2e.helpers import store_webhook_response -SINCH_SMS_CALLBACK_SECRET = 'KayakingTheSwell' +SINCH_SMS_SINCH_EVENT_SECRET = 'KayakingTheSwell' -@given('the SMS Webhooks handler is available') +@given('the SMS Sinch Events handler is available') def step_webhook_handler_is_available(context): - context.sms_webhook = SmsSinchEvent(SINCH_SMS_CALLBACK_SECRET) + context.sms_sinch_event = SmsSinchEvent(SINCH_SMS_SINCH_EVENT_SECRET) @when('I send a request to trigger an "incoming SMS" event') def step_send_incoming_sms_event(context): response = requests.get('http://localhost:3017/webhooks/sms/incoming-sms') store_webhook_response(context, response) - context.event = context.sms_webhook.parse_event(context.raw_event) + context.event = context.sms_sinch_event.parse_event(context.raw_event) @then('the header of the event "{event_type}" contains a valid signature') @then('the header of the event "{event_type}" with the status "{status}" contains a valid signature') def step_check_valid_signature(context, event_type, status=None): - assert context.sms_webhook.validate_authentication_header( + assert context.sms_sinch_event.validate_authentication_header( context.webhook_headers, context.raw_event ), 'Signature validation failed' @@ -51,7 +51,7 @@ def step_check_incoming_sms_event(context): def step_send_delivery_report_event(context): response = requests.get('http://localhost:3017/webhooks/sms/delivery-report-sms') store_webhook_response(context, response) - context.event = context.sms_webhook.parse_event(context.raw_event) + context.event = context.sms_sinch_event.parse_event(context.raw_event) @then('the SMS event describes an "SMS delivery report" event') @@ -79,7 +79,7 @@ def step_send_recipient_delivery_report_event_delivered(context): 'http://localhost:3017/webhooks/sms/recipient-delivery-report-sms-delivered' ) store_webhook_response(context, response) - context.event = context.sms_webhook.parse_event(context.raw_event) + context.event = context.sms_sinch_event.parse_event(context.raw_event) @when('I send a request to trigger an "SMS recipient delivery report" event with the status "Aborted"') @@ -88,7 +88,7 @@ def step_send_recipient_delivery_report_event_aborted(context): 'http://localhost:3017/webhooks/sms/recipient-delivery-report-sms-aborted' ) store_webhook_response(context, response) - context.event = context.sms_webhook.parse_event(context.raw_event) + context.event = context.sms_sinch_event.parse_event(context.raw_event) @then('the SMS event describes an SMS recipient delivery report event with the status "Delivered"') From 69be48170334a8394cc67a0112f38d3c943f7dc2 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Thu, 19 Mar 2026 18:15:14 +0100 Subject: [PATCH 06/10] fix ci --- tests/e2e/sms/features/steps/webhooks.steps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/sms/features/steps/webhooks.steps.py b/tests/e2e/sms/features/steps/webhooks.steps.py index e2e04bd1..99907fc4 100644 --- a/tests/e2e/sms/features/steps/webhooks.steps.py +++ b/tests/e2e/sms/features/steps/webhooks.steps.py @@ -14,7 +14,7 @@ SINCH_SMS_SINCH_EVENT_SECRET = 'KayakingTheSwell' -@given('the SMS Sinch Events handler is available') +@given('the SMS Webhooks handler is available') def step_webhook_handler_is_available(context): context.sms_sinch_event = SmsSinchEvent(SINCH_SMS_SINCH_EVENT_SECRET) From fe18ffa4b6ebec133a64ba81c4bbdc68c2bea6db Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Thu, 19 Mar 2026 18:43:44 +0100 Subject: [PATCH 07/10] Update examples/sinch_events/server.py Co-authored-by: Jean-Pierre Portier <141755467+JPPortier@users.noreply.github.com> --- examples/sinch_events/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sinch_events/server.py b/examples/sinch_events/server.py index 67876863..527bb628 100644 --- a/examples/sinch_events/server.py +++ b/examples/sinch_events/server.py @@ -18,7 +18,7 @@ config = load_config() port = int(config.get('SERVER_PORT') or 3001) numbers_webhooks_secret = config.get('NUMBERS_WEBHOOKS_SECRET') -sms_sinch_event_secret = config.get('SMS_WEBHOOKS_SECRET') +sms_sinch_event_secret = config.get('SMS_SINCH_EVENT_SECRET') conversation_webhooks_secret = config.get('CONVERSATION_WEBHOOKS_SECRET') sinch_client = get_sinch_client(config) From 3fdb13fe8ec480819d4de33c896875af971988c9 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Thu, 19 Mar 2026 18:43:55 +0100 Subject: [PATCH 08/10] Update examples/sinch_events/README.md Co-authored-by: Jean-Pierre Portier <141755467+JPPortier@users.noreply.github.com> --- examples/sinch_events/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sinch_events/README.md b/examples/sinch_events/README.md index ce9ada5b..8bb96063 100644 --- a/examples/sinch_events/README.md +++ b/examples/sinch_events/README.md @@ -108,6 +108,6 @@ Use this value to configure the Sinch Events URLs: You can also set these Sinch Events URLs in the Sinch dashboard; the API parameters above override the default values configured there. -> **Note**: If you have set a Sinch Event secret (e.g., `SMS_WEBHOOKS_SECRET`), the Sinch Event URL must be configured in the Sinch dashboard +> **Note**: If you have set a Sinch Event secret (e.g., `SMS_SINCH_EVENT_SECRET`), the Sinch Event URL must be configured in the Sinch dashboard > and cannot be overridden via API parameters. The Sinch Event secret is used to validate incoming webhook requests, > and the URL associated with it must be set in the dashboard. From fb022feacea4efd8e52df2ce09678e10429a7082 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Thu, 19 Mar 2026 18:58:22 +0100 Subject: [PATCH 09/10] code review II --- examples/sinch_events/.env.example | 8 ++++---- examples/sinch_events/README.md | 4 ++-- examples/sinch_events/numbers_api/controller.py | 6 +++--- examples/sinch_events/server.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/sinch_events/.env.example b/examples/sinch_events/.env.example index 13561254..baf66017 100644 --- a/examples/sinch_events/.env.example +++ b/examples/sinch_events/.env.example @@ -1,11 +1,11 @@ # Server Configuration SERVER_PORT = -# Webhook Configuration -# The secret value used for webhook calls validation +# Sinch Event Configuration +# The secret value used for Sinch Event callback validation # See https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Numbers-Callbacks/ -NUMBERS_WEBHOOKS_SECRET = NUMBERS_WEBHOOKS_SECRET +NUMBERS_SINCH_EVENT_SECRET = NUMBERS_SINCH_EVENT_SECRET # See https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/#tag/Webhooks/section/Callbacks -SMS_WEBHOOKS_SECRET = SMS_WEBHOOKS_SECRET +SMS_SINCH_EVENT_SECRET = SMS_SINCH_EVENT_SECRET # See https://developers.sinch.com/docs/conversation/callbacks CONVERSATION_WEBHOOKS_SECRET = CONVERSATION_WEBHOOKS_SECRET \ No newline at end of file diff --git a/examples/sinch_events/README.md b/examples/sinch_events/README.md index 8bb96063..c0a03713 100644 --- a/examples/sinch_events/README.md +++ b/examples/sinch_events/README.md @@ -32,13 +32,13 @@ This directory contains both the Event handlers and the server application (`ser - Controller Settings - Numbers controller: Set the `numbers` Sinch Event secret. You can retrieve it using the `/event_destination` endpoint (see SDK implementation: [event_destinations_apis.py](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/numbers/api/v1/event_destinations_apis.py); for additional details, refer to the [Numbers API callbacks documentation](https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Numbers-Callbacks/)): ``` - NUMBERS_WEBHOOKS_SECRET=Your Sinch Numbers Webhook Secret + NUMBERS_SINCH_EVENT_SECRET=Your Sinch Numbers Sinch Event Secret ``` - SMS controller: To configure the `sms` Sinch Event secret, contact your account manager to enable authentication for SMS callbacks. For more details, refer to [SMS API](https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/#tag/Webhooks/section/Callbacks), ``` - SMS_WEBHOOKS_SECRET=Your Sinch SMS Webhook Secret + SMS_SINCH_EVENT_SECRET=Your Sinch SMS Sinch Event Secret ``` - Conversation controller: Set the webhook secret you configured when creating the webhook (see [Conversation API callbacks](https://developers.sinch.com/docs/conversation/callbacks)): ``` diff --git a/examples/sinch_events/numbers_api/controller.py b/examples/sinch_events/numbers_api/controller.py index 8b46257f..cdc43e22 100644 --- a/examples/sinch_events/numbers_api/controller.py +++ b/examples/sinch_events/numbers_api/controller.py @@ -3,9 +3,9 @@ class NumbersController: - def __init__(self, sinch_client, webhooks_secret): + def __init__(self, sinch_client, sinch_event_secret): self.sinch_client = sinch_client - self.webhooks_secret = webhooks_secret + self.sinch_event_secret = sinch_event_secret self.logger = self.sinch_client.configuration.logger def numbers_event(self): @@ -13,7 +13,7 @@ def numbers_event(self): raw_body = request.raw_body if request.raw_body else b"" sinch_events_service = self.sinch_client.numbers.sinch_events( - self.webhooks_secret + self.sinch_event_secret ) ensure_valid_authentication = False diff --git a/examples/sinch_events/server.py b/examples/sinch_events/server.py index 527bb628..49288b83 100644 --- a/examples/sinch_events/server.py +++ b/examples/sinch_events/server.py @@ -17,7 +17,7 @@ config = load_config() port = int(config.get('SERVER_PORT') or 3001) -numbers_webhooks_secret = config.get('NUMBERS_WEBHOOKS_SECRET') +numbers_sinch_event_secret = config.get('NUMBERS_SINCH_EVENT_SECRET') sms_sinch_event_secret = config.get('SMS_SINCH_EVENT_SECRET') conversation_webhooks_secret = config.get('CONVERSATION_WEBHOOKS_SECRET') sinch_client = get_sinch_client(config) @@ -26,7 +26,7 @@ logging.basicConfig() sinch_client.configuration.logger.setLevel(logging.INFO) -numbers_controller = NumbersController(sinch_client, numbers_webhooks_secret) +numbers_controller = NumbersController(sinch_client, numbers_sinch_event_secret) sms_controller = SmsController(sinch_client, sms_sinch_event_secret) conversation_controller = ConversationController(sinch_client, conversation_webhooks_secret or '') From 7647bd1cf1cb4577fb279bce812caaf31f04d3f9 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Thu, 19 Mar 2026 19:00:46 +0100 Subject: [PATCH 10/10] rename conversation start --- examples/sinch_events/.env.example | 2 +- examples/sinch_events/README.md | 2 +- examples/sinch_events/conversation_api/controller.py | 8 +++++--- examples/sinch_events/server.py | 6 ++++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/examples/sinch_events/.env.example b/examples/sinch_events/.env.example index baf66017..02133356 100644 --- a/examples/sinch_events/.env.example +++ b/examples/sinch_events/.env.example @@ -8,4 +8,4 @@ NUMBERS_SINCH_EVENT_SECRET = NUMBERS_SINCH_EVENT_SECRET # See https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/#tag/Webhooks/section/Callbacks SMS_SINCH_EVENT_SECRET = SMS_SINCH_EVENT_SECRET # See https://developers.sinch.com/docs/conversation/callbacks -CONVERSATION_WEBHOOKS_SECRET = CONVERSATION_WEBHOOKS_SECRET \ No newline at end of file +CONVERSATION_SINCH_EVENT_SECRET = CONVERSATION_SINCH_EVENT_SECRET \ No newline at end of file diff --git a/examples/sinch_events/README.md b/examples/sinch_events/README.md index c0a03713..826fe987 100644 --- a/examples/sinch_events/README.md +++ b/examples/sinch_events/README.md @@ -42,7 +42,7 @@ This directory contains both the Event handlers and the server application (`ser ``` - Conversation controller: Set the webhook secret you configured when creating the webhook (see [Conversation API callbacks](https://developers.sinch.com/docs/conversation/callbacks)): ``` - CONVERSATION_WEBHOOKS_SECRET=Your Conversation Webhook Secret + CONVERSATION_SINCH_EVENT_SECRET=Your Conversation Sinch Event Secret ``` ## Usage diff --git a/examples/sinch_events/conversation_api/controller.py b/examples/sinch_events/conversation_api/controller.py index 03bdbae9..b71a287b 100644 --- a/examples/sinch_events/conversation_api/controller.py +++ b/examples/sinch_events/conversation_api/controller.py @@ -3,16 +3,18 @@ class ConversationController: - def __init__(self, sinch_client, webhooks_secret): + def __init__(self, sinch_client, sinch_event_secret): self.sinch_client = sinch_client - self.webhooks_secret = webhooks_secret + self.sinch_event_secret = sinch_event_secret self.logger = self.sinch_client.configuration.logger def conversation_event(self): headers = dict(request.headers) raw_body = request.raw_body if request.raw_body else b"" - webhooks_service = self.sinch_client.conversation.webhooks(self.webhooks_secret) + webhooks_service = self.sinch_client.conversation.webhooks( + self.sinch_event_secret + ) # Set to True to enforce signature validation (recommended in production) ensure_valid_signature = False diff --git a/examples/sinch_events/server.py b/examples/sinch_events/server.py index 49288b83..368cec68 100644 --- a/examples/sinch_events/server.py +++ b/examples/sinch_events/server.py @@ -19,7 +19,7 @@ port = int(config.get('SERVER_PORT') or 3001) numbers_sinch_event_secret = config.get('NUMBERS_SINCH_EVENT_SECRET') sms_sinch_event_secret = config.get('SMS_SINCH_EVENT_SECRET') -conversation_webhooks_secret = config.get('CONVERSATION_WEBHOOKS_SECRET') +conversation_sinch_event_secret = config.get('CONVERSATION_SINCH_EVENT_SECRET') sinch_client = get_sinch_client(config) # Set up logging at the INFO level @@ -28,7 +28,9 @@ numbers_controller = NumbersController(sinch_client, numbers_sinch_event_secret) sms_controller = SmsController(sinch_client, sms_sinch_event_secret) -conversation_controller = ConversationController(sinch_client, conversation_webhooks_secret or '') +conversation_controller = ConversationController( + sinch_client, conversation_sinch_event_secret or '' +) # Middleware to capture raw body