-
Notifications
You must be signed in to change notification settings - Fork 3
DEVEXP-1228: Conversation Getting Started (SMS) #125
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
matsk-sinch
merged 3 commits into
v2.0
from
DEVEXP-1228_Conversation-Getting-Started-SMS
Mar 4, 2026
Merged
Changes from all commits
Commits
Show all changes
3 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
16 changes: 16 additions & 0 deletions
16
examples/getting-started/conversation/send_handle_incoming_sms/.env.example
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,16 @@ | ||
| # Sinch credentials (from dashboard.sinch.com → Access Keys) | ||
| SINCH_PROJECT_ID= | ||
| SINCH_KEY_ID= | ||
| SINCH_KEY_SECRET= | ||
|
|
||
| # Conversation API: existing app (already created and configured for SMS). | ||
| # SINCH_CONVERSATION_REGION is required. | ||
| # Set it to the same region as the one your app was created in (e.g. eu). | ||
| CONVERSATION_APP_ID= | ||
| SINCH_CONVERSATION_REGION= | ||
|
|
||
| # Webhook secret (set when configuring the callback in Sinch dashboard) | ||
| CONVERSATION_WEBHOOKS_SECRET= | ||
|
|
||
| # Server | ||
| SERVER_PORT=3001 |
100 changes: 100 additions & 0 deletions
100
examples/getting-started/conversation/send_handle_incoming_sms/README.md
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,100 @@ | ||
| # Getting Started: Receive Mobile-originated (MO) SMS and send Mobile-terminated (MT) reply (Conversation API) | ||
|
|
||
|
|
||
| This directory contains a small server built with the [Sinch Python SDK](https://github.com/sinch/sinch-sdk-python) | ||
| that receives mobile-originated (MO) SMS on your Sinch number and sends a mobile-terminated (MT) SMS back | ||
| to the same phone. The reply echoes the incoming text (e.g. *"Your message said: <content of MO>"*) so you can | ||
| see that the MO was received and processed. | ||
|
|
||
|
|
||
|
|
||
| ## Requirements | ||
|
|
||
| - [Python 3.9+](https://www.python.org/) | ||
| - [Flask](https://flask.palletsprojects.com/en/stable/) | ||
| - [Sinch account](https://dashboard.sinch.com/) | ||
| - An existing Conversation API app configured for SMS (with a Sinch number) | ||
| - [ngrok](https://ngrok.com/docs) (or similar) to expose your local server | ||
| - [Poetry](https://python-poetry.org/) | ||
|
|
||
| ## Configuration | ||
|
|
||
| 1. **Environment variables** | ||
| Copy [.env.example](.env.example) to `.env` in this directory, then set your credentials and app settings. | ||
|
|
||
| - Sinch credentials (from the Sinch dashboard, Access Keys): | ||
| ``` | ||
| SINCH_PROJECT_ID=your_project_id | ||
| SINCH_KEY_ID=your_key_id | ||
| SINCH_KEY_SECRET=your_key_secret | ||
| ``` | ||
|
|
||
| - Conversation API app (existing app, already configured for SMS). Set `SINCH_CONVERSATION_REGION` to the same region as the one your app was created in (e.g. `eu`): | ||
| ``` | ||
| CONVERSATION_APP_ID=your_conversation_app_id | ||
| SINCH_CONVERSATION_REGION= | ||
| ``` | ||
|
|
||
| - Webhook secret (the value you set when configuring the callback URL for this app). | ||
| See [Conversation API callbacks](https://developers.sinch.com/docs/conversation/callbacks): | ||
| ``` | ||
| CONVERSATION_WEBHOOKS_SECRET=your_webhook_secret | ||
| ``` | ||
|
|
||
| - Server port (optional; default 3001): | ||
| ``` | ||
| SERVER_PORT=3001 | ||
| ``` | ||
|
|
||
| 2. **Install dependencies** | ||
| From this directory: | ||
| ```bash | ||
| poetry install | ||
| ``` | ||
| Install the Sinch SDK from the **repository root**: `pip install -e .` (recommended when developing from this repo). | ||
| Alternatively, install with pip: `flask`, `python-dotenv`, and `sinch` (e.g. from PyPI). | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Running the server | ||
|
|
||
| 1. Navigate to this directory: | ||
| ``` | ||
| cd examples/getting-started/conversation/send_handle_incoming_sms | ||
| ``` | ||
|
|
||
|
|
||
| 2. Start the server: | ||
| ```bash | ||
| poetry run python server.py | ||
| ``` | ||
| Or run it directly: | ||
| ```bash | ||
| python server.py | ||
| ``` | ||
|
|
||
| The server listens on the port set in your `.env` file (default: 3001). | ||
|
|
||
| ### Exposing the server with ngrok | ||
|
|
||
| To receive webhooks on your machine, expose the server with a tunnel (e.g. ngrok). | ||
|
|
||
|
|
||
| ```bash | ||
| ngrok http 3001 | ||
| ``` | ||
|
|
||
| You will see output similar to: | ||
| ``` | ||
| Forwarding https://abc123.ngrok-free.app -> http://localhost:3001 | ||
| ``` | ||
|
|
||
| Use the **HTTPS** URL when configuring the callback: | ||
| `https://<your-ngrok-host>/ConversationEvent` | ||
|
|
||
| Configure this callback URL (and the webhook secret) in the Sinch dashboard for your Conversation API app. | ||
| The webhook secret must match `CONVERSATION_WEBHOOKS_SECRET` in your `.env`. | ||
|
|
||
| ### Sending an SMS to your Sinch number | ||
|
|
||
| Send an SMS from your phone to the **Sinch number** linked to your Conversation API app. You should receive the echo reply on your phone. |
36 changes: 36 additions & 0 deletions
36
examples/getting-started/conversation/send_handle_incoming_sms/controller.py
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,36 @@ | ||
| from flask import request, Response | ||
| from server_business_logic import handle_conversation_event | ||
|
|
||
|
|
||
| class ConversationController: | ||
| def __init__(self, sinch_client, webhooks_secret, app_id): | ||
| self.sinch_client = sinch_client | ||
| self.webhooks_secret = webhooks_secret | ||
| self.app_id = app_id | ||
| self.logger = self.sinch_client.configuration.logger | ||
|
|
||
| def conversation_event(self): | ||
| headers = dict(request.headers) | ||
| raw_body = getattr(request, "raw_body", None) or b"" | ||
|
|
||
| webhooks_service = self.sinch_client.conversation.webhooks(self.webhooks_secret) | ||
|
|
||
| # Set to True to enforce signature validation (recommended in production) | ||
| ensure_valid_signature = False | ||
| if ensure_valid_signature: | ||
| valid = webhooks_service.validate_authentication_header( | ||
| headers=headers, | ||
| json_payload=raw_body, | ||
| ) | ||
| if not valid: | ||
| return Response(status=401) | ||
|
|
||
| event = webhooks_service.parse_event(raw_body, headers) | ||
| handle_conversation_event( | ||
| event=event, | ||
| logger=self.logger, | ||
| sinch_client=self.sinch_client, | ||
| app_id=self.app_id, | ||
| ) | ||
|
|
||
| return Response(status=200) | ||
15 changes: 15 additions & 0 deletions
15
examples/getting-started/conversation/send_handle_incoming_sms/pyproject.toml
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,15 @@ | ||
| [tool.poetry] | ||
| name = "sinch-getting-started-send-handle-incoming-sms" | ||
| version = "0.1.0" | ||
| description = "Getting Started: send and handle incoming SMS with Conversation API (DISPATCH, channel identity)" | ||
| readme = "README.md" | ||
| package-mode = false | ||
|
|
||
| [tool.poetry.dependencies] | ||
| python = "^3.9" | ||
| python-dotenv = "^1.0.0" | ||
| flask = "^3.0.0" | ||
|
|
||
| [build-system] | ||
| requires = ["poetry-core"] | ||
| build-backend = "poetry.core.masonry.api" |
62 changes: 62 additions & 0 deletions
62
examples/getting-started/conversation/send_handle_incoming_sms/server.py
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,62 @@ | ||
| import logging | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| from flask import Flask, request | ||
| from dotenv import dotenv_values | ||
|
|
||
| from sinch import SinchClient | ||
| from controller import ConversationController | ||
|
|
||
| app = Flask(__name__) | ||
|
|
||
|
|
||
| def load_config(): | ||
| current_dir = Path(__file__).resolve().parent | ||
| env_file = current_dir / ".env" | ||
| if not env_file.exists(): | ||
| raise FileNotFoundError(f"Missing .env in {current_dir}. Copy from .env.example.") | ||
| return dict(dotenv_values(env_file)) | ||
|
|
||
|
|
||
| config = load_config() | ||
| port = int(config.get("SERVER_PORT") or "3001") | ||
| app_id = config.get("CONVERSATION_APP_ID") or "" | ||
| webhooks_secret = config.get("CONVERSATION_WEBHOOKS_SECRET") or "" | ||
| conversation_region = (config.get("SINCH_CONVERSATION_REGION") or "").strip() | ||
| if not conversation_region: | ||
| raise ValueError( | ||
| "SINCH_CONVERSATION_REGION is required in .env to provide all parameters needed for Conversation API requests. " | ||
| "Set it to the same region as the one your Conversation API app was created in (e.g. eu)." | ||
| ) | ||
matsk-sinch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| sinch_client = SinchClient( | ||
| project_id=config.get("SINCH_PROJECT_ID", ""), | ||
| key_id=config.get("SINCH_KEY_ID", ""), | ||
| key_secret=config.get("SINCH_KEY_SECRET", ""), | ||
| conversation_region=conversation_region, | ||
| ) | ||
| logging.basicConfig() | ||
| sinch_client.configuration.logger.setLevel(logging.INFO) | ||
|
|
||
| conversation_controller = ConversationController( | ||
| sinch_client, webhooks_secret, app_id | ||
| ) | ||
|
|
||
|
|
||
| @app.before_request | ||
| def before_request(): | ||
| request.raw_body = request.get_data() | ||
|
|
||
|
|
||
| app.add_url_rule( | ||
| "/ConversationEvent", | ||
| methods=["POST"], | ||
| view_func=conversation_controller.conversation_event, | ||
| ) | ||
|
|
||
| if __name__ == "__main__": | ||
| print("Getting Started: MO SMS → MT reply (Conversation API, DISPATCH, channel identity)") | ||
| print(f"App ID: {app_id or '(set CONVERSATION_APP_ID in .env)'}") | ||
| print(f"Listening on port {port}. Expose with: ngrok http {port}") | ||
| app.run(port=port) | ||
52 changes: 52 additions & 0 deletions
52
examples/getting-started/conversation/send_handle_incoming_sms/server_business_logic.py
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,52 @@ | ||
| """ | ||
| On inbound SMS (MO), send a reply (MT) to the same number: "Your message said: <text>". | ||
| Uses channel identity (SMS + phone number) only; app is in DISPATCH mode. | ||
| """ | ||
|
|
||
| from sinch.domains.conversation.models.v1.webhooks import MessageInboundEvent | ||
|
|
||
|
|
||
| def handle_conversation_event(event, logger, sinch_client, app_id): | ||
| """Webhook entry: handle only MESSAGE_INBOUND; delegate to inbound handler.""" | ||
| if not isinstance(event, MessageInboundEvent): | ||
| return | ||
| _handle_message_inbound(event, logger, sinch_client, app_id) | ||
|
|
||
|
|
||
| def _get_mo_text(event: MessageInboundEvent) -> str: | ||
| """Return the inbound message text, or a short placeholder if none.""" | ||
| msg = event.message | ||
| contact_msg = msg.contact_message | ||
| if getattr(contact_msg, "text_message", None): | ||
| return contact_msg.text_message.text or "(empty)" | ||
| return "(no text content)" | ||
|
|
||
|
|
||
| def _handle_message_inbound(event: MessageInboundEvent, logger, sinch_client, app_id): | ||
| """Parse MO, then send MT echo to the same number via Conversation API.""" | ||
| msg = event.message | ||
| channel_identity = msg.channel_identity | ||
| if not channel_identity: | ||
| logger.warning("MESSAGE_INBOUND with no channel_identity") | ||
| return | ||
|
|
||
| identity = channel_identity.identity | ||
| mo_text = _get_mo_text(event) | ||
| logger.info("MO SMS from %s: %s", identity, mo_text) | ||
|
|
||
| if not app_id: | ||
| logger.warning("CONVERSATION_APP_ID not set; skipping MT reply.") | ||
| return | ||
|
|
||
| reply_text = f"Your message said: {mo_text}" | ||
| response = sinch_client.conversation.messages.send_text_message( | ||
| app_id=app_id, | ||
| text=reply_text, | ||
| recipient_identities=[{"channel": "SMS", "identity": identity}], | ||
| ) | ||
| logger.info("MT reply sent to %s (channel identity): %s", identity, reply_text[:60]) | ||
| logger.debug( | ||
| "Response: message_id=%s accepted_time=%s", | ||
| response.message_id, | ||
| response.accepted_time | ||
| ) |
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.