From 371a3a32161ea435333701e1ec65c43636a6db32 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Sun, 14 Dec 2025 18:33:37 +0100 Subject: [PATCH 1/5] DEVEXP-788: SMS API - Webhooks Quickstart --- examples/{ => snippets}/.env.example | 0 examples/{ => snippets}/README.md | 2 +- examples/{ => snippets}/pyproject.toml | 0 examples/webhooks/.env.example | 16 +++ examples/webhooks/README.md | 104 +++++++++++++++++ examples/webhooks/numbers_api/__init__.py | 0 examples/webhooks/numbers_api/controller.py | 30 +++++ .../numbers_api/server_business_logic.py | 11 ++ examples/webhooks/pyproject.toml | 16 +++ examples/webhooks/server.py | 40 +++++++ examples/webhooks/sinch_client_helper.py | 42 +++++++ examples/webhooks/sms_api/__init__.py | 0 examples/webhooks/sms_api/controller.py | 26 +++++ .../webhooks/sms_api/server_business_logic.py | 11 ++ .../clients/sinch_client_configuration.py | 17 ++- sinch/domains/sms/models/batches/__init__.py | 22 ---- sinch/domains/sms/models/batches/requests.py | 108 ------------------ sinch/domains/sms/models/batches/responses.py | 60 ---------- .../sms/models/delivery_reports/__init__.py | 18 --- .../sms/models/delivery_reports/requests.py | 27 ----- .../sms/models/delivery_reports/responses.py | 34 ------ sinch/domains/sms/sms.py | 12 ++ 22 files changed, 324 insertions(+), 272 deletions(-) rename examples/{ => snippets}/.env.example (100%) rename examples/{ => snippets}/README.md (95%) rename examples/{ => snippets}/pyproject.toml (100%) create mode 100644 examples/webhooks/.env.example create mode 100644 examples/webhooks/README.md create mode 100644 examples/webhooks/numbers_api/__init__.py create mode 100644 examples/webhooks/numbers_api/controller.py create mode 100644 examples/webhooks/numbers_api/server_business_logic.py create mode 100644 examples/webhooks/pyproject.toml create mode 100644 examples/webhooks/server.py create mode 100644 examples/webhooks/sinch_client_helper.py create mode 100644 examples/webhooks/sms_api/__init__.py create mode 100644 examples/webhooks/sms_api/controller.py create mode 100644 examples/webhooks/sms_api/server_business_logic.py delete mode 100644 sinch/domains/sms/models/batches/__init__.py delete mode 100644 sinch/domains/sms/models/batches/requests.py delete mode 100644 sinch/domains/sms/models/batches/responses.py delete mode 100644 sinch/domains/sms/models/delivery_reports/__init__.py delete mode 100644 sinch/domains/sms/models/delivery_reports/requests.py delete mode 100644 sinch/domains/sms/models/delivery_reports/responses.py diff --git a/examples/.env.example b/examples/snippets/.env.example similarity index 100% rename from examples/.env.example rename to examples/snippets/.env.example diff --git a/examples/README.md b/examples/snippets/README.md similarity index 95% rename from examples/README.md rename to examples/snippets/README.md index 922fb91d..35bb6550 100644 --- a/examples/README.md +++ b/examples/snippets/README.md @@ -2,7 +2,7 @@ Sinch Python SDK Code Snippets -This repository contains code snippets demonstrating usage of the +This directory contains code snippets demonstrating usage of the [Sinch Python SDK](https://github.com/sinch/sinch-sdk-python). ## Requirements diff --git a/examples/pyproject.toml b/examples/snippets/pyproject.toml similarity index 100% rename from examples/pyproject.toml rename to examples/snippets/pyproject.toml diff --git a/examples/webhooks/.env.example b/examples/webhooks/.env.example new file mode 100644 index 00000000..a09975dc --- /dev/null +++ b/examples/webhooks/.env.example @@ -0,0 +1,16 @@ +# Unified Credentials +SINCH_PROJECT_ID = PROJECT_ID +SINCH_KEY_ID = KEY_ID +SINCH_KEY_SECRET = KEY_SECRET + +# Server Configuration +SERVER_PORT = + +# Webhook Configuration +# The secret value used for webhook calls validation +# See https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Numbers-Callbacks/ +NUMBERS_WEBHOOKS_SECRET = WEBHOOKS_SECRET + +# SMS Service Plan ID related credentials. +#SINCH_SERVICE_PLAN_ID = SERVICE_PLAN_ID +#SINCH_API_TOKEN = API_TOKEN \ No newline at end of file diff --git a/examples/webhooks/README.md b/examples/webhooks/README.md new file mode 100644 index 00000000..041d8747 --- /dev/null +++ b/examples/webhooks/README.md @@ -0,0 +1,104 @@ +# Webhook 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/`) + +This directory contains both the webhook handlers and the server application (`server.py`) that uses them. + +## Requirements + +- [Python 3.9+](https://www.python.org/) +- [Flask](https://flask.palletsprojects.com/en/stable/) +- [Sinch account](https://dashboard.sinch.com/) +- [ngrok](https://ngrok.com/docs) +- [Poetry](https://python-poetry.org/) + +## 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. + + - To use [Numbers](https://developers.sinch.com/docs/numbers/), you need to fill the following variables + with the values from your Sinch account: + ``` + SINCH_PROJECT_ID=Your Sinch Project ID + SINCH_KEY_ID=Your Sinch Access Key ID + SINCH_KEY_SECRET=Your Sinch Key Secret associated to your Sinch Access Key + ``` + + - Server Port: + Define the port your server will listen to on (default: 3001): + ``` + SERVER_PORT=3001 + ``` + + - Controller Settings + - Numbers controller: Set the webhook secret, which you can retrieve from the [Numbers API](https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Numbers-Callbacks/), + using the `/callbackConfiguration` endpoint: + ``` + NUMBERS_WEBHOOKS_SECRET=Your Sinch Webhook Secret + ``` + +## Usage + +### Running the server application + +1. Navigate to the webhooks' directory: +``` + cd examples/webhooks +``` + +2. Install the project dependencies: +``` bash + poetry install +``` + +3. Rename `.env.example` to `.env` and update it with your configuration (see above). + +4. Start the server: +``` bash + poetry run python server.py +``` +Or run it directly: +``` bash + python server.py +``` + +The server will start on the port specified in your `.env` file (default: 3001). + +### Endpoints + +The server exposes the following endpoints: + +| Service | Endpoint | +|--------------|--------------------| +| Numbers | /NumbersEvent | +| SMS | /SmsEvent | + +## Using ngrok to expose your local server + +To test your webhook locally, you can tunnel requests to your local server using ngrok. + +*Note: The default port is `3001`, but this can be changed (see [Server port](#Configuration))* + +```bash + ngrok http 3001 +``` + +You'll see output similar to this: +``` +ngrok (Ctrl+C to quit) +... +Forwarding https://adbd-79-148-170-158.ngrok-free.app -> http://localhost:3001 +``` +Use the `https` forwarding URL in your callback 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 + +Use this value to configure the callback URLs: + - **Numbers**: Set the `callbackUrl` parameter when renting or updating a number via the API + - **SMS**: Set the `callback_url` parameter when configuring your SMS service plan via the API diff --git a/examples/webhooks/numbers_api/__init__.py b/examples/webhooks/numbers_api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/webhooks/numbers_api/controller.py b/examples/webhooks/numbers_api/controller.py new file mode 100644 index 00000000..130edca7 --- /dev/null +++ b/examples/webhooks/numbers_api/controller.py @@ -0,0 +1,30 @@ +from flask import request, Response +from webhooks.numbers_api.server_business_logic import handle_numbers_event + + +class NumbersController: + def __init__(self, sinch_client, webhooks_secret): + self.sinch_client = sinch_client + self.webhooks_secret = webhooks_secret + self.logger = self.sinch_client.configuration.logger + + def numbers_event(self): + headers = dict(request.headers) + body_str = request.raw_body.decode('utf-8') if request.raw_body else '' + + webhooks_service = self.sinch_client.numbers.webhooks(self.webhooks_secret) + + valid_auth = webhooks_service.validate_authentication_header( + headers=headers, + json_payload=body_str + ) + + if not valid_auth: + self.logger.error('Invalid authentication header') + return Response(status=401) + + event = webhooks_service.parse_event(body_str) + + handle_numbers_event(numbers_event=event, logger=self.logger) + + return Response(status=200) diff --git a/examples/webhooks/numbers_api/server_business_logic.py b/examples/webhooks/numbers_api/server_business_logic.py new file mode 100644 index 00000000..80082812 --- /dev/null +++ b/examples/webhooks/numbers_api/server_business_logic.py @@ -0,0 +1,11 @@ +from sinch.domains.numbers.webhooks.v1.events.numbers_webhooks_event import NumbersWebhooksEvent + + +def handle_numbers_event(numbers_event: NumbersWebhooksEvent, logger): + """ + This method handles a Numbers event. + Args: + numbers_event (NumbersWebhooksEvent): The Numbers event data. + logger (logging.Logger, optional): Logger instance for logging. Defaults to None. + """ + logger.info(f'Handling Numbers event:\n{numbers_event.model_dump_json(indent=2)}') diff --git a/examples/webhooks/pyproject.toml b/examples/webhooks/pyproject.toml new file mode 100644 index 00000000..dd9e9d42 --- /dev/null +++ b/examples/webhooks/pyproject.toml @@ -0,0 +1,16 @@ +[tool.poetry] +name = "sinch-sdk-python-quickstart-server" +version = "0.1.0" +description = "Sinch SDK Python Quickstart Server" +readme = "README.md" +package-mode = false + +[tool.poetry.dependencies] +python = "^3.9" +python-dotenv = "^1.0.0" +flask = "^3.0.0" +# sinch = "^2.0.0" # Uncomment once v2.0 is released + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/examples/webhooks/server.py b/examples/webhooks/server.py new file mode 100644 index 00000000..21f48a13 --- /dev/null +++ b/examples/webhooks/server.py @@ -0,0 +1,40 @@ +import logging +import sys +from pathlib import Path + +# Add examples directory to Python path to allow importing webhooks +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.sinch_client_helper import get_sinch_client, load_config + +app = Flask(__name__) + +config = load_config() +port = int(config.get('SERVER_PORT') or 3001) +numbers_webhooks_secret = config.get('NUMBERS_WEBHOOKS_SECRET') +sinch_client = get_sinch_client(config) + +# Set up logging at the INFO level +logging.basicConfig() +sinch_client.configuration.logger.setLevel(logging.INFO) + +numbers_controller = NumbersController(sinch_client, numbers_webhooks_secret) +sms_controller = SmsController(sinch_client) + + +# Middleware to capture raw body +@app.before_request +def before_request(): + request.raw_body = request.get_data() + + +app.add_url_rule('/NumbersEvent', methods=['POST'], view_func=numbers_controller.numbers_event) +app.add_url_rule('/SmsEvent', methods=['POST'], view_func=sms_controller.sms_event) + +if __name__ == '__main__': + app.run(port=port) diff --git a/examples/webhooks/sinch_client_helper.py b/examples/webhooks/sinch_client_helper.py new file mode 100644 index 00000000..4ec8c3fb --- /dev/null +++ b/examples/webhooks/sinch_client_helper.py @@ -0,0 +1,42 @@ +from pathlib import Path +from sinch import SinchClient +from dotenv import dotenv_values + + +def load_config() -> dict[str, str]: + """ + Load configuration from the .env file in the webhooks directory. + + Returns: + dict[str, str]: Dictionary containing configuration values + """ + # Get the directory where this file is located + current_dir = Path(__file__).resolve().parent + env_file = current_dir / '.env' + + if not env_file.exists(): + raise FileNotFoundError(f"Could not find .env file in webhooks directory: {env_file}") + + config_dict = dotenv_values(env_file) + + return config_dict + + +def get_sinch_client(config: dict) -> SinchClient: + """ + Create and return a configured SinchClient instance. + + Args: + config (dict): Dictionary containing configuration values + Returns: + SinchClient: Configured Sinch client instance + """ + project_id = config.get('SINCH_PROJECT_ID') + key_id = config.get('SINCH_KEY_ID') + key_secret = config.get('SINCH_KEY_SECRET') + + return SinchClient( + project_id=project_id, + key_id=key_id, + key_secret=key_secret, + ) diff --git a/examples/webhooks/sms_api/__init__.py b/examples/webhooks/sms_api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/webhooks/sms_api/controller.py b/examples/webhooks/sms_api/controller.py new file mode 100644 index 00000000..a626abec --- /dev/null +++ b/examples/webhooks/sms_api/controller.py @@ -0,0 +1,26 @@ +from flask import request, Response +from webhooks.sms_api.server_business_logic import ( + handle_sms_event, +) + + +class SmsController: + def __init__(self, sinch_client, webhooks_secret=None): + self.sinch_client = sinch_client + self.webhooks_secret = webhooks_secret + self.logger = self.sinch_client.configuration.logger + + def sms_event(self): + body_str = request.raw_body.decode('utf-8') if request.raw_body else '' + + webhooks_service = self.sinch_client.sms.webhooks(self.webhooks_secret) + + event = webhooks_service.parse_event(body_str) + + event_type = getattr(event, 'type', None) + + self.logger.info(f'Handling SMS event: {event_type}') + + handle_sms_event(sms_event=event, logger=self.logger) + + return Response(status=200) diff --git a/examples/webhooks/sms_api/server_business_logic.py b/examples/webhooks/sms_api/server_business_logic.py new file mode 100644 index 00000000..aa47e470 --- /dev/null +++ b/examples/webhooks/sms_api/server_business_logic.py @@ -0,0 +1,11 @@ +from sinch.domains.sms.webhooks.v1.events.sms_webhooks_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. + 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/core/clients/sinch_client_configuration.py b/sinch/core/clients/sinch_client_configuration.py index 0fe559ec..1db8e7d9 100644 --- a/sinch/core/clients/sinch_client_configuration.py +++ b/sinch/core/clients/sinch_client_configuration.py @@ -266,8 +266,21 @@ def get_sms_origin_for_auth(self): Returns the appropriate SMS origin based on the authentication method. - SMS auth (service_plan_id + sms_api_token): uses sms_origin_with_service_plan_id - Project auth (project_id): uses regular sms_origin + + Raises: + ValueError: If the SMS origin is None (sms_region not set) """ if self._authentication_method == "sms_auth": - return self.sms_origin_with_service_plan_id + origin = self.sms_origin_with_service_plan_id else: - return self.sms_origin + origin = self.sms_origin + + if origin is None: + raise ValueError( + "SMS region is required. " + "Provide sms_region when initializing SinchClient " + "Example: SinchClient(project_id='...', key_id='...', key_secret='...', sms_region='eu')" + "or set it via sinch_client.configuration.sms_region. " + ) + + return origin diff --git a/sinch/domains/sms/models/batches/__init__.py b/sinch/domains/sms/models/batches/__init__.py deleted file mode 100644 index 1e352ded..00000000 --- a/sinch/domains/sms/models/batches/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from dataclasses import dataclass -from sinch.core.models.base_model import SinchRequestBaseModel - - -@dataclass -class Batch(SinchRequestBaseModel): - id: str - to: list - from_: str - body: str - delivery_report: str - cancelled: str - type: str - campaign_id: str - created_at: str - modified_at: str - send_at: str - expire_at: str - callback_url: str = None - client_reference: str = None - feedback_enabled: bool = None - flash_message: bool = None diff --git a/sinch/domains/sms/models/batches/requests.py b/sinch/domains/sms/models/batches/requests.py deleted file mode 100644 index 99b6cb9d..00000000 --- a/sinch/domains/sms/models/batches/requests.py +++ /dev/null @@ -1,108 +0,0 @@ -from dataclasses import dataclass -from sinch.core.models.base_model import SinchRequestBaseModel - - -@dataclass -class BatchRequest(SinchRequestBaseModel): - def as_dict(self): - payload = super(BatchRequest, self).as_dict() - payload["to"] = payload.pop("to") - if payload.get("from_"): - payload["from"] = payload.pop("from_") - return payload - - -@dataclass -class SendBatchRequest(BatchRequest): - to: list - from_: str - body: str - delivery_report: str - parameters: dict - send_at: str - expire_at: str - callback_url: str - client_reference: str - feedback_enabled: bool - flash_message: bool - truncate_concat: bool - type_: str - max_number_of_message_parts: int - from_ton: int - from_npi: int - - -@dataclass -class ListBatchesRequest(SinchRequestBaseModel): - page_size: int - from_s: str - start_date: str - end_date: str - client_reference: str - page: int = 0 - - -@dataclass -class GetBatchRequest(SinchRequestBaseModel): - batch_id: str - - -@dataclass -class CancelBatchRequest(SinchRequestBaseModel): - batch_id: str - - -@dataclass -class BatchDryRunRequest(BatchRequest): - per_recipient: bool - number_of_recipients: int - to: str - from_: str - body: str - type_: str - udh: str - delivery_report: str - parameters: dict - send_at: str - expire_at: str - callback_url: str - client_reference: str - flash_message: bool - max_number_of_message_parts: int - - -@dataclass -class UpdateBatchRequest(SinchRequestBaseModel): - batch_id: str - to_add: list - to_remove: list - from_: str - body: str - delivery_report: str - send_at: str - expire_at: str - callback_url: str - - -@dataclass -class ReplaceBatchRequest(BatchRequest): - batch_id: str - to: str - from_: str - body: str - delivery_report: str - parameters: dict - send_at: str - expire_at: str - type_: str - callback_url: str - client_reference: str - flash_message: bool - max_number_of_message_parts: int - udh: str - - -@dataclass -class SendDeliveryFeedbackRequest(SinchRequestBaseModel): - batch_id: str - recipients: list diff --git a/sinch/domains/sms/models/batches/responses.py b/sinch/domains/sms/models/batches/responses.py deleted file mode 100644 index a0e918f5..00000000 --- a/sinch/domains/sms/models/batches/responses.py +++ /dev/null @@ -1,60 +0,0 @@ -from dataclasses import dataclass -from sinch.core.models.base_model import SinchBaseModel -from sinch.domains.sms.models.batches import Batch - - -@dataclass -class SendSMSBatchResponse(Batch): - pass - - -@dataclass -class ReplaceSMSBatchResponse(Batch): - pass - - -@dataclass -class ListSMSBatchesResponse(SinchBaseModel): - page: str - page_size: str - count: str - batches: list - - -@dataclass -class GetSMSBatchResponse(Batch): - pass - - -@dataclass -class CancelSMSBatchResponse(Batch): - pass - - -@dataclass -class SendSMSBatchDryRunResponse(SinchBaseModel): - number_of_recipients: int - number_of_messages: int - per_recipient: list - - -@dataclass -class UpdateSMSBatchResponse(SinchBaseModel): - id: str - to: list - from_: str - body: str - campaign_id: str - delivery_report: str - send_at: str - expire_at: str - callback_url: str - cancelled: bool - type: str - created_at: str - modified_at: str - - -@dataclass -class SendSMSDeliveryFeedbackResponse(SinchBaseModel): - pass diff --git a/sinch/domains/sms/models/delivery_reports/__init__.py b/sinch/domains/sms/models/delivery_reports/__init__.py deleted file mode 100644 index b6c40915..00000000 --- a/sinch/domains/sms/models/delivery_reports/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from dataclasses import dataclass -from sinch.core.models.base_model import SinchBaseModel - - -@dataclass -class DeliveryReport(SinchBaseModel): - at: str - batch_id: str - code: int - recipient: str - status: str - applied_originator: str - client_reference: str - encoding: str - number_of_message_parts: int - operator: str - operator_status_at: str - type: str diff --git a/sinch/domains/sms/models/delivery_reports/requests.py b/sinch/domains/sms/models/delivery_reports/requests.py deleted file mode 100644 index 09857fd3..00000000 --- a/sinch/domains/sms/models/delivery_reports/requests.py +++ /dev/null @@ -1,27 +0,0 @@ -from dataclasses import dataclass -from sinch.core.models.base_model import SinchRequestBaseModel - - -@dataclass -class ListSMSDeliveryReportsRequest(SinchRequestBaseModel): - code: str - status: str - start_date: str - end_date: str - client_reference: str - page_size: int - page: int = 0 - - -@dataclass -class GetSMSDeliveryReportForBatchRequest(SinchRequestBaseModel): - batch_id: str - type_: str - status: list - code: list - - -@dataclass -class GetSMSDeliveryReportForNumberRequest(SinchRequestBaseModel): - batch_id: str - recipient_number: str diff --git a/sinch/domains/sms/models/delivery_reports/responses.py b/sinch/domains/sms/models/delivery_reports/responses.py deleted file mode 100644 index 18a53ba4..00000000 --- a/sinch/domains/sms/models/delivery_reports/responses.py +++ /dev/null @@ -1,34 +0,0 @@ -from dataclasses import dataclass -from sinch.core.models.base_model import SinchBaseModel - - -@dataclass -class ListSMSDeliveryReportsResponse(SinchBaseModel): - page: str - page_size: str - count: str - delivery_reports: list - - -@dataclass -class GetSMSDeliveryReportForBatchResponse(SinchBaseModel): - type: str - batch_id: str - total_message_count: str - statuses: list - client_reference: str - - -@dataclass -class GetSMSDeliveryReportForNumberResponse(SinchBaseModel): - at: str - batch_id: str - code: int - recipient: str - status: str - applied_originator: str - client_reference: str - number_of_message_parts: str - operator: str - operator_status_at: str - type: str diff --git a/sinch/domains/sms/sms.py b/sinch/domains/sms/sms.py index f53bab95..3c5c3ff3 100644 --- a/sinch/domains/sms/sms.py +++ b/sinch/domains/sms/sms.py @@ -2,6 +2,7 @@ Batches, DeliveryReports, ) +from sinch.domains.sms.webhooks.v1.sms_webhooks import SmsWebhooks class SMS: @@ -15,3 +16,14 @@ def __init__(self, sinch): self.batches = Batches(self._sinch) self.delivery_reports = DeliveryReports(self._sinch) + + def webhooks(self, callback_secret: str) -> SmsWebhooks: + """ + Create an SMS webhooks 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 + """ + return SmsWebhooks(callback_secret) From 15b688d53e8fa5c7dc9b6508e2a8a418bbbe9239 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Tue, 16 Dec 2025 14:05:48 +0100 Subject: [PATCH 2/5] solve PR comments --- examples/webhooks/.env.example | 4 +++- examples/webhooks/README.md | 18 +++++++----------- examples/webhooks/numbers_api/controller.py | 18 ++++++++++-------- examples/webhooks/pyproject.toml | 3 ++- examples/webhooks/server.py | 3 ++- examples/webhooks/sms_api/controller.py | 15 ++++++++++++++- 6 files changed, 38 insertions(+), 23 deletions(-) diff --git a/examples/webhooks/.env.example b/examples/webhooks/.env.example index a09975dc..357a5136 100644 --- a/examples/webhooks/.env.example +++ b/examples/webhooks/.env.example @@ -9,7 +9,9 @@ SERVER_PORT = # Webhook Configuration # The secret value used for webhook calls validation # See https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Numbers-Callbacks/ -NUMBERS_WEBHOOKS_SECRET = WEBHOOKS_SECRET +NUMBERS_WEBHOOKS_SECRET = NUMBERS_WEBHOOKS_SECRET +# See https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/#tag/Webhooks/section/Callbacks +SMS_WEBHOOKS_SECRET = SMS_WEBHOOKS_SECRET # SMS Service Plan ID related credentials. #SINCH_SERVICE_PLAN_ID = SERVICE_PLAN_ID diff --git a/examples/webhooks/README.md b/examples/webhooks/README.md index 041d8747..dc937071 100644 --- a/examples/webhooks/README.md +++ b/examples/webhooks/README.md @@ -22,14 +22,6 @@ This directory contains both the webhook handlers and the server application (`s 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. - - To use [Numbers](https://developers.sinch.com/docs/numbers/), you need to fill the following variables - with the values from your Sinch account: - ``` - SINCH_PROJECT_ID=Your Sinch Project ID - SINCH_KEY_ID=Your Sinch Access Key ID - SINCH_KEY_SECRET=Your Sinch Key Secret associated to your Sinch Access Key - ``` - - Server Port: Define the port your server will listen to on (default: 3001): ``` @@ -57,9 +49,7 @@ This directory contains both the webhook handlers and the server application (`s poetry install ``` -3. Rename `.env.example` to `.env` and update it with your configuration (see above). - -4. Start the server: +3. Start the server: ``` bash poetry run python server.py ``` @@ -102,3 +92,9 @@ Use the `https` forwarding URL in your callback configuration. For example: Use this value to configure the callback URLs: - **Numbers**: Set the `callbackUrl` parameter when renting or updating a number via the API - **SMS**: Set the `callback_url` parameter when configuring your SMS service plan via the API + +You can also set these callback 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 +> 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. diff --git a/examples/webhooks/numbers_api/controller.py b/examples/webhooks/numbers_api/controller.py index 130edca7..cf56b6ee 100644 --- a/examples/webhooks/numbers_api/controller.py +++ b/examples/webhooks/numbers_api/controller.py @@ -14,14 +14,16 @@ def numbers_event(self): webhooks_service = self.sinch_client.numbers.webhooks(self.webhooks_secret) - valid_auth = webhooks_service.validate_authentication_header( - headers=headers, - json_payload=body_str - ) - - if not valid_auth: - self.logger.error('Invalid authentication header') - return Response(status=401) + ensure_valid_authentication = False + if ensure_valid_authentication: + valid_auth = webhooks_service.validate_authentication_header( + headers=headers, + json_payload=body_str + ) + + if not valid_auth: + self.logger.error('Invalid authentication header') + return Response(status=401) event = webhooks_service.parse_event(body_str) diff --git a/examples/webhooks/pyproject.toml b/examples/webhooks/pyproject.toml index dd9e9d42..a7c869b6 100644 --- a/examples/webhooks/pyproject.toml +++ b/examples/webhooks/pyproject.toml @@ -9,7 +9,8 @@ package-mode = false python = "^3.9" python-dotenv = "^1.0.0" flask = "^3.0.0" -# sinch = "^2.0.0" # Uncomment once v2.0 is released +# TODO: Uncomment once v2.0 is released +# sinch = "^2.0.0" [build-system] requires = ["poetry-core"] diff --git a/examples/webhooks/server.py b/examples/webhooks/server.py index 21f48a13..d7f6f1ca 100644 --- a/examples/webhooks/server.py +++ b/examples/webhooks/server.py @@ -17,6 +17,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') sinch_client = get_sinch_client(config) # Set up logging at the INFO level @@ -24,7 +25,7 @@ sinch_client.configuration.logger.setLevel(logging.INFO) numbers_controller = NumbersController(sinch_client, numbers_webhooks_secret) -sms_controller = SmsController(sinch_client) +sms_controller = SmsController(sinch_client, sms_webhooks_secret) # Middleware to capture raw body diff --git a/examples/webhooks/sms_api/controller.py b/examples/webhooks/sms_api/controller.py index a626abec..a0f9d14c 100644 --- a/examples/webhooks/sms_api/controller.py +++ b/examples/webhooks/sms_api/controller.py @@ -5,16 +5,29 @@ class SmsController: - def __init__(self, sinch_client, webhooks_secret=None): + def __init__(self, sinch_client, webhooks_secret): self.sinch_client = sinch_client self.webhooks_secret = webhooks_secret self.logger = self.sinch_client.configuration.logger def sms_event(self): + headers = dict(request.headers) + body_str = request.raw_body.decode('utf-8') if request.raw_body else '' webhooks_service = self.sinch_client.sms.webhooks(self.webhooks_secret) + ensure_valid_authentication = False + if ensure_valid_authentication: + valid_auth = webhooks_service.validate_authentication_header( + headers=headers, + json_payload=body_str + ) + + if not valid_auth: + self.logger.error('Invalid authentication header') + return Response(status=401) + event = webhooks_service.parse_event(body_str) event_type = getattr(event, 'type', None) From a27bb098dedf4b17517fd56799639f2e1fdee63b Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Wed, 17 Dec 2025 14:57:32 +0100 Subject: [PATCH 3/5] solve PR comments #2 --- examples/webhooks/.env.example | 11 +---------- examples/webhooks/README.md | 15 ++++++++++----- examples/webhooks/numbers_api/controller.py | 6 +----- examples/webhooks/pyproject.toml | 2 +- examples/webhooks/sinch_client_helper.py | 10 +--------- examples/webhooks/sms_api/controller.py | 13 ++++--------- 6 files changed, 18 insertions(+), 39 deletions(-) diff --git a/examples/webhooks/.env.example b/examples/webhooks/.env.example index 357a5136..02e98c4b 100644 --- a/examples/webhooks/.env.example +++ b/examples/webhooks/.env.example @@ -1,8 +1,3 @@ -# Unified Credentials -SINCH_PROJECT_ID = PROJECT_ID -SINCH_KEY_ID = KEY_ID -SINCH_KEY_SECRET = KEY_SECRET - # Server Configuration SERVER_PORT = @@ -11,8 +6,4 @@ SERVER_PORT = # See https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Numbers-Callbacks/ NUMBERS_WEBHOOKS_SECRET = NUMBERS_WEBHOOKS_SECRET # See https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/#tag/Webhooks/section/Callbacks -SMS_WEBHOOKS_SECRET = SMS_WEBHOOKS_SECRET - -# SMS Service Plan ID related credentials. -#SINCH_SERVICE_PLAN_ID = SERVICE_PLAN_ID -#SINCH_API_TOKEN = API_TOKEN \ No newline at end of file +SMS_WEBHOOKS_SECRET = SMS_WEBHOOKS_SECRET \ No newline at end of file diff --git a/examples/webhooks/README.md b/examples/webhooks/README.md index dc937071..f793862f 100644 --- a/examples/webhooks/README.md +++ b/examples/webhooks/README.md @@ -29,10 +29,15 @@ This directory contains both the webhook handlers and the server application (`s ``` - Controller Settings - - Numbers controller: Set the webhook secret, which you can retrieve from the [Numbers API](https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Numbers-Callbacks/), - using the `/callbackConfiguration` endpoint: + - Numbers controller: Set the `numbers` webhook secret. You can retrieve it using the `/callback_configuration` endpoint (see SDK implementation: [callback_configuration_apis.py](https://github.com/sinch/sinch-sdk-python/blob/v2.0/sinch/domains/numbers/api/v1/callback_configuration_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 Webhook Secret + NUMBERS_WEBHOOKS_SECRET=Your Sinch Numbers Webhook Secret + ``` + - SMS controller: To configure the `sms` webhooks secret, contact your account manager to enable 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 ``` ## Usage @@ -90,8 +95,8 @@ Use the `https` forwarding URL in your callback configuration. For example: - SMS: https://adbd-79-148-170-158.ngrok-free.app/SmsEvent Use this value to configure the callback URLs: - - **Numbers**: Set the `callbackUrl` parameter when renting or updating a number via the API - - **SMS**: Set the `callback_url` parameter when configuring your SMS service plan via the API +- **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#L147), [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. You can also set these callback URLs in the Sinch dashboard; the API parameters above override the default values configured there. diff --git a/examples/webhooks/numbers_api/controller.py b/examples/webhooks/numbers_api/controller.py index cf56b6ee..d13569fa 100644 --- a/examples/webhooks/numbers_api/controller.py +++ b/examples/webhooks/numbers_api/controller.py @@ -16,15 +16,11 @@ def numbers_event(self): ensure_valid_authentication = False if ensure_valid_authentication: - valid_auth = webhooks_service.validate_authentication_header( + webhooks_service.validate_authentication_header( headers=headers, json_payload=body_str ) - if not valid_auth: - self.logger.error('Invalid authentication header') - return Response(status=401) - event = webhooks_service.parse_event(body_str) handle_numbers_event(numbers_event=event, logger=self.logger) diff --git a/examples/webhooks/pyproject.toml b/examples/webhooks/pyproject.toml index a7c869b6..76a53090 100644 --- a/examples/webhooks/pyproject.toml +++ b/examples/webhooks/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "sinch-sdk-python-quickstart-server" version = "0.1.0" -description = "Sinch SDK Python Quickstart Server" +description = "Sinch SDK Python Quickstart Webhooks Server" readme = "README.md" package-mode = false diff --git a/examples/webhooks/sinch_client_helper.py b/examples/webhooks/sinch_client_helper.py index 4ec8c3fb..109fce1c 100644 --- a/examples/webhooks/sinch_client_helper.py +++ b/examples/webhooks/sinch_client_helper.py @@ -31,12 +31,4 @@ def get_sinch_client(config: dict) -> SinchClient: Returns: SinchClient: Configured Sinch client instance """ - project_id = config.get('SINCH_PROJECT_ID') - key_id = config.get('SINCH_KEY_ID') - key_secret = config.get('SINCH_KEY_SECRET') - - return SinchClient( - project_id=project_id, - key_id=key_id, - key_secret=key_secret, - ) + return SinchClient() diff --git a/examples/webhooks/sms_api/controller.py b/examples/webhooks/sms_api/controller.py index a0f9d14c..268c81c6 100644 --- a/examples/webhooks/sms_api/controller.py +++ b/examples/webhooks/sms_api/controller.py @@ -17,23 +17,18 @@ def sms_event(self): webhooks_service = self.sinch_client.sms.webhooks(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( + webhooks_service.validate_authentication_header( headers=headers, json_payload=body_str ) - if not valid_auth: - self.logger.error('Invalid authentication header') - return Response(status=401) - event = webhooks_service.parse_event(body_str) - event_type = getattr(event, 'type', None) - - self.logger.info(f'Handling SMS event: {event_type}') - handle_sms_event(sms_event=event, logger=self.logger) return Response(status=200) From b16e15c99521b4c5af5720b79a6bb839f320aef7 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Wed, 17 Dec 2025 18:00:28 +0100 Subject: [PATCH 4/5] fix controller --- examples/webhooks/numbers_api/controller.py | 5 ++++- examples/webhooks/sms_api/controller.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/webhooks/numbers_api/controller.py b/examples/webhooks/numbers_api/controller.py index d13569fa..afe5666a 100644 --- a/examples/webhooks/numbers_api/controller.py +++ b/examples/webhooks/numbers_api/controller.py @@ -16,11 +16,14 @@ def numbers_event(self): ensure_valid_authentication = False if ensure_valid_authentication: - webhooks_service.validate_authentication_header( + valid_auth = webhooks_service.validate_authentication_header( headers=headers, json_payload=body_str ) + if not valid_auth: + return Response(status=401) + event = webhooks_service.parse_event(body_str) handle_numbers_event(numbers_event=event, logger=self.logger) diff --git a/examples/webhooks/sms_api/controller.py b/examples/webhooks/sms_api/controller.py index 268c81c6..dbebd1de 100644 --- a/examples/webhooks/sms_api/controller.py +++ b/examples/webhooks/sms_api/controller.py @@ -22,11 +22,14 @@ def sms_event(self): # leave auth disabled here unless SMS callbacks are configured. ensure_valid_authentication = False if ensure_valid_authentication: - webhooks_service.validate_authentication_header( + valid_auth = webhooks_service.validate_authentication_header( headers=headers, json_payload=body_str ) + if not valid_auth: + return Response(status=401) + event = webhooks_service.parse_event(body_str) handle_sms_event(sms_event=event, logger=self.logger) From 3aebd6d859f3cd6e16076ddf18c3d46d1f3dae45 Mon Sep 17 00:00:00 2001 From: Jessica Matsuoka Date: Wed, 17 Dec 2025 19:42:27 +0100 Subject: [PATCH 5/5] update readme --- examples/webhooks/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/webhooks/README.md b/examples/webhooks/README.md index f793862f..8d9674c5 100644 --- a/examples/webhooks/README.md +++ b/examples/webhooks/README.md @@ -33,7 +33,7 @@ This directory contains both the webhook handlers and the server application (`s ``` NUMBERS_WEBHOOKS_SECRET=Your Sinch Numbers Webhook Secret ``` - - SMS controller: To configure the `sms` webhooks secret, contact your account manager to enable SMS callbacks. For more details, refer to + - SMS controller: To configure the `sms` webhooks 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), ```