Skip to content
Closed
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
6 changes: 1 addition & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,7 @@ jobs:
cp sinch-sdk-mockserver/features/numbers/callback-configuration.feature ./tests/e2e/numbers/features/
cp sinch-sdk-mockserver/features/numbers/numbers.feature ./tests/e2e/numbers/features/
cp sinch-sdk-mockserver/features/numbers/webhooks.feature ./tests/e2e/numbers/features/
cp sinch-sdk-mockserver/features/sms/delivery-reports.feature ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/sms/delivery-reports_servicePlanId.feature ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/sms/batches.feature ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/sms/batches_servicePlanId.feature ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/sms/webhooks.feature ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/sms/* ./tests/e2e/sms/features/
cp sinch-sdk-mockserver/features/number-lookup/lookups.feature ./tests/e2e/number-lookup/features/
cp sinch-sdk-mockserver/features/conversation/messages.feature ./tests/e2e/conversation/features/
cp sinch-sdk-mockserver/features/conversation/webhooks-events.feature ./tests/e2e/conversation/features/
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
lcov.info

# E2E features
*.feature
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,26 @@ All notable changes to the **Sinch Python SDK** are documented in this file.

---

## v2.1.0 – 2026-06-05

### SDK

- **[feature]** `HTTPTransport` now automatically retries rate-limited (`HTTP 429`) responses up to 3 times with backoff. When the server sends a `Retry-After` header it is honoured; otherwise an exponential backoff with full jitter is used. If all retries are exhausted the last response is returned to the caller (#158).
- **[dependency]** Set up minimum version for `requests` to `>=2.0.0` to prevent pulling in versions with known vulnerabilities (#152).
- **[fix]** Fixed a race condition in OAuth token creation and renewal under concurrent requests: `TokenManagerBase` now uses a lock with double-checked locking so the initial token is fetched exactly once, and a new `refresh_auth_token(used_token)` deduplicates concurrent renewals by only fetching when the stale token still matches the cached one (#156).
- **[refactor]** `HTTPTransport` now prepares and authenticates requests in `request()`, so the new `send_request(request_data)` receives an already-prepared `HttpRequest` and acts as a pure I/O primitive, simplifying subclassing (#156).
- **[deprecation notice]** `HTTPTransport.send(endpoint)` is deprecated in favour of `send_request(request_data)`; the legacy method still works for backward compatibility, but will be removed in 3.0 (#156).
- **[deprecation notice]** `TokenManagerBase.invalidate_expired_token()` and `handle_invalid_token()` (and the `TokenState.EXPIRED` value) are deprecated and will be removed in 3.0, as token renewal now goes through `refresh_auth_token()` (#156).


### SMS

- **[feature]** SMS Groups API: `create`, `list`, `get`, `update`, `replace`, `delete`, and `list_members` operations, with full model, endpoint, and unit test coverage (see [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md#groups-api)).
- **[feature]** SMS Inbounds API: `get` and `list` operations, with full model, endpoint, and unit test coverage (see [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md#inbounds-api)).
- **[design]** SMS Sinch Events inbound payload models unified with the Inbounds API: `MOTextSinchEvent`, `MOBinarySinchEvent`, `MOMediaSinchEvent`, `MediaBody`, and `MediaItem` removed from `sinch_events`; use `InboundMessage` (and its variants) from `sinch.domains.sms.models.v1.types` instead (see [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md#sms-sinch-events)).

---

## v2.0.1 – 2026-06-02

### SMS
Expand Down
72 changes: 68 additions & 4 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Sinch Python SDK Migration Guide

## 2.0.0
## 2.1.0

This release removes legacy SDK support.

Expand Down Expand Up @@ -205,9 +205,7 @@ The Conversation HTTP API still expects the JSON field **`callback_url`**. In V2

#### Replacement APIs

The SMS domain API access remains the same: `sinch.sms.batches` and `sinch.sms.delivery_reports`. However, the underlying models and method signatures have changed.

Note that `sinch.sms.groups` and `sinch.sms.inbounds` are not supported yet and will be available in future minor versions.
The SMS domain API access remains the same: `sinch.sms.batches`, `sinch.sms.delivery_reports`, `sinch.sms.inbounds` and `sinch.sms.groups`. However, the underlying models and method signatures have changed. See the sections below for the full list of changes: [Batches](#batches-api), [Delivery Reports](#delivery-reports-api), [Groups](#groups-api), [Inbounds](#inbounds-api).

##### Batches API

Expand All @@ -230,6 +228,71 @@ Note that `sinch.sms.groups` and `sinch.sms.inbounds` are not supported yet and
| `get_for_batch()` with `GetSMSDeliveryReportForBatchRequest` | `get()` with `batch_id: str` and optional parameters: `report_type`, `status`, `code`, `client_reference` |
| `get_for_number()` with `GetSMSDeliveryReportForNumberRequest` | `get_for_number()` with `batch_id: str` and `recipient: str` parameters |

##### Groups API

###### Replacement models

| Old class | New class |
|-----------|-----------|
| `sinch.domains.sms.models.groups.requests.CreateSMSGroupRequest` | [`sinch.domains.sms.models.v1.internal.GroupRequest`](sinch/domains/sms/models/v1/internal/group_request.py) |
| `sinch.domains.sms.models.groups.requests.ListSMSGroupRequest` | [`sinch.domains.sms.models.v1.internal.ListGroupsRequest`](sinch/domains/sms/models/v1/internal/list_groups_request.py) |
| `sinch.domains.sms.models.groups.requests.GetSMSGroupRequest` | [`sinch.domains.sms.models.v1.internal.GroupIdRequest`](sinch/domains/sms/models/v1/internal/group_id_request.py) |
| `sinch.domains.sms.models.groups.requests.DeleteSMSGroupRequest` | [`sinch.domains.sms.models.v1.internal.GroupIdRequest`](sinch/domains/sms/models/v1/internal/group_id_request.py) |
| `sinch.domains.sms.models.groups.requests.GetSMSGroupPhoneNumbersRequest` | [`sinch.domains.sms.models.v1.internal.GroupIdRequest`](sinch/domains/sms/models/v1/internal/group_id_request.py) |
| `sinch.domains.sms.models.groups.requests.UpdateSMSGroupRequest` | [`sinch.domains.sms.models.v1.internal.UpdateGroupRequest`](sinch/domains/sms/models/v1/internal/update_group_request.py) |
| `sinch.domains.sms.models.groups.requests.ReplaceSMSGroupPhoneNumbersRequest` | [`sinch.domains.sms.models.v1.internal.ReplaceGroupRequest`](sinch/domains/sms/models/v1/internal/replace_group_request.py) |
| `sinch.domains.sms.models.groups.responses.CreateSMSGroupResponse` | [`sinch.domains.sms.models.v1.response.GroupResponse`](sinch/domains/sms/models/v1/response/group_response.py) |
| `sinch.domains.sms.models.groups.responses.GetSMSGroupResponse` | [`sinch.domains.sms.models.v1.response.GroupResponse`](sinch/domains/sms/models/v1/response/group_response.py) |
| `sinch.domains.sms.models.groups.responses.UpdateSMSGroupResponse` | [`sinch.domains.sms.models.v1.response.GroupResponse`](sinch/domains/sms/models/v1/response/group_response.py) |
| `sinch.domains.sms.models.groups.responses.ReplaceSMSGroupResponse` | [`sinch.domains.sms.models.v1.response.GroupResponse`](sinch/domains/sms/models/v1/response/group_response.py) |
| `sinch.domains.sms.models.groups.responses.SinchListSMSGroupResponse` | [`sinch.domains.sms.models.v1.response.ListGroupsResponse`](sinch/domains/sms/models/v1/response/list_groups_response.py) |
| `sinch.domains.sms.models.groups.responses.SinchGetSMSGroupPhoneNumbersResponse` | [`sinch.domains.sms.models.v1.response.ListGroupMembersResponse`](sinch/domains/sms/models/v1/response/list_group_members_response.py) |
| `sinch.domains.sms.models.groups.responses.SinchDeleteSMSGroupResponse` | `None` (method returns `None`) |

###### Replacement APIs

| Old method | New method in `sms.groups` |
|------------|---------------------------|
| `create()` with `CreateSMSGroupRequest` | `create()` with individual parameters: `name`, `members`, `child_groups`, `auto_update` |
| `list()` with `ListSMSGroupRequest` | `list()` with individual parameters: `page`, `page_size`. Returns **`Paginator[GroupResponse]`** |
| `get()` with `GetSMSGroupRequest` | `get()` with `group_id: str` parameter |
| `update()` with `UpdateSMSGroupRequest` | `update()` with `group_id: str` and optional parameters: `add`, `remove`, `name`, `add_from_group`, `remove_from_group`, `auto_update` |
| `replace()` with `ReplaceSMSGroupPhoneNumbersRequest` | `replace()` with `group_id: str` and optional parameters: `name`, `members`, `child_groups`, `auto_update` |
| `delete()` with `DeleteSMSGroupRequest` | `delete()` with `group_id: str` parameter |
| `get_phone_numbers()` / phone number listing | `list_members()` with `group_id: str`. Returns **`Paginator[str]`** |

##### Inbounds API

###### Replacement models

| Old class | New class |
|-----------|-----------|
| `sinch.domains.sms.models.inbounds.requests.ListSMSInboundMessageRequest` | [`sinch.domains.sms.models.v1.internal.ListInboundsRequest`](sinch/domains/sms/models/v1/internal/list_inbounds_request.py) |
| `sinch.domains.sms.models.inbounds.requests.GetSMSInboundMessageRequest` | [`sinch.domains.sms.models.v1.internal.InboundIdRequest`](sinch/domains/sms/models/v1/internal/inbound_id_request.py) |
| `sinch.domains.sms.models.inbounds.responses.SinchListInboundMessagesResponse` | [`sinch.domains.sms.models.v1.internal.ListInboundsResponse`](sinch/domains/sms/models/v1/internal/list_inbounds_response.py) |
| `sinch.domains.sms.models.inbounds.responses.GetInboundMessagesResponse` | [`sinch.domains.sms.models.v1.types.InboundMessage`](sinch/domains/sms/models/v1/types/inbound_message.py) (Union of `MOTextMessage`, `MOBinaryMessage`, `MOMediaMessage`) |

###### Replacement APIs

| Old method | New method in `sms.inbounds` |
|------------|------------------------------|
| `list()` with `ListSMSInboundMessageRequest` | `list()` with individual parameters: `page`, `page_size`, `to`, `start_date`, `end_date`, `client_reference`. Returns **`Paginator[InboundMessage]`** |
| `get()` with `GetSMSInboundMessageRequest` | `get()` with `inbound_id: str` parameter |

##### SMS Sinch Events

The inbound payload models in `sinch_events` have been unified with the Inbounds API models. The following classes have been removed:

| Removed class | Replacement |
|---------------|-------------|
| `sinch.domains.sms.sinch_events.v1.events.MOTextSinchEvent` | [`sinch.domains.sms.models.v1.shared.MOTextMessage`](sinch/domains/sms/models/v1/shared/mo_text_message.py) |
| `sinch.domains.sms.sinch_events.v1.events.MOBinarySinchEvent` | [`sinch.domains.sms.models.v1.shared.MOBinaryMessage`](sinch/domains/sms/models/v1/shared/mo_binary_message.py) |
| `sinch.domains.sms.sinch_events.v1.events.MOMediaSinchEvent` | [`sinch.domains.sms.models.v1.shared.MOMediaMessage`](sinch/domains/sms/models/v1/shared/mo_media_message.py) |
| `sinch.domains.sms.sinch_events.v1.events.MediaBody` | Embedded in `MOMediaMessage` |
| `sinch.domains.sms.sinch_events.v1.events.MediaItem` | Embedded in `MOMediaMessage` |

`IncomingSMSSinchEvent` is now a type alias for [`InboundMessage`](sinch/domains/sms/models/v1/types/inbound_message.py) (discriminated union of `MOTextMessage`, `MOBinaryMessage`, `MOMediaMessage`). Code that previously type-checked against `MOTextSinchEvent` and siblings should switch to their `MO*Message` equivalents.

---

### [`Numbers` (Virtual Numbers)](https://github.com/sinch/sinch-sdk-python/tree/main/sinch/domains/numbers)
Expand All @@ -244,6 +307,7 @@ Note that `sinch.sms.groups` and `sinch.sms.inbounds` are not supported yet and

##### Replacement models


| Old class | New class |
|-----------|-----------|
| `UpdateNumbersCallbackConfigurationRequest` | `UpdateEventDestinationRequest` |
Expand Down
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ The following example replaces the default `requests` backend with `httpx` and r
import httpx
from sinch import SinchClient
from sinch.core.ports.http_transport import HTTPTransport
from sinch.core.endpoint import HTTPEndpoint
from sinch.core.models.http_request import HttpRequest
from sinch.core.models.http_response import HTTPResponse


Expand All @@ -204,9 +204,7 @@ class MyHTTPImplementation(HTTPTransport):
proxy=f"http://{proxy_user}:{proxy_password}@{proxy_url}"
)

def send(self, endpoint: HTTPEndpoint) -> HTTPResponse:
request_data = self.prepare_request(endpoint)
request_data = self.authenticate(endpoint, request_data)
def send_request(self, request_data: HttpRequest) -> HTTPResponse:

body = request_data.request_body
response = self.http_client.request(
Expand Down
26 changes: 26 additions & 0 deletions examples/snippets/sms/groups/create/snippet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Sinch Python Snippet

This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""

import os

from dotenv import load_dotenv

from sinch import SinchClient

load_dotenv()

sinch_client = SinchClient(
project_id=os.environ.get("SINCH_PROJECT_ID") or "MY_PROJECT_ID",
key_id=os.environ.get("SINCH_KEY_ID") or "MY_KEY_ID",
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET",
sms_region=os.environ.get("SINCH_SMS_REGION") or "MY_SMS_REGION"
)

response = sinch_client.sms.groups.create(
name="Sinch Python SDK group", members=["+1234567890", "+1987654321"]
)

print(f"Group created:\n{response}")
27 changes: 27 additions & 0 deletions examples/snippets/sms/groups/delete/snippet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""
Sinch Python Snippet

This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""

import os

from dotenv import load_dotenv

from sinch import SinchClient

load_dotenv()

sinch_client = SinchClient(
project_id=os.environ.get("SINCH_PROJECT_ID") or "MY_PROJECT_ID",
key_id=os.environ.get("SINCH_KEY_ID") or "MY_KEY_ID",
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET",
sms_region=os.environ.get("SINCH_SMS_REGION") or "MY_SMS_REGION"
)

# The ID of the group to delete
group_id = "GROUP_ID"

sinch_client.sms.groups.delete(group_id=group_id)

print(f"Group {group_id} deleted")
29 changes: 29 additions & 0 deletions examples/snippets/sms/groups/get/snippet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""
Sinch Python Snippet

This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""

import os

from dotenv import load_dotenv

from sinch import SinchClient

load_dotenv()

sinch_client = SinchClient(
project_id=os.environ.get("SINCH_PROJECT_ID") or "MY_PROJECT_ID",
key_id=os.environ.get("SINCH_KEY_ID") or "MY_KEY_ID",
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET",
sms_region=os.environ.get("SINCH_SMS_REGION") or "MY_SMS_REGION"
)

# The ID of the group to retrieve
GROUP_ID = "GROUP_ID"

response = sinch_client.sms.groups.get(
group_id=GROUP_ID
)

print(f"Group details:\n{response}")
26 changes: 26 additions & 0 deletions examples/snippets/sms/groups/list/snippet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Sinch Python Snippet

This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""

import os

from dotenv import load_dotenv

from sinch import SinchClient

load_dotenv()

sinch_client = SinchClient(
project_id=os.environ.get("SINCH_PROJECT_ID") or "MY_PROJECT_ID",
key_id=os.environ.get("SINCH_KEY_ID") or "MY_KEY_ID",
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET",
sms_region=os.environ.get("SINCH_SMS_REGION") or "MY_SMS_REGION"
)

groups = sinch_client.sms.groups.list()

print("List of groups:\n")
for group in groups.iterator():
print(group)
30 changes: 30 additions & 0 deletions examples/snippets/sms/groups/list_members/snippet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Sinch Python Snippet

This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""

import os
from typing import List

from dotenv import load_dotenv

from sinch import SinchClient

load_dotenv()

sinch_client = SinchClient(
project_id=os.environ.get("SINCH_PROJECT_ID") or "MY_PROJECT_ID",
key_id=os.environ.get("SINCH_KEY_ID") or "MY_KEY_ID",
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET",
sms_region=os.environ.get("SINCH_SMS_REGION") or "MY_SMS_REGION"
)

# The ID of the group to list members for
group_id = "GROUP_ID"

members = sinch_client.sms.groups.list_members(group_id=group_id)

print("List of members:\n")
for member in members.iterator():
print(member)
30 changes: 30 additions & 0 deletions examples/snippets/sms/groups/replace/snippet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Sinch Python Snippet

This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""

import os

from dotenv import load_dotenv

from sinch import SinchClient

load_dotenv()

sinch_client = SinchClient(
project_id=os.environ.get("SINCH_PROJECT_ID") or "MY_PROJECT_ID",
key_id=os.environ.get("SINCH_KEY_ID") or "MY_KEY_ID",
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET",
sms_region=os.environ.get("SINCH_SMS_REGION") or "MY_SMS_REGION"
)

# The ID of the group to replace
group_id = "GROUP_ID"

response = sinch_client.sms.groups.replace(
group_id=group_id,
members=["+1234567890", "+1987654321"],
)

print(f"Group replaced:\n{response}")
32 changes: 32 additions & 0 deletions examples/snippets/sms/groups/update/snippet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
Sinch Python Snippet

This snippet is available at https://github.com/sinch/sinch-sdk-python/tree/main/examples/snippets
"""

import os

from dotenv import load_dotenv

from sinch import SinchClient

load_dotenv()

sinch_client = SinchClient(
project_id=os.environ.get("SINCH_PROJECT_ID") or "MY_PROJECT_ID",
key_id=os.environ.get("SINCH_KEY_ID") or "MY_KEY_ID",
key_secret=os.environ.get("SINCH_KEY_SECRET") or "MY_KEY_SECRET",
sms_region=os.environ.get("SINCH_SMS_REGION") or "MY_SMS_REGION"
)

# The ID of the group to update
group_id = "GROUP_ID"

response = sinch_client.sms.groups.update(
group_id=group_id,
add=["+1234567890"],
remove=["+1987654321"],
name="Renamed Group",
)

print(f"Group updated:\n{response}")
Loading
Loading