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
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
build/

# Virtual environments
.venv/
venv/
env/

# Tooling caches
.pytest_cache/
.ruff_cache/
.mypy_cache/

# Claude local files
.claude/

# OS
.DS_Store
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.1.0] - 2026-04-22

### Changed
- Webhook event types are now namespaced (e.g. `message.delivery`,
`engagement.click`, `unsubscribe.list_unsubscribe`). The SDK continues to
pass `events` strings through unchanged, so callers just need to switch to
the new names. Docs and tests updated accordingly.
- `webhooks.update()` now sends the webhook URL as `url` instead of `target`
to match the updated API.

### Deprecated
- `webhooks.update(..., target=...)` — use `url=` instead. `target` still
works (mapped to `url` in the request body) but emits a
`DeprecationWarning`. Passing both raises `TypeError`.

## [1.0.0] - 2026-04-20

### Changed
Expand Down Expand Up @@ -114,7 +129,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`ValidationError`, `NotFoundError`, `ConflictError`, `BadRequestError`,
`ServerError`)

[Unreleased]: https://github.com/lettr/lettr-python/compare/v1.0.0...HEAD
[Unreleased]: https://github.com/lettr/lettr-python/compare/v1.1.0...HEAD
[1.1.0]: https://github.com/lettr/lettr-python/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/lettr/lettr-python/compare/v0.3.0...v1.0.0
[0.3.0]: https://github.com/lettr/lettr-python/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/lettr/lettr-python/compare/v0.1.0...v0.2.0
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ webhook = client.webhooks.create(
url="https://example.com/webhook",
auth_type="none",
events_mode="selected",
events=["delivery", "bounce", "spam_complaint"],
events=["message.delivery", "message.bounce", "message.spam_complaint"],
)

# Create with authentication
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "lettr"
version = "1.0.0"
version = "1.1.0"
description = "Official Python SDK for the Lettr Email API"
readme = "README.md"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion src/lettr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
)
from .resources import Domains, Emails, Projects, Templates, Webhooks

__version__ = "1.0.0"
__version__ = "1.1.0"

__all__ = [
# Client
Expand Down
Binary file removed src/lettr/__pycache__/__init__.cpython-313.pyc
Binary file not shown.
Binary file removed src/lettr/__pycache__/_client.cpython-313.pyc
Binary file not shown.
Binary file removed src/lettr/__pycache__/_exceptions.cpython-313.pyc
Binary file not shown.
Binary file removed src/lettr/__pycache__/_types.cpython-313.pyc
Binary file not shown.
4 changes: 2 additions & 2 deletions src/lettr/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __init__(
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "lettr-python/1.0.0",
"User-Agent": "lettr-python/1.1.0",
},
)

Expand Down Expand Up @@ -98,7 +98,7 @@ def get_no_auth(self, path: str, *, params: dict[str, Any] | None = None) -> Any
timeout=self._timeout,
headers={
"Accept": "application/json",
"User-Agent": "lettr-python/1.0.0",
"User-Agent": "lettr-python/1.1.0",
},
)
except httpx.HTTPError as exc:
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
28 changes: 23 additions & 5 deletions src/lettr/resources/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import builtins
import warnings
from typing import Any

