-
Notifications
You must be signed in to change notification settings - Fork 3
DEVEXP-788: SMS API - Webhooks Quickstart #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # 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 = 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| # 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. | ||
|
|
||
| - Server Port: | ||
| Define the port your server will listen to on (default: 3001): | ||
| ``` | ||
| SERVER_PORT=3001 | ||
| ``` | ||
|
|
||
| - Controller Settings | ||
asein-sinch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - 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 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 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 | ||
|
|
||
| ### Running the server application | ||
|
|
||
| 1. Navigate to the webhooks' directory: | ||
| ``` | ||
| cd examples/webhooks | ||
| ``` | ||
|
|
||
| 2. Install the project dependencies: | ||
| ``` bash | ||
| poetry install | ||
| ``` | ||
|
|
||
| 3. 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 `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. | ||
|
|
||
| > **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. | ||
JPPortier marked this conversation as resolved.
Show resolved
Hide resolved
|
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| 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) | ||
|
|
||
| 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: | ||
| return Response(status=401) | ||
|
|
||
| event = webhooks_service.parse_event(body_str) | ||
|
|
||
| handle_numbers_event(numbers_event=event, logger=self.logger) | ||
|
|
||
| return Response(status=200) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)}') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| [tool.poetry] | ||
| name = "sinch-sdk-python-quickstart-server" | ||
| version = "0.1.0" | ||
| description = "Sinch SDK Python Quickstart Webhooks Server" | ||
| readme = "README.md" | ||
| package-mode = false | ||
|
|
||
| [tool.poetry.dependencies] | ||
| python = "^3.9" | ||
| python-dotenv = "^1.0.0" | ||
| flask = "^3.0.0" | ||
| # TODO: Uncomment once v2.0 is released | ||
| # sinch = "^2.0.0" | ||
|
|
||
| [build-system] | ||
| requires = ["poetry-core"] | ||
| build-backend = "poetry.core.masonry.api" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| 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') | ||
| sms_webhooks_secret = config.get('SMS_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, sms_webhooks_secret) | ||
|
|
||
|
|
||
| # 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| 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 | ||
| """ | ||
| return SinchClient() |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| 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): | ||
| 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) | ||
|
|
||
| # 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 | ||
asein-sinch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if ensure_valid_authentication: | ||
| 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) | ||
|
|
||
| return Response(status=200) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)}') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.