Skip to content
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,7 @@ poetry.lock
# .DS_Store files
.DS_Store

qodana.yaml
qodana.yaml

# AI stuff
.claude
32 changes: 32 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,38 @@ The Conversation domain API access remains `sinch_client.conversation`; message
| `list()` with `ListConversationMessagesRequest` | In Progress |
| — | **New in V2:** `update()` with `message_id`, `metadata`, and optional `messages_source`|

##### Replacement APIs / attributes

| Old | New |
|-----|-----|
| `sinch_client.conversation.webhook` (REST: create, list, get, update, delete webhooks; models under `sinch.domains.conversation.models.webhook`, e.g. `CreateConversationWebhookRequest`, `SinchListWebhooksResponse`) | **Not available in V2.** The Conversation client only exposes `messages` and `sinch_events`; More features are planned for future releases. To validate and parse inbound Sinch Events payloads, use `sinch_client.conversation.sinch_events(callback_secret)`—see **Sinch Events** below. |

#### Sinch Events (Event Destinations payload models and package path)

| Old | New |
|-----|-----|
| — _(N/A)_ | `sinch.domains.conversation.models.v1.sinch_events` (package path for inbound payload models) |
| — | [`ConversationSinchEvent`](sinch/domains/conversation/sinch_events/v1/conversation_sinch_event.py) (handler: signature validation and `parse_event`) |
| — | `ConversationSinchEventPayload`, `ConversationSinchEventBase`, and concrete event types (e.g. `MessageInboundEvent`, `MessageDeliveryReceiptEvent`, `MessageSubmitEvent`) |

To obtain a Conversation Sinch Events handler: `sinch_client.conversation.sinch_events(callback_secret)` returns a [`ConversationSinchEvent`](sinch/domains/conversation/sinch_events/v1/conversation_sinch_event.py) instance; `handler.parse_event(request_body)` returns a `ConversationSinchEventPayload`.

```python
# New
handler = sinch_client.conversation.sinch_events("your_callback_secret")
event = handler.parse_event(request_body)
```

#### Request and response fields: callback URL → event destination target

| | Old | New |
|---|-----|-----|
| **Messages (`send`)** | `sinch.domains.conversation.models.message.requests.SendConversationMessageRequest` field `callback_url` | [`SendMessageRequest`](sinch/domains/conversation/models/v1/messages/internal/request/send_message_request.py) field `event_destination_target` |
| **Messages (methods)** | `ConversationMessage.send(..., callback_url=...)` | `sinch_client.conversation.messages.send()`, `send_text_message()`, and other `send_*_message()` methods with `event_destination_target=...` |
| **Send event** | `sinch.domains.conversation.models.event.requests.SendConversationEventRequest` field `callback_url` | `event_destination_target` on the V2 send-event request model when that API is exposed |

The Conversation HTTP API still expects the JSON field **`callback_url`**. In V2, use the Python parameter / model field `event_destination_target`; it is serialized as `callback_url` on the wire (same pattern as other domains, e.g. SMS).

<br>

### [`SMS`](https://github.com/sinch/sinch-sdk-python/tree/main/sinch/domains/sms)
Expand Down
Comment thread
JPPortier marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ def conversation_event(self):
headers = dict(request.headers)
raw_body = getattr(request, "raw_body", None) or b""

webhooks_service = self.sinch_client.conversation.webhooks()
event = webhooks_service.parse_event(raw_body, headers)
sinch_events_service = self.sinch_client.conversation.sinch_events()
event = sinch_events_service.parse_event(raw_body, headers)
handle_conversation_event(
event=event,
logger=self.logger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
Uses channel identity (SMS + phone number) only; app is in DISPATCH mode.
"""

from sinch.domains.conversation.models.v1.webhooks import MessageInboundEvent
from sinch.domains.conversation.models.v1.sinch_events import MessageInboundEvent


def handle_conversation_event(event, logger, sinch_client):
"""Webhook entry: handle only MESSAGE_INBOUND; delegate to inbound handler."""
"""Sinch Event entry: handle only MESSAGE_INBOUND; delegate to inbound handler."""
if not isinstance(event, MessageInboundEvent):
return
_handle_message_inbound(event, logger, sinch_client)
Expand Down
2 changes: 1 addition & 1 deletion examples/sinch_events/README.md
Comment thread
JPPortier marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ This directory contains both the Event handlers and the server application (`ser
```
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)):
- Conversation controller: Set the Sinch Event secret you configured for your Conversation app event destination (see [Conversation API callbacks](https://developers.sinch.com/docs/conversation/callbacks)):
```
CONVERSATION_SINCH_EVENT_SECRET=Your Conversation Sinch Event Secret
```
Expand Down
6 changes: 3 additions & 3 deletions examples/sinch_events/conversation_api/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ 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(
sinch_events_service = self.sinch_client.conversation.sinch_events(
self.sinch_event_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(
valid = sinch_events_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)
event = sinch_events_service.parse_event(raw_body, headers)
handle_conversation_event(event=event, logger=self.logger)

return Response(status=200)
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from sinch.domains.conversation.models.v1.webhooks import (
ConversationWebhookEventBase,
from sinch.domains.conversation.models.v1.sinch_events import (
ConversationSinchEventBase,
MessageDeliveryReceiptEvent,
MessageInboundEvent,
MessageSubmitEvent,
)


def handle_conversation_event(event: ConversationWebhookEventBase, logger):
def handle_conversation_event(event: ConversationSinchEventBase, logger):
"""
Dispatch a Conversation webhook event to the appropriate handler by trigger type.
Dispatch a Conversation Sinch Event to the appropriate handler by trigger type.

:param event: Parsed webhook event (MessageDeliveryReceiptEvent, MessageInboundEvent, etc.).
:param event: Parsed Sinch Event (MessageDeliveryReceiptEvent, MessageInboundEvent, etc.).
:param logger: Logger instance for output.
"""
if isinstance(event, MessageInboundEvent):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def validate_signature_header(
return False

expected_signature = compute_hmac_signature(body, callback_secret)
return signature == expected_signature
return hmac.compare_digest(signature, expected_signature)


def normalize_headers(headers: Dict[str, str]) -> Dict[str, str]:
Expand Down
Loading
Loading