resend
Advanced tools
| from typing import List, Union | ||
| from typing_extensions import Literal, TypedDict | ||
| WebhookStatus = Literal["enabled", "disabled"] | ||
| WebhookEvent = Literal[ | ||
| # Email events | ||
| "email.sent", | ||
| "email.delivered", | ||
| "email.delivery_delayed", | ||
| "email.complained", | ||
| "email.bounced", | ||
| "email.opened", | ||
| "email.clicked", | ||
| "email.received", | ||
| "email.failed", | ||
| # Contact events | ||
| "contact.created", | ||
| "contact.updated", | ||
| "contact.deleted", | ||
| # Domain events | ||
| "domain.created", | ||
| "domain.updated", | ||
| "domain.deleted", | ||
| ] | ||
| class WebhookHeaders(TypedDict): | ||
| """ | ||
| WebhookHeaders contains the Svix headers required for webhook verification | ||
| Attributes: | ||
| id (str): The svix-id header value | ||
| timestamp (str): The svix-timestamp header value | ||
| signature (str): The svix-signature header value | ||
| """ | ||
| id: str | ||
| """ | ||
| The svix-id header value | ||
| """ | ||
| timestamp: str | ||
| """ | ||
| The svix-timestamp header value | ||
| """ | ||
| signature: str | ||
| """ | ||
| The svix-signature header value | ||
| """ | ||
| class VerifyWebhookOptions(TypedDict): | ||
| """ | ||
| VerifyWebhookOptions contains the parameters needed to verify a webhook | ||
| Attributes: | ||
| payload (str): The raw request body as a string | ||
| headers (WebhookHeaders): The Svix headers from the request | ||
| webhook_secret (str): The webhook signing secret (starts with whsec_) | ||
| """ | ||
| payload: str | ||
| """ | ||
| The raw request body as a string | ||
| """ | ||
| headers: WebhookHeaders | ||
| """ | ||
| The Svix headers from the request | ||
| """ | ||
| webhook_secret: str | ||
| """ | ||
| The webhook signing secret (starts with whsec_) | ||
| """ | ||
| class Webhook(TypedDict): | ||
| """ | ||
| Webhook represents a webhook configuration object | ||
| Attributes: | ||
| id (str): The webhook ID | ||
| object (str): The object type, always "webhook" | ||
| created_at (str): When the webhook was created | ||
| status (str): The webhook status, either "enabled" or "disabled" | ||
| endpoint (str): The URL where webhook events will be sent | ||
| events (List[str]): Array of event types to subscribe to | ||
| signing_secret (Union[str, None]): The signing secret for webhook verification | ||
| """ | ||
| id: str | ||
| """ | ||
| The webhook ID | ||
| """ | ||
| object: str | ||
| """ | ||
| The object type, always "webhook" | ||
| """ | ||
| created_at: str | ||
| """ | ||
| When the webhook was created (ISO 8601 format) | ||
| """ | ||
| status: WebhookStatus | ||
| """ | ||
| The webhook status, either "enabled" or "disabled" | ||
| """ | ||
| endpoint: str | ||
| """ | ||
| The URL where webhook events will be sent | ||
| """ | ||
| events: List[WebhookEvent] | ||
| """ | ||
| Array of event types to subscribe to | ||
| """ | ||
| signing_secret: Union[str, None] | ||
| """ | ||
| The signing secret for webhook verification | ||
| """ |
| import base64 | ||
| import hmac | ||
| import time | ||
| from hashlib import sha256 | ||
| from typing import Any, Dict, List, Optional, cast | ||
| from typing_extensions import NotRequired, TypedDict | ||
| from resend import request | ||
| from resend.pagination_helper import PaginationHelper | ||
| from resend.webhooks._webhook import (VerifyWebhookOptions, Webhook, | ||
| WebhookEvent, WebhookStatus) | ||
| # Default tolerance for timestamp validation (5 minutes) | ||
| DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300 | ||
| class Webhooks: | ||
| class ListParams(TypedDict): | ||
| limit: NotRequired[int] | ||
| """ | ||
| Number of webhooks to retrieve. Maximum is 100, and minimum is 1. | ||
| """ | ||
| after: NotRequired[str] | ||
| """ | ||
| The ID after which we'll retrieve more webhooks (for pagination). | ||
| This ID will not be included in the returned list. | ||
| Cannot be used with the before parameter. | ||
| """ | ||
| before: NotRequired[str] | ||
| """ | ||
| The ID before which we'll retrieve more webhooks (for pagination). | ||
| This ID will not be included in the returned list. | ||
| Cannot be used with the after parameter. | ||
| """ | ||
| class ListResponse(TypedDict): | ||
| """ | ||
| ListResponse type that wraps a list of webhook objects with pagination metadata | ||
| Attributes: | ||
| object (str): The object type, always "list" | ||
| data (List[Webhook]): A list of webhook objects | ||
| has_more (bool): Whether there are more results available | ||
| """ | ||
| object: str | ||
| """ | ||
| The object type, always "list" | ||
| """ | ||
| data: List[Webhook] | ||
| """ | ||
| A list of webhook objects | ||
| """ | ||
| has_more: bool | ||
| """ | ||
| Whether there are more results available for pagination | ||
| """ | ||
| class CreateWebhookResponse(TypedDict): | ||
| """ | ||
| CreateWebhookResponse is the type that wraps the response of the webhook that was created | ||
| Attributes: | ||
| object (str): The object type, always "webhook" | ||
| id (str): The ID of the created webhook | ||
| signing_secret (str): The signing secret for webhook verification | ||
| """ | ||
| object: str | ||
| """ | ||
| The object type, always "webhook" | ||
| """ | ||
| id: str | ||
| """ | ||
| The ID of the created webhook | ||
| """ | ||
| signing_secret: str | ||
| """ | ||
| The signing secret for webhook verification | ||
| """ | ||
| class CreateParams(TypedDict): | ||
| endpoint: str | ||
| """ | ||
| The URL where webhook events will be sent. | ||
| """ | ||
| events: List[WebhookEvent] | ||
| """ | ||
| Array of event types to subscribe to. | ||
| See https://resend.com/docs/dashboard/webhooks/event-types for available options. | ||
| """ | ||
| class UpdateParams(TypedDict): | ||
| webhook_id: str | ||
| """ | ||
| The webhook ID. | ||
| """ | ||
| endpoint: NotRequired[str] | ||
| """ | ||
| The URL where webhook events will be sent. | ||
| """ | ||
| events: NotRequired[List[WebhookEvent]] | ||
| """ | ||
| Array of event types to subscribe to. | ||
| """ | ||
| status: NotRequired[WebhookStatus] | ||
| """ | ||
| The webhook status. Can be either "enabled" or "disabled". | ||
| """ | ||
| class UpdateWebhookResponse(TypedDict): | ||
| """ | ||
| UpdateWebhookResponse is the type that wraps the response of the webhook that was updated | ||
| Attributes: | ||
| object (str): The object type, always "webhook" | ||
| id (str): The ID of the updated webhook | ||
| """ | ||
| object: str | ||
| """ | ||
| The object type, always "webhook" | ||
| """ | ||
| id: str | ||
| """ | ||
| The ID of the updated webhook | ||
| """ | ||
| class DeleteWebhookResponse(TypedDict): | ||
| """ | ||
| DeleteWebhookResponse is the type that wraps the response of the webhook that was deleted | ||
| Attributes: | ||
| object (str): The object type, always "webhook" | ||
| id (str): The ID of the deleted webhook | ||
| deleted (bool): Whether the webhook was successfully deleted | ||
| """ | ||
| object: str | ||
| """ | ||
| The object type, always "webhook" | ||
| """ | ||
| id: str | ||
| """ | ||
| The ID of the deleted webhook | ||
| """ | ||
| deleted: bool | ||
| """ | ||
| Whether the webhook was successfully deleted | ||
| """ | ||
| @classmethod | ||
| def create(cls, params: CreateParams) -> CreateWebhookResponse: | ||
| """ | ||
| Create a webhook to receive real-time notifications about email events. | ||
| see more: https://resend.com/docs/api-reference/webhooks/create-webhook | ||
| Args: | ||
| params (CreateParams): The webhook creation parameters | ||
| - endpoint: The URL where webhook events will be sent | ||
| - events: Array of event types to subscribe to | ||
| Returns: | ||
| CreateWebhookResponse: The created webhook response with id and signing_secret | ||
| """ | ||
| path = "/webhooks" | ||
| resp = request.Request[Webhooks.CreateWebhookResponse]( | ||
| path=path, params=cast(Dict[Any, Any], params), verb="post" | ||
| ).perform_with_content() | ||
| return resp | ||
| @classmethod | ||
| def get(cls, webhook_id: str) -> Webhook: | ||
| """ | ||
| Retrieve a single webhook for the authenticated user. | ||
| see more: https://resend.com/docs/api-reference/webhooks/get-webhook | ||
| Args: | ||
| webhook_id (str): The webhook ID | ||
| Returns: | ||
| Webhook: The webhook object | ||
| """ | ||
| path = f"/webhooks/{webhook_id}" | ||
| resp = request.Request[Webhook]( | ||
| path=path, params={}, verb="get" | ||
| ).perform_with_content() | ||
| return resp | ||
| @classmethod | ||
| def update(cls, params: UpdateParams) -> UpdateWebhookResponse: | ||
| """ | ||
| Update an existing webhook configuration. | ||
| see more: https://resend.com/docs/api-reference/webhooks/update-webhook | ||
| Args: | ||
| params (UpdateParams): The webhook update parameters | ||
| - webhook_id: The webhook ID | ||
| - endpoint: (Optional) The URL where webhook events will be sent | ||
| - events: (Optional) Array of event types to subscribe to | ||
| - status: (Optional) The webhook status ("enabled" or "disabled") | ||
| Returns: | ||
| UpdateWebhookResponse: The updated webhook response with id | ||
| """ | ||
| webhook_id = params["webhook_id"] | ||
| path = f"/webhooks/{webhook_id}" | ||
| resp = request.Request[Webhooks.UpdateWebhookResponse]( | ||
| path=path, params=cast(Dict[Any, Any], params), verb="patch" | ||
| ).perform_with_content() | ||
| return resp | ||
| @classmethod | ||
| def list(cls, params: Optional[ListParams] = None) -> ListResponse: | ||
| """ | ||
| Retrieve a list of webhooks for the authenticated user. | ||
| see more: https://resend.com/docs/api-reference/webhooks/list-webhooks | ||
| Args: | ||
| params (Optional[ListParams]): Optional pagination parameters | ||
| - limit: Number of webhooks to retrieve (max 100, min 1). | ||
| If not provided, all webhooks will be returned without pagination. | ||
| - after: ID after which to retrieve more webhooks | ||
| - before: ID before which to retrieve more webhooks | ||
| Returns: | ||
| ListResponse: A list of webhook objects | ||
| """ | ||
| base_path = "/webhooks" | ||
| query_params = cast(Dict[Any, Any], params) if params else None | ||
| path = PaginationHelper.build_paginated_path(base_path, query_params) | ||
| resp = request.Request[Webhooks.ListResponse]( | ||
| path=path, params={}, verb="get" | ||
| ).perform_with_content() | ||
| return resp | ||
| @classmethod | ||
| def remove(cls, webhook_id: str) -> DeleteWebhookResponse: | ||
| """ | ||
| Remove an existing webhook. | ||
| see more: https://resend.com/docs/api-reference/webhooks/delete-webhook | ||
| Args: | ||
| webhook_id (str): The webhook ID | ||
| Returns: | ||
| DeleteWebhookResponse: The deleted webhook response | ||
| """ | ||
| path = f"/webhooks/{webhook_id}" | ||
| resp = request.Request[Webhooks.DeleteWebhookResponse]( | ||
| path=path, params={}, verb="delete" | ||
| ).perform_with_content() | ||
| return resp | ||
| @classmethod | ||
| def verify(cls, options: VerifyWebhookOptions) -> None: | ||
| """ | ||
| Verify validates a webhook payload using HMAC-SHA256 signature verification. | ||
| This implements manual verification without external dependencies. | ||
| see more: https://docs.svix.com/receiving/verifying-payloads/how-manual | ||
| Args: | ||
| options (VerifyWebhookOptions): The webhook verification parameters | ||
| - payload: The raw request body as a string | ||
| - headers: The Svix headers (svix-id, svix-timestamp, svix-signature) | ||
| - webhook_secret: The webhook signing secret (starts with whsec_) | ||
| Raises: | ||
| ValueError: If verification fails or required parameters are missing | ||
| Returns: | ||
| None: Returns None on successful verification, raises ValueError otherwise | ||
| """ | ||
| # Validate required parameters | ||
| if not options: | ||
| raise ValueError("options cannot be None") | ||
| if not options.get("payload"): | ||
| raise ValueError("payload cannot be empty") | ||
| if not options.get("webhook_secret"): | ||
| raise ValueError("webhook_secret cannot be empty") | ||
| headers = options.get("headers") | ||
| if not headers: | ||
| raise ValueError("headers are required") | ||
| if not headers.get("id"): | ||
| raise ValueError("svix-id header is required") | ||
| if not headers.get("timestamp"): | ||
| raise ValueError("svix-timestamp header is required") | ||
| if not headers.get("signature"): | ||
| raise ValueError("svix-signature header is required") | ||
| # Step 1: Validate timestamp to prevent replay attacks | ||
| try: | ||
| timestamp = int(headers["timestamp"]) | ||
| except (ValueError, TypeError) as e: | ||
| raise ValueError(f"invalid timestamp format: {e}") | ||
| now = int(time.time()) | ||
| diff = now - timestamp | ||
| if ( | ||
| diff > DEFAULT_WEBHOOK_TOLERANCE_SECONDS | ||
| or diff < -DEFAULT_WEBHOOK_TOLERANCE_SECONDS | ||
| ): | ||
| raise ValueError( | ||
| f"timestamp outside tolerance window: difference of {diff} seconds" | ||
| ) | ||
| # Step 2: Construct signed content: {id}.{timestamp}.{payload} | ||
| signed_content = f"{headers['id']}.{headers['timestamp']}.{options['payload']}" | ||
| # Step 3: Decode the signing secret (strip whsec_ prefix and base64 decode) | ||
| secret = options["webhook_secret"] | ||
| if secret.startswith("whsec_"): | ||
| secret = secret[6:] # Remove "whsec_" prefix | ||
| try: | ||
| decoded_secret = base64.b64decode(secret) | ||
| except Exception as e: | ||
| raise ValueError(f"failed to decode webhook secret: {e}") | ||
| # Step 4: Calculate expected signature using HMAC-SHA256 | ||
| expected_signature = cls._generate_signature( | ||
| decoded_secret, signed_content.encode("utf-8") | ||
| ) | ||
| # Step 5: Compare signatures using constant-time comparison | ||
| # The signature header contains space-separated signatures with version prefixes | ||
| # (e.g., "v1,sig1 v1,sig2") | ||
| signatures = headers["signature"].split(" ") | ||
| for sig in signatures: | ||
| # Strip version prefix (e.g., "v1,") | ||
| parts = sig.split(",", 1) | ||
| if len(parts) != 2: | ||
| continue | ||
| received_signature = parts[1] | ||
| if hmac.compare_digest(expected_signature, received_signature): | ||
| return # Signature matches | ||
| raise ValueError("no matching signature found") | ||
| @staticmethod | ||
| def _generate_signature(secret: bytes, content: bytes) -> str: | ||
| """ | ||
| Generate an HMAC-SHA256 signature and return it as base64. | ||
| Args: | ||
| secret (bytes): The decoded webhook secret | ||
| content (bytes): The content to sign | ||
| Returns: | ||
| str: The base64-encoded signature | ||
| """ | ||
| h = hmac.new(secret, content, sha256) | ||
| signature = h.digest() | ||
| return base64.b64encode(signature).decode("utf-8") |
+1
-1
| Metadata-Version: 2.1 | ||
| Name: resend | ||
| Version: 2.18.0a1 | ||
| Version: 2.18.0a2 | ||
| Summary: Resend Python SDK | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/resendlabs/resend-python |
| Metadata-Version: 2.1 | ||
| Name: resend | ||
| Version: 2.18.0a1 | ||
| Version: 2.18.0a2 | ||
| Summary: Resend Python SDK | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/resendlabs/resend-python |
@@ -48,2 +48,5 @@ LICENSE.md | ||
| resend/topics/_topic.py | ||
| resend/topics/_topics.py | ||
| resend/topics/_topics.py | ||
| resend/webhooks/__init__.py | ||
| resend/webhooks/_webhook.py | ||
| resend/webhooks/_webhooks.py |
@@ -30,2 +30,5 @@ import os | ||
| from .version import __version__, get_version | ||
| from .webhooks._webhook import (VerifyWebhookOptions, Webhook, WebhookEvent, | ||
| WebhookHeaders, WebhookStatus) | ||
| from .webhooks._webhooks import Webhooks | ||
@@ -53,2 +56,3 @@ # Config vars | ||
| "Broadcasts", | ||
| "Webhooks", | ||
| "Attachments", | ||
@@ -66,2 +70,7 @@ "Topics", | ||
| "Broadcast", | ||
| "Webhook", | ||
| "WebhookEvent", | ||
| "WebhookHeaders", | ||
| "WebhookStatus", | ||
| "VerifyWebhookOptions", | ||
| "Topic", | ||
@@ -68,0 +77,0 @@ "BatchValidationError", |
@@ -1,2 +0,2 @@ | ||
| __version__ = "2.18.0-alpha.1" | ||
| __version__ = "2.18.0a2" | ||
@@ -6,4 +6,4 @@ | ||
| """ | ||
| Returns the current version of this lib | ||
| Returns the current version of this SDK. | ||
| """ | ||
| return __version__ |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
118729
14.97%52
6.12%3181
15.13%