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
5 changes: 5 additions & 0 deletions examples/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@
else:
print("No webhook events available")

rotated: resend.Webhooks.RotateSigningSecretResponse = (
resend.Webhooks.rotate_signing_secret(webhook["id"])
Comment thread
gabrielmfern marked this conversation as resolved.
)
print(f"Rotated signing secret: {rotated['signing_secret']}")
Comment thread
gabrielmfern marked this conversation as resolved.
Dismissed
Comment thread
gabrielmfern marked this conversation as resolved.

rm_webhook: resend.Webhooks.DeleteWebhookResponse = resend.Webhooks.remove(
webhook_id=webhook["id"]
)
Expand Down
2 changes: 1 addition & 1 deletion resend/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "2.43.0"
__version__ = "2.44.0"


def get_version() -> str:
Expand Down
59 changes: 59 additions & 0 deletions resend/webhooks/_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,29 @@ class DeleteWebhookResponse(BaseResponse):
Whether the webhook was successfully deleted
"""

class RotateSigningSecretResponse(BaseResponse):
"""
RotateSigningSecretResponse is the type that wraps the response of the webhook whose signing secret was rotated

Attributes:
object (str): The object type, always "webhook"
id (str): The ID of the webhook
signing_secret (str): The new signing secret for webhook verification
"""

object: str
"""
The object type, always "webhook"
"""
id: str
"""
The ID of the webhook
"""
signing_secret: str
"""
The new signing secret for webhook verification
"""

@classmethod
def create(cls, params: CreateParams) -> CreateWebhookResponse:
"""
Expand Down Expand Up @@ -471,6 +494,23 @@ def remove(cls, webhook_id: str) -> DeleteWebhookResponse:
).perform_with_content()
return resp

@classmethod
def rotate_signing_secret(cls, webhook_id: str) -> RotateSigningSecretResponse:
"""
Rotate the signing secret of a webhook.
see more: https://resend.com/docs/api-reference/webhooks/rotate-signing-secret

Args:
webhook_id (str): The webhook ID

Returns:
RotateSigningSecretResponse: The webhook with its new signing_secret
"""
path = f"/webhooks/{webhook_id}/signing-secret/rotate"
return request.Request[Webhooks.RotateSigningSecretResponse](
path=path, params={}, verb="post"
).perform_with_content()

@classmethod
def verify(cls, options: VerifyWebhookOptions) -> WebhookEventPayload:
"""
Expand Down Expand Up @@ -750,6 +790,25 @@ async def remove_async(cls, webhook_id: str) -> DeleteWebhookResponse:
).perform_with_content()
return resp

@classmethod
async def rotate_signing_secret_async(
cls, webhook_id: str
) -> RotateSigningSecretResponse:
"""
Rotate the signing secret of a webhook (async).
see more: https://resend.com/docs/api-reference/webhooks/rotate-signing-secret

Args:
webhook_id (str): The webhook ID

Returns:
RotateSigningSecretResponse: The webhook with its new signing_secret
"""
path = f"/webhooks/{webhook_id}/signing-secret/rotate"
return await AsyncRequest[Webhooks.RotateSigningSecretResponse](
path=path, params={}, verb="post"
).perform_with_content()

@staticmethod
def _generate_signature(secret: bytes, content: bytes) -> str:
"""
Expand Down
25 changes: 25 additions & 0 deletions tests/webhooks_async_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@ async def test_should_remove_webhooks_async_raise_exception_when_no_content(
with pytest.raises(NoContentError):
_ = await resend.Webhooks.remove_async("wh_123")

async def test_rotate_signing_secret_async_raises_exception_when_no_content(
self,
) -> None:
self.set_mock_json(None)
with pytest.raises(NoContentError):
_ = await resend.Webhooks.rotate_signing_secret_async("wh_123")


class TestWebhooksRequestAsync:
def setup_method(self) -> None:
Expand Down Expand Up @@ -249,3 +256,21 @@ async def test_replay_event_async_posts_to_the_replay_path(self) -> None:
kwargs["url"]
== "https://api.resend.com/webhooks/wh_123/events/msg_1srOrx2ZWZBpBUvZwXKQmoEYga2/replay"
)

async def test_rotate_signing_secret_async_posts_to_the_rotate_path(self) -> None:
self.mock_client.request.return_value = (
b'{"object": "webhook", "id": "wh_123", "signing_secret": "whsec_new"}',
200,
{"content-type": "application/json"},
)

webhook = await resend.Webhooks.rotate_signing_secret_async("wh_123")

assert webhook["object"] == "webhook"
assert webhook["id"] == "wh_123"
assert webhook["signing_secret"] == "whsec_new"
_, kwargs = self.mock_client.request.call_args
assert kwargs["method"] == "post"
assert (
kwargs["url"] == "https://api.resend.com/webhooks/wh_123/signing-secret/rotate"
)
24 changes: 24 additions & 0 deletions tests/webhooks_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ def test_webhooks_remove(self) -> None:
assert result["id"] == "wh_123"
assert result["deleted"] is True

def test_rotate_signing_secret_raises_exception_when_no_content(self) -> None:
self.set_mock_json(None)
with pytest.raises(NoContentError):
_ = resend.Webhooks.rotate_signing_secret("wh_123")


class TestWebhooksRequest(TestCase):
def setUp(self) -> None:
Expand Down Expand Up @@ -210,6 +215,25 @@ def test_replay_event_posts_to_the_replay_path(self) -> None:
== "https://api.resend.com/webhooks/wh_123/events/msg_1srOrx2ZWZBpBUvZwXKQmoEYga2/replay"
)

def test_rotate_signing_secret_posts_to_the_rotate_path(self) -> None:
self.mock_client.request.return_value = (
b'{"object": "webhook", "id": "wh_123", "signing_secret": "whsec_new"}',
200,
{"Content-Type": "application/json"},
)

webhook = resend.Webhooks.rotate_signing_secret("wh_123")

assert webhook["object"] == "webhook"
assert webhook["id"] == "wh_123"
assert webhook["signing_secret"] == "whsec_new"
_, kwargs = self.mock_client.request.call_args
assert kwargs["method"] == "post"
assert (
kwargs["url"]
== "https://api.resend.com/webhooks/wh_123/signing-secret/rotate"
)


class TestWebhookVerification:
"""Test webhook signature verification"""
Expand Down
Loading