Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
2 changes: 1 addition & 1 deletion examples/README.md → examples/snippets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
File renamed without changes.
9 changes: 9 additions & 0 deletions examples/webhooks/.env.example
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
105 changes: 105 additions & 0 deletions examples/webhooks/README.md
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
- 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.
Empty file.
31 changes: 31 additions & 0 deletions examples/webhooks/numbers_api/controller.py
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)
11 changes: 11 additions & 0 deletions examples/webhooks/numbers_api/server_business_logic.py
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)}')
17 changes: 17 additions & 0 deletions examples/webhooks/pyproject.toml
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"
41 changes: 41 additions & 0 deletions examples/webhooks/server.py
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)
34 changes: 34 additions & 0 deletions examples/webhooks/sinch_client_helper.py
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.
37 changes: 37 additions & 0 deletions examples/webhooks/sms_api/controller.py
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
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)
11 changes: 11 additions & 0 deletions examples/webhooks/sms_api/server_business_logic.py
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)}')
17 changes: 15 additions & 2 deletions sinch/core/clients/sinch_client_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 0 additions & 22 deletions sinch/domains/sms/models/batches/__init__.py

This file was deleted.

Loading