from .._client import ApiClient
Expand Down Expand Up @@ -93,7 +94,9 @@ def create(
oauth_client_id: OAuth2 client ID (when ``auth_type="oauth2"``).
oauth_client_secret: OAuth2 client secret (when ``auth_type="oauth2"``).
oauth_token_url: OAuth2 token URL (when ``auth_type="oauth2"``).
events: Event types to receive (when ``events_mode="selected"``).
events: Namespaced event types to receive (when
``events_mode="selected"``), e.g. ``["message.delivery",
"engagement.click"]``.

Returns:
A :class:`Webhook` with the created webhook details.
Expand Down Expand Up @@ -129,6 +132,7 @@ def update(
webhook_id: str,
*,
name: str | None = None,
url: str | None = None,
target: str | None = None,
auth_type: str | None = None,
auth_username: str | None = None,
Expand All @@ -146,14 +150,18 @@ def update(
Args:
webhook_id: The webhook ID to update.
name: New webhook name.
target: New webhook URL.
url: New webhook URL.
target: Deprecated alias for ``url``. Still accepted but emits a
:class:`DeprecationWarning`; passing both ``url`` and
``target`` raises :class:`TypeError`.
auth_type: New authentication type.
auth_username: New basic auth username.
auth_password: New basic auth password.
oauth_token_url: New OAuth2 token URL.
oauth_client_id: New OAuth2 client ID.
oauth_client_secret: New OAuth2 client secret.
events: New event types to receive.
events: New namespaced event types to receive, e.g.
``["message.delivery", "engagement.click"]``.
active: Enable or disable the webhook.

Returns:
Expand All @@ -163,11 +171,21 @@ def update(
NotFoundError: If the webhook is not found.
ValidationError: If validation fails.
"""
if target is not None:
if url is not None:
raise TypeError("Pass either `url` or `target` (deprecated), not both.")
warnings.warn(
"The `target` parameter is deprecated; use `url` instead.",
DeprecationWarning,
stacklevel=2,
)
url = target

payload: dict[str, Any] = {}
if name is not None:
payload["name"] = name
if target is not None:
payload["target"] = target
if url is not None:
payload["url"] = url
if auth_type is not None:
payload["auth_type"] = auth_type
if auth_username is not None:
Expand Down
Binary file removed tests/__pycache__/__init__.cpython-313.pyc
Binary file not shown.
Binary file removed tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
39 changes: 33 additions & 6 deletions tests/test_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def webhooks(mock_client: MagicMock) -> Webhooks:
"enabled": True,
"auth_type": "none",
"has_auth_credentials": False,
"event_types": ["delivery", "bounce"],
"event_types": ["message.delivery", "message.bounce"],
"last_successful_at": "2025-06-01",
"last_failure_at": None,
"last_status": "200",
Expand All @@ -37,7 +37,7 @@ def test_list_webhooks(self, webhooks: Webhooks, mock_client: MagicMock) -> None
assert len(result) == 1
assert isinstance(result[0], Webhook)
assert result[0].id == "wh_123"
assert result[0].event_types == ["delivery", "bounce"]
assert result[0].event_types == ["message.delivery", "message.bounce"]


class TestGet:
Expand All @@ -59,7 +59,7 @@ def test_create_webhook(self, webhooks: Webhooks, mock_client: MagicMock) -> Non
url="https://example.com/hook",
auth_type="none",
events_mode="selected",
events=["delivery", "bounce"],
events=["message.delivery", "message.bounce"],
)

assert isinstance(result, Webhook)
Expand All @@ -69,7 +69,7 @@ def test_create_webhook(self, webhooks: Webhooks, mock_client: MagicMock) -> Non
assert payload["name"] == "My Hook"
assert payload["auth_type"] == "none"
assert payload["events_mode"] == "selected"
assert payload["events"] == ["delivery", "bounce"]
assert payload["events"] == ["message.delivery", "message.bounce"]

def test_create_with_basic_auth(self, webhooks: Webhooks, mock_client: MagicMock) -> None:
mock_client.post.return_value = {
Expand Down Expand Up @@ -123,11 +123,38 @@ def test_update_webhook(self, webhooks: Webhooks, mock_client: MagicMock) -> Non
def test_update_partial(self, webhooks: Webhooks, mock_client: MagicMock) -> None:
mock_client.put.return_value = {"data": WEBHOOK_DATA}

webhooks.update("wh_123", events=["delivery"])
webhooks.update("wh_123", events=["message.delivery"])
payload = mock_client.put.call_args.kwargs["json"]
assert payload == {"events": ["delivery"]}
assert payload == {"events": ["message.delivery"]}
assert "name" not in payload

def test_update_with_url(self, webhooks: Webhooks, mock_client: MagicMock) -> None:
mock_client.put.return_value = {"data": WEBHOOK_DATA}

webhooks.update("wh_123", url="https://new.example.com/hook")
payload = mock_client.put.call_args.kwargs["json"]
assert payload == {"url": "https://new.example.com/hook"}

def test_update_target_deprecated(self, webhooks: Webhooks, mock_client: MagicMock) -> None:
mock_client.put.return_value = {"data": WEBHOOK_DATA}

with pytest.warns(DeprecationWarning, match="target"):
webhooks.update("wh_123", target="https://legacy.example.com/hook")

payload = mock_client.put.call_args.kwargs["json"]
assert payload == {"url": "https://legacy.example.com/hook"}

def test_update_url_and_target_conflict(
self, webhooks: Webhooks, mock_client: MagicMock
) -> None:
with pytest.raises(TypeError, match="both"):
webhooks.update(
"wh_123",
url="https://a.example.com/hook",
target="https://b.example.com/hook",
)
mock_client.put.assert_not_called()


class TestDelete:
def test_delete_webhook(self, webhooks: Webhooks, mock_client: MagicMock) -> None:
Expand Down
Loading