diff --git a/examples/async/automations_async.py b/examples/async/automations_async.py
new file mode 100644
index 0000000..1d7d78e
--- /dev/null
+++ b/examples/async/automations_async.py
@@ -0,0 +1,150 @@
+import asyncio
+import os
+
+import resend
+
+if not os.environ["RESEND_API_KEY"]:
+ raise EnvironmentError("RESEND_API_KEY is missing")
+
+
+async def main() -> None:
+ # Create and publish a template to use in the automation
+ print("--- Create and publish template ---")
+ tpl: resend.Templates.CreateResponse = await resend.Templates.create_async(
+ {
+ "name": "welcome-email",
+ "subject": "Welcome!",
+ "html": "Welcome to our service!",
+ }
+ )
+ await resend.Templates.publish_async(tpl["id"])
+ print(f"Template: {tpl['id']}")
+
+ # --- Create a simple automation (trigger → send_email) ---
+ print("\n--- Create automation ---")
+ simple: resend.Automations.CreateResponse = await resend.Automations.create_async(
+ {
+ "name": "Welcome Flow",
+ "status": "disabled",
+ "steps": [
+ {
+ "key": "trigger_1",
+ "type": "trigger",
+ "config": {"event_name": "user.created"},
+ },
+ {
+ "key": "send_1",
+ "type": "send_email",
+ "config": {"template": {"id": tpl["id"]}},
+ },
+ ],
+ "connections": [
+ {"from": "trigger_1", "to": "send_1"},
+ ],
+ }
+ )
+ automation_id = simple["id"]
+ print(f"Created automation: {automation_id}")
+
+ # --- Get automation ---
+ print("\n--- Get automation ---")
+ automation: resend.Automation = await resend.Automations.get_async(automation_id)
+ print(f"Name: {automation['name']}, status: {automation['status']}")
+ for step in automation["steps"]:
+ print(f" Step key={step['key']} type={step['type']}")
+
+ # --- Update automation ---
+ print("\n--- Update automation ---")
+ updated: resend.Automations.UpdateResponse = await resend.Automations.update_async(
+ {
+ "automation_id": automation_id,
+ "status": "enabled",
+ }
+ )
+ print(f"Updated: {updated['id']}")
+
+ # --- List automations ---
+ print("\n--- List automations ---")
+ list_resp: resend.Automations.ListResponse = await resend.Automations.list_async()
+ print(f"Total: {len(list_resp['data'])}, has_more: {list_resp['has_more']}")
+
+ # --- Stop automation ---
+ print("\n--- Stop automation ---")
+ stopped: resend.Automations.StopResponse = await resend.Automations.stop_async(
+ automation_id
+ )
+ print(f"Stopped: {stopped['id']}, status: {stopped['status']}")
+
+ # --- List runs ---
+ print("\n--- List runs ---")
+ runs: resend.Automations.Runs.ListResponse = (
+ await resend.Automations.Runs.list_async(automation_id)
+ )
+ print(f"Total runs: {len(runs['data'])}")
+ if runs["data"]:
+ run_id = runs["data"][0]["id"]
+ run: resend.AutomationRun = await resend.Automations.Runs.get_async(
+ automation_id, run_id
+ )
+ print(f"Run status: {run['status']}")
+
+ # --- Multi-step automation: delay + wait_for_event ---
+ print("\n--- Create multi-step automation (delay + wait_for_event) ---")
+ multi: resend.Automations.CreateResponse = await resend.Automations.create_async(
+ {
+ "name": "Onboarding Flow",
+ "status": "disabled",
+ "steps": [
+ {
+ "key": "trigger_1",
+ "type": "trigger",
+ "config": {"event_name": "user.created"},
+ },
+ {
+ "key": "delay_1",
+ "type": "delay",
+ # duration is a human-readable string; "seconds" (int) is also accepted
+ "config": {"duration": "30 minutes"},
+ },
+ {
+ "key": "wait_1",
+ "type": "wait_for_event",
+ # timeout is a human-readable string; timeout_seconds is NOT supported
+ "config": {"event_name": "user.verified", "timeout": "1 hour"},
+ },
+ {
+ "key": "send_1",
+ "type": "send_email",
+ "config": {"template": {"id": tpl["id"]}},
+ },
+ ],
+ "connections": [
+ {"from": "trigger_1", "to": "delay_1"},
+ {"from": "delay_1", "to": "wait_1"},
+ {"from": "wait_1", "to": "send_1", "type": "event_received"},
+ {"from": "wait_1", "to": "send_1", "type": "timeout"},
+ ],
+ }
+ )
+ multi_id = multi["id"]
+ print(f"Created: {multi_id}")
+
+ retrieved: resend.Automation = await resend.Automations.get_async(multi_id)
+ for step in retrieved["steps"]:
+ print(f" Step key={step['key']} type={step['type']} config={step['config']}")
+
+ # --- Cleanup ---
+ print("\n--- Cleanup ---")
+ del1: resend.Automations.DeleteResponse = await resend.Automations.remove_async(
+ automation_id
+ )
+ print(f"Deleted automation {del1['id']}: {del1['deleted']}")
+ del2: resend.Automations.DeleteResponse = await resend.Automations.remove_async(
+ multi_id
+ )
+ print(f"Deleted automation {del2['id']}: {del2['deleted']}")
+ await resend.Templates.remove_async(tpl["id"])
+ print(f"Deleted template {tpl['id']}")
+
+
+asyncio.run(main())
diff --git a/examples/async/events_async.py b/examples/async/events_async.py
new file mode 100644
index 0000000..f43357b
--- /dev/null
+++ b/examples/async/events_async.py
@@ -0,0 +1,95 @@
+import asyncio
+import os
+
+import resend
+
+if not os.environ["RESEND_API_KEY"]:
+ raise EnvironmentError("RESEND_API_KEY is missing")
+
+
+async def main() -> None:
+ # Create a contact to use with event sends
+ print("--- Create contact ---")
+ contact: resend.Contacts.CreateContactResponse = await resend.Contacts.create_async(
+ {
+ "email": "test-events-async@example.com",
+ "first_name": "Test",
+ "last_name": "User",
+ }
+ )
+ contact_id = contact["id"]
+ print(f"Contact: {contact_id}")
+
+ # --- Create event ---
+ print("\n--- Create event ---")
+ created: resend.Events.CreateResponse = await resend.Events.create_async(
+ {
+ "name": "user.signed_up",
+ "schema": {
+ "plan": "string",
+ "trial_days": "number",
+ "is_enterprise": "boolean",
+ "upgraded_at": "date",
+ },
+ }
+ )
+ event_id = created["id"]
+ print(f"Created event: {event_id}")
+
+ # --- Get event by ID ---
+ print("\n--- Get event by ID ---")
+ event: resend.Event = await resend.Events.get_async(event_id)
+ print(f"Name: {event['name']}, schema: {event['schema']}")
+
+ # --- Get event by name ---
+ print("\n--- Get event by name ---")
+ event_by_name: resend.Event = await resend.Events.get_async("user.signed_up")
+ print(f"Found by name: {event_by_name['name']}")
+
+ # --- Update event schema ---
+ print("\n--- Update event schema ---")
+ updated: resend.Events.UpdateResponse = await resend.Events.update_async(
+ {
+ "identifier": "user.signed_up",
+ "schema": {"plan": "string", "source": "string"},
+ }
+ )
+ print(f"Updated event: {updated['id']}")
+
+ # --- Send event with contact_id ---
+ print("\n--- Send event with contact_id ---")
+ sent: resend.Events.SendResponse = await resend.Events.send_async(
+ {
+ "event": "user.signed_up",
+ "contact_id": contact_id,
+ "payload": {"plan": "pro"},
+ }
+ )
+ print(f"Sent event: {sent['event']}")
+
+ # --- Send event with email ---
+ print("\n--- Send event with email ---")
+ sent_email: resend.Events.SendResponse = await resend.Events.send_async(
+ {
+ "event": "user.signed_up",
+ "email": "test-events-async@example.com",
+ }
+ )
+ print(f"Sent event: {sent_email['event']}")
+
+ # --- List events ---
+ print("\n--- List events ---")
+ list_resp: resend.Events.ListResponse = await resend.Events.list_async()
+ print(f"Total: {len(list_resp['data'])}, has_more: {list_resp['has_more']}")
+
+ # --- Cleanup ---
+ print("\n--- Delete event ---")
+ deleted: resend.Events.DeleteResponse = await resend.Events.remove_async(event_id)
+ print(f"Deleted: {deleted['deleted']}")
+
+ print("\n--- Delete contact ---")
+ await resend.Contacts.remove_async(id=contact_id)
+ print(f"Deleted contact: {contact_id}")
+
+
+asyncio.run(main())
diff --git a/examples/async/receiving_email_async.py b/examples/async/receiving_email_async.py
index 8f7ad10..b8dd24f 100644
--- a/examples/async/receiving_email_async.py
+++ b/examples/async/receiving_email_async.py
@@ -79,7 +79,9 @@ async def main() -> None:
print("No attachments")
print("\n--- Listing All Received Emails ---")
- all_emails: EmailsReceiving.ListResponse = await resend.Emails.Receiving.list_async()
+ all_emails: EmailsReceiving.ListResponse = (
+ await resend.Emails.Receiving.list_async()
+ )
print(f"Total emails in this batch: {len(all_emails['data'])}")
print(f"Has more emails: {all_emails['has_more']}")
@@ -138,7 +140,9 @@ async def main() -> None:
first_attachment = received_email["attachments"][0]
attachment_id = first_attachment["id"]
- print(f"\n--- Retrieving Attachment Details: {first_attachment['filename']} ---")
+ print(
+ f"\n--- Retrieving Attachment Details: {first_attachment['filename']} ---"
+ )
attachment_details: resend.EmailAttachmentDetails = (
await resend.Emails.Receiving.Attachments.get_async(
diff --git a/examples/automations.py b/examples/automations.py
new file mode 100644
index 0000000..ec5c39c
--- /dev/null
+++ b/examples/automations.py
@@ -0,0 +1,145 @@
+import os
+
+import resend
+
+if not os.environ["RESEND_API_KEY"]:
+ raise EnvironmentError("RESEND_API_KEY is missing")
+
+# Create and publish a template to use in the automation
+print("--- Create and publish template ---")
+tpl: resend.Templates.CreateResponse = resend.Templates.create(
+ {
+ "name": "welcome-email",
+ "subject": "Welcome!",
+ "html": "Welcome to our service!",
+ }
+)
+resend.Templates.publish(tpl["id"])
+print(f"Template: {tpl['id']}")
+
+# --- Create a simple automation (trigger → send_email) ---
+print("\n--- Create automation ---")
+simple: resend.Automations.CreateResponse = resend.Automations.create(
+ {
+ "name": "Welcome Flow",
+ "status": "disabled",
+ "steps": [
+ {
+ "key": "trigger_1",
+ "type": "trigger",
+ "config": {"event_name": "user.created"},
+ },
+ {
+ "key": "send_1",
+ "type": "send_email",
+ "config": {"template": {"id": tpl["id"]}},
+ },
+ ],
+ "connections": [
+ {"from": "trigger_1", "to": "send_1"},
+ ],
+ }
+)
+automation_id = simple["id"]
+print(f"Created automation: {automation_id}")
+
+# --- Get automation ---
+print("\n--- Get automation ---")
+automation: resend.Automation = resend.Automations.get(automation_id)
+print(f"Name: {automation['name']}, status: {automation['status']}")
+for step in automation["steps"]:
+ print(f" Step key={step['key']} type={step['type']}")
+for conn in automation["connections"]:
+ print(f" Connection: {conn['from']} -> {conn['to']}")
+
+# --- Update automation ---
+print("\n--- Update automation ---")
+updated: resend.Automations.UpdateResponse = resend.Automations.update(
+ {
+ "automation_id": automation_id,
+ "status": "enabled",
+ }
+)
+print(f"Updated: {updated['id']}")
+
+# --- List automations ---
+print("\n--- List automations ---")
+list_resp: resend.Automations.ListResponse = resend.Automations.list()
+print(f"Total: {len(list_resp['data'])}, has_more: {list_resp['has_more']}")
+
+list_enabled: resend.Automations.ListResponse = resend.Automations.list(
+ params={"status": "enabled", "limit": 10}
+)
+print(f"Enabled: {len(list_enabled['data'])}")
+
+# --- Stop automation ---
+print("\n--- Stop automation ---")
+stopped: resend.Automations.StopResponse = resend.Automations.stop(automation_id)
+print(f"Stopped: {stopped['id']}, status: {stopped['status']}")
+
+# --- List runs ---
+print("\n--- List runs ---")
+runs: resend.Automations.Runs.ListResponse = resend.Automations.Runs.list(automation_id)
+print(f"Total runs: {len(runs['data'])}")
+if runs["data"]:
+ run_id = runs["data"][0]["id"]
+ run: resend.AutomationRun = resend.Automations.Runs.get(automation_id, run_id)
+ print(f"Run status: {run['status']}, steps: {len(run['steps'])}")
+ for run_step in run["steps"]:
+ print(
+ f" Step key={run_step['key']} type={run_step['type']} status={run_step['status']}"
+ )
+
+# --- Multi-step automation: delay + wait_for_event ---
+print("\n--- Create multi-step automation (delay + wait_for_event) ---")
+multi: resend.Automations.CreateResponse = resend.Automations.create(
+ {
+ "name": "Onboarding Flow",
+ "status": "disabled",
+ "steps": [
+ {
+ "key": "trigger_1",
+ "type": "trigger",
+ "config": {"event_name": "user.created"},
+ },
+ {
+ "key": "delay_1",
+ "type": "delay",
+ # duration is a human-readable string; "seconds" (int) is also accepted
+ "config": {"duration": "30 minutes"},
+ },
+ {
+ "key": "wait_1",
+ "type": "wait_for_event",
+ # timeout is a human-readable string; timeout_seconds is NOT supported
+ "config": {"event_name": "user.verified", "timeout": "1 hour"},
+ },
+ {
+ "key": "send_1",
+ "type": "send_email",
+ "config": {"template": {"id": tpl["id"]}},
+ },
+ ],
+ "connections": [
+ {"from": "trigger_1", "to": "delay_1"},
+ {"from": "delay_1", "to": "wait_1"},
+ {"from": "wait_1", "to": "send_1", "type": "event_received"},
+ {"from": "wait_1", "to": "send_1", "type": "timeout"},
+ ],
+ }
+)
+multi_id = multi["id"]
+print(f"Created: {multi_id}")
+
+retrieved: resend.Automation = resend.Automations.get(multi_id)
+for step in retrieved["steps"]:
+ print(f" Step key={step['key']} type={step['type']} config={step['config']}")
+
+# --- Delete automations and template ---
+print("\n--- Cleanup ---")
+del1: resend.Automations.DeleteResponse = resend.Automations.remove(automation_id)
+print(f"Deleted automation {del1['id']}: {del1['deleted']}")
+del2: resend.Automations.DeleteResponse = resend.Automations.remove(multi_id)
+print(f"Deleted automation {del2['id']}: {del2['deleted']}")
+resend.Templates.remove(tpl["id"])
+print(f"Deleted template {tpl['id']}")
diff --git a/examples/events.py b/examples/events.py
new file mode 100644
index 0000000..cdb035e
--- /dev/null
+++ b/examples/events.py
@@ -0,0 +1,107 @@
+import os
+
+import resend
+
+if not os.environ["RESEND_API_KEY"]:
+ raise EnvironmentError("RESEND_API_KEY is missing")
+
+# Create a contact to use with event sends
+print("--- Create contact ---")
+contact: resend.Contacts.CreateContactResponse = resend.Contacts.create(
+ {
+ "email": "test-events@example.com",
+ "first_name": "Test",
+ "last_name": "User",
+ }
+)
+contact_id = contact["id"]
+print(f"Contact: {contact_id}")
+
+# --- Create event without schema ---
+print("\n--- Create event without schema ---")
+created: resend.Events.CreateResponse = resend.Events.create({"name": "user.signed_up"})
+event_id = created["id"]
+print(f"Created event: {event_id}")
+
+# --- Create event with schema ---
+print("\n--- Create event with schema ---")
+created_with_schema: resend.Events.CreateResponse = resend.Events.create(
+ {
+ "name": "user.upgraded",
+ "schema": {
+ "plan": "string",
+ "trial_days": "number",
+ "is_enterprise": "boolean",
+ "upgraded_at": "date",
+ },
+ }
+)
+print(f"Created event with schema: {created_with_schema['id']}")
+
+# --- Get event by ID ---
+print("\n--- Get event by ID ---")
+event: resend.Event = resend.Events.get(event_id)
+print(f"ID: {event['id']}")
+print(f"Name: {event['name']}")
+print(f"Schema: {event['schema']}")
+
+# --- Get event by name ---
+print("\n--- Get event by name ---")
+event_by_name: resend.Event = resend.Events.get("user.signed_up")
+print(f"Found by name: {event_by_name['name']}")
+
+# --- Update event schema ---
+print("\n--- Update event schema ---")
+updated: resend.Events.UpdateResponse = resend.Events.update(
+ {
+ "identifier": "user.signed_up",
+ "schema": {"plan": "string", "source": "string"},
+ }
+)
+print(f"Updated event: {updated['id']}")
+
+# --- Send event with contact_id ---
+print("\n--- Send event with contact_id ---")
+sent: resend.Events.SendResponse = resend.Events.send(
+ {
+ "event": "user.signed_up",
+ "contact_id": contact_id,
+ "payload": {"plan": "pro", "source": "web"},
+ }
+)
+print(f"Sent event: {sent['event']}")
+
+# --- Send event with email ---
+print("\n--- Send event with email ---")
+sent_email: resend.Events.SendResponse = resend.Events.send(
+ {
+ "event": "user.signed_up",
+ "email": "test-events@example.com",
+ }
+)
+print(f"Sent event: {sent_email['event']}")
+
+# --- List events ---
+print("\n--- List events ---")
+list_resp: resend.Events.ListResponse = resend.Events.list()
+print(f"Total events: {len(list_resp['data'])}, has_more: {list_resp['has_more']}")
+for ev in list_resp["data"]:
+ print(f" {ev['name']} ({ev['id']})")
+
+# --- List events with pagination ---
+print("\n--- List events with pagination ---")
+paginated: resend.Events.ListResponse = resend.Events.list(params={"limit": 5})
+print(f"Retrieved {len(paginated['data'])} events")
+
+# --- Cleanup ---
+print("\n--- Delete event by ID ---")
+deleted: resend.Events.DeleteResponse = resend.Events.remove(event_id)
+print(f"Deleted: {deleted['deleted']}")
+
+print("\n--- Delete event by name ---")
+deleted_by_name: resend.Events.DeleteResponse = resend.Events.remove("user.upgraded")
+print(f"Deleted by name: {deleted_by_name['deleted']}")
+
+print("\n--- Delete contact ---")
+resend.Contacts.remove(id=contact_id)
+print(f"Deleted contact: {contact_id}")
diff --git a/examples/logs.py b/examples/logs.py
index 4e091a6..3717d66 100644
--- a/examples/logs.py
+++ b/examples/logs.py
@@ -19,9 +19,7 @@
"limit": 10,
"after": logs["data"][0]["id"],
}
- paginated_logs: resend.Logs.ListResponse = resend.Logs.list(
- params=paginated_params
- )
+ paginated_logs: resend.Logs.ListResponse = resend.Logs.list(params=paginated_params)
print(f"Retrieved {len(paginated_logs['data'])} logs with pagination")
print(f"Has more logs: {paginated_logs['has_more']}")
else:
diff --git a/resend/__init__.py b/resend/__init__.py
index 2bce0f8..2e1c8d5 100644
--- a/resend/__init__.py
+++ b/resend/__init__.py
@@ -1,10 +1,19 @@
import os
-from typing import Union
+from typing import Optional, Union
from .api_keys._api_key import ApiKey
from .api_keys._api_keys import ApiKeys
from .audiences._audience import Audience
from .audiences._audiences import Audiences
+from .automations._automation import (Automation, AutomationConnection,
+ AutomationConnectionType,
+ AutomationListItem,
+ AutomationResponseStep, AutomationRun,
+ AutomationRunListItem,
+ AutomationRunStatus, AutomationRunStep,
+ AutomationStatus, AutomationStep,
+ AutomationStepType)
+from .automations._automations import Automations
from .broadcasts._broadcast import Broadcast
from .broadcasts._broadcasts import Broadcasts
from .contact_properties._contact_properties import ContactProperties
@@ -17,8 +26,6 @@
from .contacts.segments._contact_segments import ContactSegments
from .domains._domain import Domain
from .domains._domains import Domains
-from .logs._log import Log
-from .logs._logs import Logs
from .emails._attachment import Attachment, RemoteAttachment
from .emails._attachments import Attachments as EmailAttachments
from .emails._batch import Batch, BatchValidationError
@@ -28,12 +35,16 @@
ListReceivedEmail, ReceivedEmail)
from .emails._receiving import Receiving as EmailsReceiving
from .emails._tag import Tag
+from .events._event import (Event, EventListItem, EventSchema,
+ EventSchemaFieldType)
+from .events._events import Events
from .http_client import HTTPClient
from .http_client_async import \
AsyncHTTPClient # Okay to import AsyncHTTPClient since it is just an interface.
from .http_client_requests import RequestsClient
+from .logs._log import Log
+from .logs._logs import Logs
from .request import Request
-from typing import Optional
from .segments._segment import Segment
from .segments._segments import Segments
from .templates._template import Template, TemplateListItem, Variable
@@ -69,9 +80,11 @@
"Domains",
"Batch",
"Audiences",
+ "Automations",
"Contacts",
"ContactProperties",
"Broadcasts",
+ "Events",
"Segments",
"Templates",
"Webhooks",
@@ -79,6 +92,22 @@
"Logs",
# Types
"Audience",
+ "Automation",
+ "AutomationConnection",
+ "AutomationConnectionType",
+ "AutomationListItem",
+ "AutomationResponseStep",
+ "AutomationRun",
+ "AutomationRunListItem",
+ "AutomationRunStatus",
+ "AutomationRunStep",
+ "AutomationStatus",
+ "AutomationStep",
+ "AutomationStepType",
+ "Event",
+ "EventListItem",
+ "EventSchema",
+ "EventSchemaFieldType",
"Contact",
"ContactSegment",
"ContactSegments",
diff --git a/resend/async_request.py b/resend/async_request.py
index ebc0987..ad8dcd6 100644
--- a/resend/async_request.py
+++ b/resend/async_request.py
@@ -84,7 +84,9 @@ async def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]:
async_client = resend.default_async_http_client
# Priority 2: user set an AsyncHTTPClient on default_http_client (legacy, still supported)
- if async_client is None and isinstance(resend.default_http_client, AsyncHTTPClient):
+ if async_client is None and isinstance(
+ resend.default_http_client, AsyncHTTPClient
+ ):
async_client = resend.default_http_client
if async_client is None:
diff --git a/resend/automations/__init__.py b/resend/automations/__init__.py
new file mode 100644
index 0000000..f9a0391
--- /dev/null
+++ b/resend/automations/__init__.py
@@ -0,0 +1,27 @@
+from resend.automations._automation import (Automation, AutomationConnection,
+ AutomationConnectionType,
+ AutomationListItem,
+ AutomationResponseStep,
+ AutomationRun,
+ AutomationRunListItem,
+ AutomationRunStatus,
+ AutomationRunStep,
+ AutomationStatus, AutomationStep,
+ AutomationStepType)
+from resend.automations._automations import Automations
+
+__all__ = [
+ "Automation",
+ "AutomationConnection",
+ "AutomationConnectionType",
+ "AutomationListItem",
+ "AutomationResponseStep",
+ "AutomationRun",
+ "AutomationRunListItem",
+ "AutomationRunStatus",
+ "AutomationRunStep",
+ "AutomationStatus",
+ "AutomationStep",
+ "AutomationStepType",
+ "Automations",
+]
diff --git a/resend/automations/_automation.py b/resend/automations/_automation.py
new file mode 100644
index 0000000..541f608
--- /dev/null
+++ b/resend/automations/_automation.py
@@ -0,0 +1,299 @@
+from typing import Any, Dict, List, Union
+
+from typing_extensions import Literal, NotRequired, TypedDict
+
+AutomationStatus = Literal["enabled", "disabled"]
+AutomationRunStatus = Literal["running", "completed", "failed", "cancelled"]
+AutomationStepType = Literal[
+ "trigger",
+ "send_email",
+ "delay",
+ "wait_for_event",
+ "condition",
+ "contact_update",
+ "contact_delete",
+ "add_to_segment",
+]
+AutomationConnectionType = Literal[
+ "default",
+ "condition_met",
+ "condition_not_met",
+ "timeout",
+ "event_received",
+]
+
+# Uses functional TypedDict syntax because "from" is a reserved keyword in Python
+AutomationConnection = TypedDict(
+ "AutomationConnection",
+ {
+ "from": str,
+ "to": str,
+ "type": NotRequired[AutomationConnectionType],
+ },
+)
+"""
+AutomationConnection represents a connection between two steps in an automation graph.
+
+Attributes:
+ from (str): The key of the source step
+ to (str): The key of the target step
+ type (NotRequired[AutomationConnectionType]): The type of connection, defaults to "default"
+"""
+
+
+class AutomationStep(TypedDict):
+ """
+ AutomationStep represents a step in an automation workflow request.
+
+ Attributes:
+ key (str): Unique identifier for the step within the graph
+ type (AutomationStepType): The type of step
+ config (Dict[str, Any]): Step-specific configuration object
+ """
+
+ key: str
+ """
+ Unique identifier for the step within the graph.
+ """
+ type: AutomationStepType
+ """
+ The type of step.
+ """
+ config: Dict[str, Any]
+ """
+ Step-specific configuration. Shape depends on the step type.
+ """
+
+
+class AutomationResponseStep(TypedDict):
+ """
+ AutomationResponseStep represents a step as returned by the API.
+
+ Attributes:
+ key (str): The step identifier
+ type (AutomationStepType): The type of step
+ config (Any): Step-specific configuration
+ """
+
+ key: str
+ """
+ The step identifier.
+ """
+ type: AutomationStepType
+ """
+ The type of step.
+ """
+ config: Any
+ """
+ Step-specific configuration.
+ """
+
+
+class AutomationRunStep(TypedDict):
+ """
+ AutomationRunStep represents a step execution within an automation run.
+
+ Attributes:
+ key (str): The step identifier
+ type (str): The type of step
+ status (str): Execution status of this step
+ started_at (Union[str, None]): When the step started executing
+ completed_at (Union[str, None]): When the step completed executing
+ output (Any): The step output data
+ error (Any): Any error that occurred
+ created_at (str): When the run step was created
+ """
+
+ key: str
+ """
+ The step identifier.
+ """
+ type: AutomationStepType
+ """
+ The type of step.
+ """
+ status: str
+ """
+ Execution status of this step.
+ """
+ started_at: Union[str, None]
+ """
+ When the step started executing (ISO 8601 format), or None if not started.
+ """
+ completed_at: Union[str, None]
+ """
+ When the step completed executing (ISO 8601 format), or None if not completed.
+ """
+ output: Any
+ """
+ The step output data.
+ """
+ error: Any
+ """
+ Any error that occurred during step execution.
+ """
+ created_at: str
+ """
+ When the run step was created (ISO 8601 format).
+ """
+
+
+class AutomationListItem(TypedDict):
+ """
+ AutomationListItem represents an automation in list responses.
+
+ Attributes:
+ id (str): The automation ID
+ name (str): The automation name
+ status (AutomationStatus): Current status
+ created_at (str): Creation date/time
+ updated_at (str): Last update date/time
+ """
+
+ id: str
+ """
+ The automation ID.
+ """
+ name: str
+ """
+ The automation name.
+ """
+ status: AutomationStatus
+ """
+ Current status of the automation.
+ """
+ created_at: str
+ """
+ When the automation was created (ISO 8601 format).
+ """
+ updated_at: str
+ """
+ When the automation was last updated (ISO 8601 format).
+ """
+
+
+class Automation(TypedDict):
+ """
+ Automation represents a full automation object.
+
+ Attributes:
+ object (str): The object type, always "automation"
+ id (str): The automation ID
+ name (str): The automation name
+ status (AutomationStatus): Current status
+ created_at (str): Creation date/time
+ updated_at (str): Last update date/time
+ steps (List[AutomationResponseStep]): Steps in the active version
+ connections (List[AutomationConnection]): Connections between steps
+ """
+
+ object: str
+ """
+ The object type, always "automation".
+ """
+ id: str
+ """
+ The automation ID.
+ """
+ name: str
+ """
+ The automation name.
+ """
+ status: AutomationStatus
+ """
+ Current status of the automation.
+ """
+ created_at: str
+ """
+ When the automation was created (ISO 8601 format).
+ """
+ updated_at: str
+ """
+ When the automation was last updated (ISO 8601 format).
+ """
+ steps: List[AutomationResponseStep]
+ """
+ Steps in the active version of the automation.
+ """
+ connections: List[AutomationConnection]
+ """
+ Connections between steps in the active version of the automation.
+ """
+
+
+class AutomationRunListItem(TypedDict):
+ """
+ AutomationRunListItem represents an automation run in list responses.
+
+ Attributes:
+ id (str): The run ID
+ status (AutomationRunStatus): Current run status
+ started_at (Union[str, None]): When the run started
+ completed_at (Union[str, None]): When the run completed
+ created_at (str): When the run was created
+ """
+
+ id: str
+ """
+ The run ID.
+ """
+ status: AutomationRunStatus
+ """
+ Current run status.
+ """
+ started_at: Union[str, None]
+ """
+ When the run started (ISO 8601 format), or None.
+ """
+ completed_at: Union[str, None]
+ """
+ When the run completed (ISO 8601 format), or None.
+ """
+ created_at: str
+ """
+ When the run was created (ISO 8601 format).
+ """
+
+
+class AutomationRun(TypedDict):
+ """
+ AutomationRun represents a full automation run object.
+
+ Attributes:
+ object (str): The object type, always "automation_run"
+ id (str): The run ID
+ status (AutomationRunStatus): Current run status
+ started_at (Union[str, None]): When the run started
+ completed_at (Union[str, None]): When the run completed
+ created_at (str): When the run was created
+ steps (List[AutomationRunStep]): Steps in the run, sorted in graph order
+ """
+
+ object: str
+ """
+ The object type, always "automation_run".
+ """
+ id: str
+ """
+ The run ID.
+ """
+ status: AutomationRunStatus
+ """
+ Current run status.
+ """
+ started_at: Union[str, None]
+ """
+ When the run started (ISO 8601 format), or None.
+ """
+ completed_at: Union[str, None]
+ """
+ When the run completed (ISO 8601 format), or None.
+ """
+ created_at: str
+ """
+ When the run was created (ISO 8601 format).
+ """
+ steps: List[AutomationRunStep]
+ """
+ Steps in the run, sorted in graph order.
+ """
diff --git a/resend/automations/_automations.py b/resend/automations/_automations.py
new file mode 100644
index 0000000..6e8e85d
--- /dev/null
+++ b/resend/automations/_automations.py
@@ -0,0 +1,609 @@
+from typing import Any, Dict, List, Optional, cast
+
+from typing_extensions import NotRequired, TypedDict
+
+from resend import request
+from resend._base_response import BaseResponse
+from resend.pagination_helper import PaginationHelper
+
+from ._automation import (Automation, AutomationConnection, AutomationListItem,
+ AutomationRun, AutomationRunListItem,
+ AutomationStatus, AutomationStep)
+
+# Async imports (optional - only available with pip install resend[async])
+try:
+ from resend.async_request import AsyncRequest
+except ImportError:
+ pass
+
+
+class Automations:
+
+ class CreateParams(TypedDict):
+ """
+ CreateParams is the class that wraps the parameters for the create method.
+
+ Attributes:
+ name (str): The name of the automation
+ steps (List[AutomationStep]): The automation workflow steps (must include at least one trigger)
+ connections (List[AutomationConnection]): Connections between steps in the automation graph
+ status (NotRequired[AutomationStatus]): Initial status, defaults to "disabled"
+ """
+
+ name: str
+ """
+ The name of the automation.
+ """
+ steps: List[AutomationStep]
+ """
+ The automation workflow steps. Must include at least one trigger step.
+ """
+ connections: List[AutomationConnection]
+ """
+ Connections between steps in the automation graph.
+ """
+ status: NotRequired[AutomationStatus]
+ """
+ Initial status of the automation. Defaults to "disabled".
+ """
+
+ class UpdateParams(TypedDict):
+ """
+ UpdateParams is the class that wraps the parameters for the update method.
+
+ Attributes:
+ automation_id (str): The ID of the automation to update
+ name (NotRequired[str]): Updated automation name
+ status (NotRequired[AutomationStatus]): Updated status
+ steps (NotRequired[List[AutomationStep]]): Updated steps (must be provided together with connections)
+ connections (NotRequired[List[AutomationConnection]]): Updated connections (must be provided together with steps)
+ """
+
+ automation_id: str
+ """
+ The ID of the automation to update.
+ """
+ name: NotRequired[str]
+ """
+ Updated automation name.
+ """
+ status: NotRequired[AutomationStatus]
+ """
+ Updated status.
+ """
+ steps: NotRequired[List[AutomationStep]]
+ """
+ Updated workflow steps. Must be provided together with connections.
+ """
+ connections: NotRequired[List[AutomationConnection]]
+ """
+ Updated connections. Must be provided together with steps.
+ """
+
+ class ListParams(TypedDict):
+ """
+ ListParams is the class that wraps the parameters for the list method.
+
+ Attributes:
+ status (NotRequired[AutomationStatus]): Filter automations by status
+ limit (NotRequired[int]): Number of automations to retrieve (max 100, min 1)
+ after (NotRequired[str]): Return items after this cursor
+ before (NotRequired[str]): Return items before this cursor
+ """
+
+ status: NotRequired[AutomationStatus]
+ """
+ Filter automations by status.
+ """
+ limit: NotRequired[int]
+ """
+ Number of automations to retrieve. Maximum is 100, and minimum is 1.
+ """
+ after: NotRequired[str]
+ """
+ Return items after this cursor (for pagination).
+ Cannot be used with the before parameter.
+ """
+ before: NotRequired[str]
+ """
+ Return items before this cursor (for pagination).
+ Cannot be used with the after parameter.
+ """
+
+ class CreateResponse(BaseResponse):
+ """
+ CreateResponse is the class that wraps the response of the create method.
+
+ Attributes:
+ object (str): The object type, always "automation"
+ id (str): The ID of the created automation
+ """
+
+ object: str
+ """
+ The object type, always "automation".
+ """
+ id: str
+ """
+ The ID of the created automation.
+ """
+
+ class UpdateResponse(BaseResponse):
+ """
+ UpdateResponse is the class that wraps the response of the update method.
+
+ Attributes:
+ object (str): The object type, always "automation"
+ id (str): The ID of the updated automation
+ """
+
+ object: str
+ """
+ The object type, always "automation".
+ """
+ id: str
+ """
+ The ID of the updated automation.
+ """
+
+ class DeleteResponse(BaseResponse):
+ """
+ DeleteResponse is the class that wraps the response of the remove method.
+
+ Attributes:
+ object (str): The object type, always "automation"
+ id (str): The ID of the deleted automation
+ deleted (bool): Whether the automation was successfully deleted
+ """
+
+ object: str
+ """
+ The object type, always "automation".
+ """
+ id: str
+ """
+ The ID of the deleted automation.
+ """
+ deleted: bool
+ """
+ Whether the automation was successfully deleted.
+ """
+
+ class StopResponse(BaseResponse):
+ """
+ StopResponse is the class that wraps the response of the stop method.
+
+ Attributes:
+ object (str): The object type, always "automation"
+ id (str): The ID of the stopped automation
+ status (str): The status after stopping
+ """
+
+ object: str
+ """
+ The object type, always "automation".
+ """
+ id: str
+ """
+ The ID of the stopped automation.
+ """
+ status: str
+ """
+ The status after stopping.
+ """
+
+ class ListResponse(BaseResponse):
+ """
+ ListResponse is the class that wraps the response of the list method.
+
+ Attributes:
+ object (str): The object type, always "list"
+ data (List[AutomationListItem]): A list of automation objects
+ has_more (bool): Whether there are more results available
+ """
+
+ object: str
+ """
+ The object type, always "list".
+ """
+ data: List[AutomationListItem]
+ """
+ A list of automation objects.
+ """
+ has_more: bool
+ """
+ Whether there are more results available for pagination.
+ """
+
+ class Runs:
+ """
+ Sub-namespace for automation run methods.
+ Accessible as resend.Automations.Runs.
+ """
+
+ class ListParams(TypedDict):
+ """
+ ListParams is the class that wraps the parameters for the list method.
+
+ Attributes:
+ status (NotRequired[str]): Comma-separated filter values: "running", "completed", "failed", "cancelled"
+ limit (NotRequired[int]): Number of runs to retrieve (max 100, min 1)
+ after (NotRequired[str]): Return items after this cursor
+ before (NotRequired[str]): Return items before this cursor
+ """
+
+ status: NotRequired[str]
+ """
+ Comma-separated filter values. Valid values: "running", "completed", "failed", "cancelled".
+ """
+ limit: NotRequired[int]
+ """
+ Number of runs to retrieve. Maximum is 100, and minimum is 1.
+ """
+ after: NotRequired[str]
+ """
+ Return items after this cursor (for pagination).
+ Cannot be used with the before parameter.
+ """
+ before: NotRequired[str]
+ """
+ Return items before this cursor (for pagination).
+ Cannot be used with the after parameter.
+ """
+
+ class ListResponse(BaseResponse):
+ """
+ ListResponse is the class that wraps the response of the list method.
+
+ Attributes:
+ object (str): The object type, always "list"
+ data (List[AutomationRunListItem]): A list of automation run objects
+ has_more (bool): Whether there are more results available
+ """
+
+ object: str
+ """
+ The object type, always "list".
+ """
+ data: List[AutomationRunListItem]
+ """
+ A list of automation run objects.
+ """
+ has_more: bool
+ """
+ Whether there are more results available for pagination.
+ """
+
+ @classmethod
+ def list(
+ cls,
+ automation_id: str,
+ params: Optional["Automations.Runs.ListParams"] = None,
+ ) -> "Automations.Runs.ListResponse":
+ """
+ Retrieve a list of runs for an automation.
+ see more: https://resend.com/docs/api-reference/automations/list-automation-runs
+
+ Args:
+ automation_id (str): The automation ID
+ params (Optional[ListParams]): Optional filter and pagination parameters
+ - status: Comma-separated filter values: "running", "completed", "failed", "cancelled"
+ - limit: Number of runs to retrieve (max 100, min 1)
+ - after: Return items after this cursor
+ - before: Return items before this cursor
+
+ Returns:
+ ListResponse: A list of automation run objects
+ """
+ base_path = f"/automations/{automation_id}/runs"
+ query_params = cast(Dict[Any, Any], params) if params else None
+ path = PaginationHelper.build_paginated_path(base_path, query_params)
+ resp = request.Request[Automations.Runs.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def get(cls, automation_id: str, run_id: str) -> AutomationRun:
+ """
+ Retrieve a single automation run.
+ see more: https://resend.com/docs/api-reference/automations/get-automation-run
+
+ Args:
+ automation_id (str): The automation ID
+ run_id (str): The run ID
+
+ Returns:
+ AutomationRun: The automation run object
+ """
+ path = f"/automations/{automation_id}/runs/{run_id}"
+ resp = request.Request[AutomationRun](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def list_async(
+ cls,
+ automation_id: str,
+ params: Optional["Automations.Runs.ListParams"] = None,
+ ) -> "Automations.Runs.ListResponse":
+ """
+ Retrieve a list of runs for an automation (async).
+ see more: https://resend.com/docs/api-reference/automations/list-automation-runs
+
+ Args:
+ automation_id (str): The automation ID
+ params (Optional[ListParams]): Optional filter and pagination parameters
+ - status: Comma-separated filter values: "running", "completed", "failed", "cancelled"
+ - limit: Number of runs to retrieve (max 100, min 1)
+ - after: Return items after this cursor
+ - before: Return items before this cursor
+
+ Returns:
+ ListResponse: A list of automation run objects
+ """
+ base_path = f"/automations/{automation_id}/runs"
+ query_params = cast(Dict[Any, Any], params) if params else None
+ path = PaginationHelper.build_paginated_path(base_path, query_params)
+ resp = await AsyncRequest[Automations.Runs.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def get_async(cls, automation_id: str, run_id: str) -> AutomationRun:
+ """
+ Retrieve a single automation run (async).
+ see more: https://resend.com/docs/api-reference/automations/get-automation-run
+
+ Args:
+ automation_id (str): The automation ID
+ run_id (str): The run ID
+
+ Returns:
+ AutomationRun: The automation run object
+ """
+ path = f"/automations/{automation_id}/runs/{run_id}"
+ resp = await AsyncRequest[AutomationRun](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def create(cls, params: "Automations.CreateParams") -> "Automations.CreateResponse":
+ """
+ Create an automation.
+ see more: https://resend.com/docs/api-reference/automations/create-automation
+
+ Args:
+ params (CreateParams): The automation creation parameters
+
+ Returns:
+ CreateResponse: The created automation response
+ """
+ path = "/automations"
+ resp = request.Request[Automations.CreateResponse](
+ path=path, params=cast(Dict[Any, Any], params), verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def get(cls, automation_id: str) -> Automation:
+ """
+ Retrieve a single automation.
+ see more: https://resend.com/docs/api-reference/automations/get-automation
+
+ Args:
+ automation_id (str): The automation ID
+
+ Returns:
+ Automation: The automation object
+ """
+ path = f"/automations/{automation_id}"
+ resp = request.Request[Automation](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def update(cls, params: "Automations.UpdateParams") -> "Automations.UpdateResponse":
+ """
+ Update an automation.
+ see more: https://resend.com/docs/api-reference/automations/update-automation
+
+ Args:
+ params (UpdateParams): The automation update parameters
+
+ Returns:
+ UpdateResponse: The updated automation response
+ """
+ path = f"/automations/{params['automation_id']}"
+ body = {k: v for k, v in params.items() if k != "automation_id"}
+ resp = request.Request[Automations.UpdateResponse](
+ path=path, params=cast(Dict[Any, Any], body), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def remove(cls, automation_id: str) -> "Automations.DeleteResponse":
+ """
+ Delete an automation.
+ see more: https://resend.com/docs/api-reference/automations/delete-automation
+
+ Args:
+ automation_id (str): The automation ID
+
+ Returns:
+ DeleteResponse: The delete response
+ """
+ path = f"/automations/{automation_id}"
+ resp = request.Request[Automations.DeleteResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def stop(cls, automation_id: str) -> "Automations.StopResponse":
+ """
+ Stop all active runs of an automation.
+ see more: https://resend.com/docs/api-reference/automations/stop-automation
+
+ Args:
+ automation_id (str): The automation ID
+
+ Returns:
+ StopResponse: The stop response
+ """
+ path = f"/automations/{automation_id}/stop"
+ resp = request.Request[Automations.StopResponse](
+ path=path, params={}, verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def list(
+ cls, params: Optional["Automations.ListParams"] = None
+ ) -> "Automations.ListResponse":
+ """
+ Retrieve a list of automations.
+ see more: https://resend.com/docs/api-reference/automations/list-automations
+
+ Args:
+ params (Optional[ListParams]): Optional filter and pagination parameters
+ - status: Filter automations by status ("enabled" or "disabled")
+ - limit: Number of automations to retrieve (max 100, min 1)
+ - after: Return items after this cursor
+ - before: Return items before this cursor
+
+ Returns:
+ ListResponse: A list of automation objects
+ """
+ base_path = "/automations"
+ query_params = cast(Dict[Any, Any], params) if params else None
+ path = PaginationHelper.build_paginated_path(base_path, query_params)
+ resp = request.Request[Automations.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def create_async(
+ cls, params: "Automations.CreateParams"
+ ) -> "Automations.CreateResponse":
+ """
+ Create an automation (async).
+ see more: https://resend.com/docs/api-reference/automations/create-automation
+
+ Args:
+ params (CreateParams): The automation creation parameters
+
+ Returns:
+ CreateResponse: The created automation response
+ """
+ path = "/automations"
+ resp = await AsyncRequest[Automations.CreateResponse](
+ path=path, params=cast(Dict[Any, Any], params), verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def get_async(cls, automation_id: str) -> Automation:
+ """
+ Retrieve a single automation (async).
+ see more: https://resend.com/docs/api-reference/automations/get-automation
+
+ Args:
+ automation_id (str): The automation ID
+
+ Returns:
+ Automation: The automation object
+ """
+ path = f"/automations/{automation_id}"
+ resp = await AsyncRequest[Automation](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def update_async(
+ cls, params: "Automations.UpdateParams"
+ ) -> "Automations.UpdateResponse":
+ """
+ Update an automation (async).
+ see more: https://resend.com/docs/api-reference/automations/update-automation
+
+ Args:
+ params (UpdateParams): The automation update parameters
+
+ Returns:
+ UpdateResponse: The updated automation response
+ """
+ path = f"/automations/{params['automation_id']}"
+ body = {k: v for k, v in params.items() if k != "automation_id"}
+ resp = await AsyncRequest[Automations.UpdateResponse](
+ path=path, params=cast(Dict[Any, Any], body), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def remove_async(cls, automation_id: str) -> "Automations.DeleteResponse":
+ """
+ Delete an automation (async).
+ see more: https://resend.com/docs/api-reference/automations/delete-automation
+
+ Args:
+ automation_id (str): The automation ID
+
+ Returns:
+ DeleteResponse: The delete response
+ """
+ path = f"/automations/{automation_id}"
+ resp = await AsyncRequest[Automations.DeleteResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def stop_async(cls, automation_id: str) -> "Automations.StopResponse":
+ """
+ Stop all active runs of an automation (async).
+ see more: https://resend.com/docs/api-reference/automations/stop-automation
+
+ Args:
+ automation_id (str): The automation ID
+
+ Returns:
+ StopResponse: The stop response
+ """
+ path = f"/automations/{automation_id}/stop"
+ resp = await AsyncRequest[Automations.StopResponse](
+ path=path, params={}, verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def list_async(
+ cls, params: Optional["Automations.ListParams"] = None
+ ) -> "Automations.ListResponse":
+ """
+ Retrieve a list of automations (async).
+ see more: https://resend.com/docs/api-reference/automations/list-automations
+
+ Args:
+ params (Optional[ListParams]): Optional filter and pagination parameters
+ - status: Filter automations by status ("enabled" or "disabled")
+ - limit: Number of automations to retrieve (max 100, min 1)
+ - after: Return items after this cursor
+ - before: Return items before this cursor
+
+ Returns:
+ ListResponse: A list of automation objects
+ """
+ base_path = "/automations"
+ query_params = cast(Dict[Any, Any], params) if params else None
+ path = PaginationHelper.build_paginated_path(base_path, query_params)
+ resp = await AsyncRequest[Automations.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
diff --git a/resend/events/__init__.py b/resend/events/__init__.py
new file mode 100644
index 0000000..52e8379
--- /dev/null
+++ b/resend/events/__init__.py
@@ -0,0 +1,11 @@
+from resend.events._event import (Event, EventListItem, EventSchema,
+ EventSchemaFieldType)
+from resend.events._events import Events
+
+__all__ = [
+ "Event",
+ "EventListItem",
+ "EventSchema",
+ "EventSchemaFieldType",
+ "Events",
+]
diff --git a/resend/events/_event.py b/resend/events/_event.py
new file mode 100644
index 0000000..6ae9935
--- /dev/null
+++ b/resend/events/_event.py
@@ -0,0 +1,88 @@
+from typing import Dict, Union
+
+from typing_extensions import Literal, TypedDict
+
+EventSchemaFieldType = Literal["string", "number", "boolean", "date"]
+"""
+EventSchemaFieldType is the type of a field in an event schema.
+Supported types: "string", "number", "boolean", "date"
+"""
+
+EventSchema = Dict[str, EventSchemaFieldType]
+"""
+EventSchema is a flat key/type map defining the structure of an event payload.
+Keys are field names, values are the field types.
+"""
+
+
+class EventListItem(TypedDict):
+ """
+ EventListItem represents an event in list responses.
+
+ Attributes:
+ id (str): The event ID (UUID)
+ name (str): The event name
+ schema (Union[EventSchema, None]): The event schema definition
+ created_at (str): Creation date/time
+ updated_at (Union[str, None]): Last update date/time
+ """
+
+ id: str
+ """
+ The event ID (UUID).
+ """
+ name: str
+ """
+ The event name.
+ """
+ schema: Union[EventSchema, None]
+ """
+ The event schema definition, or None if no schema is defined.
+ """
+ created_at: str
+ """
+ When the event was created (ISO 8601 format).
+ """
+ updated_at: Union[str, None]
+ """
+ When the event was last updated (ISO 8601 format), or None.
+ """
+
+
+class Event(TypedDict):
+ """
+ Event represents a full event object.
+
+ Attributes:
+ object (str): The object type, always "event"
+ id (str): The event ID (UUID)
+ name (str): The event name
+ schema (Union[EventSchema, None]): The event schema definition
+ created_at (str): Creation date/time
+ updated_at (Union[str, None]): Last update date/time
+ """
+
+ object: str
+ """
+ The object type, always "event".
+ """
+ id: str
+ """
+ The event ID (UUID).
+ """
+ name: str
+ """
+ The event name.
+ """
+ schema: Union[EventSchema, None]
+ """
+ The event schema definition, or None if no schema is defined.
+ """
+ created_at: str
+ """
+ When the event was created (ISO 8601 format).
+ """
+ updated_at: Union[str, None]
+ """
+ When the event was last updated (ISO 8601 format), or None.
+ """
diff --git a/resend/events/_events.py b/resend/events/_events.py
new file mode 100644
index 0000000..dd1890d
--- /dev/null
+++ b/resend/events/_events.py
@@ -0,0 +1,451 @@
+from typing import Any, Dict, List, Optional, Union, cast
+from urllib.parse import quote
+
+from typing_extensions import NotRequired, TypedDict
+
+from resend import request
+from resend._base_response import BaseResponse
+from resend.pagination_helper import PaginationHelper
+
+from ._event import Event, EventListItem, EventSchema
+
+# Async imports (optional - only available with pip install resend[async])
+try:
+ from resend.async_request import AsyncRequest
+except ImportError:
+ pass
+
+
+class Events:
+
+ class CreateParams(TypedDict):
+ """
+ CreateParams is the class that wraps the parameters for the create method.
+
+ Attributes:
+ name (str): The name of the event. Cannot start with the "resend:" prefix.
+ schema (NotRequired[Union[EventSchema, None]]): Flat key/type map defining the payload schema.
+ """
+
+ name: str
+ """
+ The name of the event. Cannot start with the "resend:" prefix.
+ """
+ schema: NotRequired[Union[EventSchema, None]]
+ """
+ Flat key/type map defining the event payload schema.
+ Supported types: "string", "number", "boolean", "date".
+ """
+
+ class UpdateParams(TypedDict):
+ """
+ UpdateParams is the class that wraps the parameters for the update method.
+
+ Attributes:
+ identifier (str): The event ID (UUID) or event name.
+ schema (Union[EventSchema, None]): Updated schema. Set to None to clear the schema.
+ """
+
+ identifier: str
+ """
+ The event ID (UUID) or event name.
+ """
+ schema: Union[EventSchema, None]
+ """
+ Updated schema definition. Set to None to clear the schema.
+ Supported types: "string", "number", "boolean", "date".
+ """
+
+ class SendParams(TypedDict):
+ """
+ SendParams is the class that wraps the parameters for the send method.
+ Exactly one of contact_id or email must be provided.
+
+ Attributes:
+ event (str): The name of the event to send.
+ contact_id (NotRequired[str]): The contact ID to send the event for.
+ email (NotRequired[str]): The email address to send the event for.
+ payload (NotRequired[Dict[str, Any]]): Key/value pairs to include with the event.
+ """
+
+ event: str
+ """
+ The name of the event to send.
+ """
+ contact_id: NotRequired[str]
+ """
+ The contact ID to send the event for.
+ Exactly one of contact_id or email must be provided.
+ """
+ email: NotRequired[str]
+ """
+ The email address to send the event for.
+ Exactly one of contact_id or email must be provided.
+ """
+ payload: NotRequired[Dict[str, Any]]
+ """
+ Key/value pairs to include with the event.
+ """
+
+ class ListParams(TypedDict):
+ """
+ ListParams is the class that wraps the parameters for the list method.
+
+ Attributes:
+ limit (NotRequired[int]): Number of events to retrieve (max 100, min 1)
+ after (NotRequired[str]): Return items after this cursor
+ before (NotRequired[str]): Return items before this cursor
+ """
+
+ limit: NotRequired[int]
+ """
+ Number of events to retrieve. Maximum is 100, and minimum is 1.
+ """
+ after: NotRequired[str]
+ """
+ Return items after this cursor (for pagination).
+ Cannot be used with the before parameter.
+ """
+ before: NotRequired[str]
+ """
+ Return items before this cursor (for pagination).
+ Cannot be used with the after parameter.
+ """
+
+ class CreateResponse(BaseResponse):
+ """
+ CreateResponse is the class that wraps the response of the create method.
+
+ Attributes:
+ object (str): The object type, always "event"
+ id (str): The ID of the created event
+ """
+
+ object: str
+ """
+ The object type, always "event".
+ """
+ id: str
+ """
+ The ID of the created event (UUID).
+ """
+
+ class UpdateResponse(BaseResponse):
+ """
+ UpdateResponse is the class that wraps the response of the update method.
+
+ Attributes:
+ object (str): The object type, always "event"
+ id (str): The ID of the updated event
+ """
+
+ object: str
+ """
+ The object type, always "event".
+ """
+ id: str
+ """
+ The ID of the updated event (UUID).
+ """
+
+ class DeleteResponse(BaseResponse):
+ """
+ DeleteResponse is the class that wraps the response of the remove method.
+
+ Attributes:
+ object (str): The object type, always "event"
+ id (str): The ID of the deleted event
+ deleted (bool): Whether the event was successfully deleted
+ """
+
+ object: str
+ """
+ The object type, always "event".
+ """
+ id: str
+ """
+ The ID of the deleted event (UUID).
+ """
+ deleted: bool
+ """
+ Whether the event was successfully deleted.
+ """
+
+ class SendResponse(BaseResponse):
+ """
+ SendResponse is the class that wraps the response of the send method.
+
+ Attributes:
+ object (str): The object type, always "event"
+ event (str): The name of the event that was sent
+ """
+
+ object: str
+ """
+ The object type, always "event".
+ """
+ event: str
+ """
+ The name of the event that was sent.
+ """
+
+ class ListResponse(BaseResponse):
+ """
+ ListResponse is the class that wraps the response of the list method.
+
+ Attributes:
+ object (str): The object type, always "list"
+ data (List[EventListItem]): A list of event objects
+ has_more (bool): Whether there are more results available
+ """
+
+ object: str
+ """
+ The object type, always "list".
+ """
+ data: List[EventListItem]
+ """
+ A list of event objects.
+ """
+ has_more: bool
+ """
+ Whether there are more results available for pagination.
+ """
+
+ @classmethod
+ def create(cls, params: "Events.CreateParams") -> "Events.CreateResponse":
+ """
+ Create an event definition.
+ see more: https://resend.com/docs/api-reference/events/create-event
+
+ Args:
+ params (CreateParams): The event creation parameters
+
+ Returns:
+ CreateResponse: The created event response
+ """
+ path = "/events"
+ resp = request.Request[Events.CreateResponse](
+ path=path, params=cast(Dict[Any, Any], params), verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def get(cls, identifier: str) -> Event:
+ """
+ Retrieve a single event by ID or name.
+ see more: https://resend.com/docs/api-reference/events/get-event
+
+ Args:
+ identifier (str): The event ID (UUID) or event name
+
+ Returns:
+ Event: The event object
+ """
+ path = f"/events/{quote(identifier, safe='')}"
+ resp = request.Request[Event](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def update(cls, params: "Events.UpdateParams") -> "Events.UpdateResponse":
+ """
+ Update an event definition.
+ see more: https://resend.com/docs/api-reference/events/update-event
+
+ Args:
+ params (UpdateParams): The event update parameters
+
+ Returns:
+ UpdateResponse: The updated event response
+ """
+ path = f"/events/{quote(params['identifier'], safe='')}"
+ body = {k: v for k, v in params.items() if k != "identifier"}
+ resp = request.Request[Events.UpdateResponse](
+ path=path, params=cast(Dict[Any, Any], body), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def remove(cls, identifier: str) -> "Events.DeleteResponse":
+ """
+ Delete an event definition.
+ see more: https://resend.com/docs/api-reference/events/delete-event
+
+ Args:
+ identifier (str): The event ID (UUID) or event name
+
+ Returns:
+ DeleteResponse: The delete response
+ """
+ path = f"/events/{quote(identifier, safe='')}"
+ resp = request.Request[Events.DeleteResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def send(cls, params: "Events.SendParams") -> "Events.SendResponse":
+ """
+ Send an event for a contact.
+ see more: https://resend.com/docs/api-reference/events/send-event
+
+ Args:
+ params (SendParams): The event send parameters.
+ Exactly one of contact_id or email must be provided.
+
+ Returns:
+ SendResponse: The send event response
+ """
+ path = "/events/send"
+ resp = request.Request[Events.SendResponse](
+ path=path, params=cast(Dict[Any, Any], params), verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def list(
+ cls, params: Optional["Events.ListParams"] = None
+ ) -> "Events.ListResponse":
+ """
+ Retrieve a list of event definitions.
+ see more: https://resend.com/docs/api-reference/events/list-events
+
+ Args:
+ params (Optional[ListParams]): Optional pagination parameters
+ - limit: Number of events to retrieve (max 100, min 1)
+ - after: Return items after this cursor
+ - before: Return items before this cursor
+
+ Returns:
+ ListResponse: A list of event objects
+ """
+ base_path = "/events"
+ query_params = cast(Dict[Any, Any], params) if params else None
+ path = PaginationHelper.build_paginated_path(base_path, query_params)
+ resp = request.Request[Events.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def create_async(
+ cls, params: "Events.CreateParams"
+ ) -> "Events.CreateResponse":
+ """
+ Create an event definition (async).
+ see more: https://resend.com/docs/api-reference/events/create-event
+
+ Args:
+ params (CreateParams): The event creation parameters
+
+ Returns:
+ CreateResponse: The created event response
+ """
+ path = "/events"
+ resp = await AsyncRequest[Events.CreateResponse](
+ path=path, params=cast(Dict[Any, Any], params), verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def get_async(cls, identifier: str) -> Event:
+ """
+ Retrieve a single event by ID or name (async).
+ see more: https://resend.com/docs/api-reference/events/get-event
+
+ Args:
+ identifier (str): The event ID (UUID) or event name
+
+ Returns:
+ Event: The event object
+ """
+ path = f"/events/{quote(identifier, safe='')}"
+ resp = await AsyncRequest[Event](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def update_async(
+ cls, params: "Events.UpdateParams"
+ ) -> "Events.UpdateResponse":
+ """
+ Update an event definition (async).
+ see more: https://resend.com/docs/api-reference/events/update-event
+
+ Args:
+ params (UpdateParams): The event update parameters
+
+ Returns:
+ UpdateResponse: The updated event response
+ """
+ path = f"/events/{quote(params['identifier'], safe='')}"
+ body = {k: v for k, v in params.items() if k != "identifier"}
+ resp = await AsyncRequest[Events.UpdateResponse](
+ path=path, params=cast(Dict[Any, Any], body), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def remove_async(cls, identifier: str) -> "Events.DeleteResponse":
+ """
+ Delete an event definition (async).
+ see more: https://resend.com/docs/api-reference/events/delete-event
+
+ Args:
+ identifier (str): The event ID (UUID) or event name
+
+ Returns:
+ DeleteResponse: The delete response
+ """
+ path = f"/events/{quote(identifier, safe='')}"
+ resp = await AsyncRequest[Events.DeleteResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def send_async(cls, params: "Events.SendParams") -> "Events.SendResponse":
+ """
+ Send an event for a contact (async).
+ see more: https://resend.com/docs/api-reference/events/send-event
+
+ Args:
+ params (SendParams): The event send parameters.
+ Exactly one of contact_id or email must be provided.
+
+ Returns:
+ SendResponse: The send event response
+ """
+ path = "/events/send"
+ resp = await AsyncRequest[Events.SendResponse](
+ path=path, params=cast(Dict[Any, Any], params), verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def list_async(
+ cls, params: Optional["Events.ListParams"] = None
+ ) -> "Events.ListResponse":
+ """
+ Retrieve a list of event definitions (async).
+ see more: https://resend.com/docs/api-reference/events/list-events
+
+ Args:
+ params (Optional[ListParams]): Optional pagination parameters
+ - limit: Number of events to retrieve (max 100, min 1)
+ - after: Return items after this cursor
+ - before: Return items before this cursor
+
+ Returns:
+ ListResponse: A list of event objects
+ """
+ base_path = "/events"
+ query_params = cast(Dict[Any, Any], params) if params else None
+ path = PaginationHelper.build_paginated_path(base_path, query_params)
+ resp = await AsyncRequest[Events.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
diff --git a/resend/http_client_httpx.py b/resend/http_client_httpx.py
index d7d6c7d..8ff2373 100644
--- a/resend/http_client_httpx.py
+++ b/resend/http_client_httpx.py
@@ -1,9 +1,9 @@
from typing import Dict, List, Mapping, Optional, Tuple, Union
-from resend.http_client_async import AsyncHTTPClient
-
import httpx
+from resend.http_client_async import AsyncHTTPClient
+
class HTTPXClient(AsyncHTTPClient):
"""
diff --git a/tests/async_response_headers_test.py b/tests/async_response_headers_test.py
index 7db54f7..e83f19a 100644
--- a/tests/async_response_headers_test.py
+++ b/tests/async_response_headers_test.py
@@ -4,7 +4,6 @@
import resend
-
pytestmark = pytest.mark.asyncio
@@ -104,16 +103,16 @@ async def test_received_email_async_headers_not_overwritten_by_http_headers(
resend.default_async_http_client = mock_client
try:
- response = await resend.Emails.Receiving.get_async(
- email_id="email_456"
- )
+ response = await resend.Emails.Receiving.get_async(email_id="email_456")
assert isinstance(response, dict)
assert response["id"] == "email_456"
# MIME email headers must be intact
assert "headers" in response
- assert response["headers"]["List-Unsubscribe"] == ""
+ assert (
+ response["headers"]["List-Unsubscribe"] == ""
+ )
assert response["headers"]["X-Custom"] == "value"
# HTTP response headers must be injected separately under http_headers
diff --git a/tests/automations_async_test.py b/tests/automations_async_test.py
new file mode 100644
index 0000000..7b8cfda
--- /dev/null
+++ b/tests/automations_async_test.py
@@ -0,0 +1,233 @@
+import pytest
+
+import resend
+from resend.exceptions import NoContentError
+from tests.conftest import AsyncResendBaseTest
+
+# flake8: noqa
+
+pytestmark = pytest.mark.asyncio
+
+
+class TestResendAutomationsAsync(AsyncResendBaseTest):
+ async def test_automations_create_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ }
+ )
+
+ params: resend.Automations.CreateParams = {
+ "name": "Welcome Sequence",
+ "steps": [
+ {
+ "key": "trigger_1",
+ "type": "trigger",
+ "config": {"event_name": "user.signed_up"},
+ },
+ ],
+ "connections": [],
+ }
+ automation = await resend.Automations.create_async(params)
+ assert automation["object"] == "automation"
+ assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+
+ async def test_automations_create_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ params: resend.Automations.CreateParams = {
+ "name": "Welcome Sequence",
+ "steps": [
+ {
+ "key": "trigger_1",
+ "type": "trigger",
+ "config": {"event_name": "user.signed_up"},
+ },
+ ],
+ "connections": [],
+ }
+ with pytest.raises(NoContentError):
+ _ = await resend.Automations.create_async(params)
+
+ async def test_automations_get_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "name": "Welcome Sequence",
+ "status": "enabled",
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": "2024-01-02T00:00:00.000Z",
+ "steps": [],
+ "connections": [],
+ }
+ )
+
+ automation = await resend.Automations.get_async(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ )
+ assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ assert automation["name"] == "Welcome Sequence"
+ assert automation["status"] == "enabled"
+
+ async def test_automations_get_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Automations.get_async(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ )
+
+ async def test_automations_update_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ }
+ )
+
+ params: resend.Automations.UpdateParams = {
+ "automation_id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "status": "enabled",
+ }
+ automation = await resend.Automations.update_async(params)
+ assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+
+ async def test_automations_update_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ params: resend.Automations.UpdateParams = {
+ "automation_id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "status": "enabled",
+ }
+ with pytest.raises(NoContentError):
+ _ = await resend.Automations.update_async(params)
+
+ async def test_automations_remove_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "deleted": True,
+ }
+ )
+
+ resp = await resend.Automations.remove_async(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ )
+ assert resp["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ assert resp["deleted"] is True
+
+ async def test_automations_remove_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Automations.remove_async(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ )
+
+ async def test_automations_stop_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "status": "disabled",
+ }
+ )
+
+ resp = await resend.Automations.stop_async(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ )
+ assert resp["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ assert resp["status"] == "disabled"
+
+ async def test_automations_stop_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Automations.stop_async(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ )
+
+ async def test_automations_list_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": False,
+ "data": [
+ {
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "name": "Welcome Sequence",
+ "status": "enabled",
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": "2024-01-02T00:00:00.000Z",
+ },
+ ],
+ }
+ )
+
+ automations = await resend.Automations.list_async()
+ assert automations["object"] == "list"
+ assert automations["has_more"] is False
+ assert len(automations["data"]) == 1
+ assert automations["data"][0]["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+
+ async def test_automations_list_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Automations.list_async()
+
+ async def test_automations_list_runs_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": False,
+ "data": [
+ {
+ "id": "run_123",
+ "status": "completed",
+ "started_at": "2024-01-01T10:00:00.000Z",
+ "completed_at": "2024-01-01T10:05:00.000Z",
+ "created_at": "2024-01-01T09:59:00.000Z",
+ },
+ ],
+ }
+ )
+
+ runs = await resend.Automations.Runs.list_async(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ )
+ assert runs["object"] == "list"
+ assert len(runs["data"]) == 1
+ assert runs["data"][0]["id"] == "run_123"
+ assert runs["data"][0]["status"] == "completed"
+
+ async def test_automations_list_runs_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Automations.Runs.list_async(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ )
+
+ async def test_automations_get_run_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation_run",
+ "id": "run_123",
+ "status": "completed",
+ "started_at": "2024-01-01T10:00:00.000Z",
+ "completed_at": "2024-01-01T10:05:00.000Z",
+ "created_at": "2024-01-01T09:59:00.000Z",
+ "steps": [],
+ }
+ )
+
+ run = await resend.Automations.Runs.get_async(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", "run_123"
+ )
+ assert run["object"] == "automation_run"
+ assert run["id"] == "run_123"
+ assert run["status"] == "completed"
+
+ async def test_automations_get_run_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Automations.Runs.get_async(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", "run_123"
+ )
diff --git a/tests/automations_test.py b/tests/automations_test.py
new file mode 100644
index 0000000..9f09243
--- /dev/null
+++ b/tests/automations_test.py
@@ -0,0 +1,350 @@
+import resend
+from tests.conftest import ResendBaseTest
+
+# flake8: noqa
+
+
+class TestResendAutomations(ResendBaseTest):
+ def test_automations_create(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ }
+ )
+
+ params: resend.Automations.CreateParams = {
+ "name": "Welcome Sequence",
+ "steps": [
+ {
+ "key": "trigger_1",
+ "type": "trigger",
+ "config": {"event_name": "user.signed_up"},
+ },
+ {
+ "key": "email_1",
+ "type": "send_email",
+ "config": {"template": {"id": "tpl_123"}},
+ },
+ ],
+ "connections": [
+ {"from": "trigger_1", "to": "email_1"},
+ ],
+ }
+ automation = resend.Automations.create(params)
+ assert automation["object"] == "automation"
+ assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+
+ def test_automations_create_with_status(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ }
+ )
+
+ params: resend.Automations.CreateParams = {
+ "name": "Welcome Sequence",
+ "status": "enabled",
+ "steps": [
+ {
+ "key": "trigger_1",
+ "type": "trigger",
+ "config": {"event_name": "user.signed_up"},
+ },
+ ],
+ "connections": [],
+ }
+ automation = resend.Automations.create(params)
+ assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+
+ def test_automations_get(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "name": "Welcome Sequence",
+ "status": "enabled",
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": "2024-01-02T00:00:00.000Z",
+ "steps": [
+ {
+ "key": "trigger_1",
+ "type": "trigger",
+ "config": {"event_name": "user.signed_up"},
+ }
+ ],
+ "connections": [],
+ }
+ )
+
+ automation = resend.Automations.get("b6d24b8e-af0b-4c3c-be0c-359bbd97381e")
+ assert automation["object"] == "automation"
+ assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ assert automation["name"] == "Welcome Sequence"
+ assert automation["status"] == "enabled"
+ assert automation["created_at"] == "2024-01-01T00:00:00.000Z"
+ assert automation["updated_at"] == "2024-01-02T00:00:00.000Z"
+ assert len(automation["steps"]) == 1
+ assert automation["steps"][0]["key"] == "trigger_1"
+ assert automation["steps"][0]["type"] == "trigger"
+ assert len(automation["connections"]) == 0
+
+ def test_automations_update(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ }
+ )
+
+ params: resend.Automations.UpdateParams = {
+ "automation_id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "status": "enabled",
+ }
+ automation = resend.Automations.update(params)
+ assert automation["object"] == "automation"
+ assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+
+ def test_automations_update_name(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ }
+ )
+
+ params: resend.Automations.UpdateParams = {
+ "automation_id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "name": "Updated Sequence",
+ }
+ automation = resend.Automations.update(params)
+ assert automation["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+
+ def test_automations_remove(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "deleted": True,
+ }
+ )
+
+ resp = resend.Automations.remove("b6d24b8e-af0b-4c3c-be0c-359bbd97381e")
+ assert resp["object"] == "automation"
+ assert resp["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ assert resp["deleted"] is True
+
+ def test_automations_stop(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation",
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "status": "disabled",
+ }
+ )
+
+ resp = resend.Automations.stop("b6d24b8e-af0b-4c3c-be0c-359bbd97381e")
+ assert resp["object"] == "automation"
+ assert resp["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ assert resp["status"] == "disabled"
+
+ def test_automations_list(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": False,
+ "data": [
+ {
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "name": "Welcome Sequence",
+ "status": "enabled",
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": "2024-01-02T00:00:00.000Z",
+ },
+ {
+ "id": "c7e35c9f-bf1c-5d4d-cf1d-460cce08492f",
+ "name": "Onboarding Flow",
+ "status": "disabled",
+ "created_at": "2024-02-01T00:00:00.000Z",
+ "updated_at": "2024-02-02T00:00:00.000Z",
+ },
+ ],
+ }
+ )
+
+ automations = resend.Automations.list()
+ assert automations["object"] == "list"
+ assert automations["has_more"] is False
+ assert len(automations["data"]) == 2
+
+ first = automations["data"][0]
+ assert first["id"] == "b6d24b8e-af0b-4c3c-be0c-359bbd97381e"
+ assert first["name"] == "Welcome Sequence"
+ assert first["status"] == "enabled"
+ assert first["created_at"] == "2024-01-01T00:00:00.000Z"
+
+ second = automations["data"][1]
+ assert second["id"] == "c7e35c9f-bf1c-5d4d-cf1d-460cce08492f"
+ assert second["status"] == "disabled"
+
+ def test_automations_list_with_status_filter(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": False,
+ "data": [
+ {
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "name": "Welcome Sequence",
+ "status": "enabled",
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": "2024-01-02T00:00:00.000Z",
+ },
+ ],
+ }
+ )
+
+ params: resend.Automations.ListParams = {"status": "enabled"}
+ automations = resend.Automations.list(params=params)
+ assert automations["object"] == "list"
+ assert len(automations["data"]) == 1
+ assert automations["data"][0]["status"] == "enabled"
+
+ def test_automations_list_with_pagination_params(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": True,
+ "data": [
+ {
+ "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
+ "name": "Welcome Sequence",
+ "status": "enabled",
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": "2024-01-02T00:00:00.000Z",
+ },
+ ],
+ }
+ )
+
+ params: resend.Automations.ListParams = {
+ "limit": 10,
+ "after": "some-cursor",
+ }
+ automations = resend.Automations.list(params=params)
+ assert automations["has_more"] is True
+ assert len(automations["data"]) == 1
+
+ def test_automations_list_runs(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": False,
+ "data": [
+ {
+ "id": "run_123",
+ "status": "completed",
+ "started_at": "2024-01-01T10:00:00.000Z",
+ "completed_at": "2024-01-01T10:05:00.000Z",
+ "created_at": "2024-01-01T09:59:00.000Z",
+ },
+ {
+ "id": "run_456",
+ "status": "running",
+ "started_at": "2024-01-02T10:00:00.000Z",
+ "completed_at": None,
+ "created_at": "2024-01-02T09:59:00.000Z",
+ },
+ ],
+ }
+ )
+
+ runs = resend.Automations.Runs.list("b6d24b8e-af0b-4c3c-be0c-359bbd97381e")
+ assert runs["object"] == "list"
+ assert runs["has_more"] is False
+ assert len(runs["data"]) == 2
+
+ first = runs["data"][0]
+ assert first["id"] == "run_123"
+ assert first["status"] == "completed"
+ assert first["started_at"] == "2024-01-01T10:00:00.000Z"
+ assert first["completed_at"] == "2024-01-01T10:05:00.000Z"
+
+ second = runs["data"][1]
+ assert second["id"] == "run_456"
+ assert second["status"] == "running"
+ assert second["completed_at"] is None
+
+ def test_automations_list_runs_with_status_filter(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": False,
+ "data": [
+ {
+ "id": "run_123",
+ "status": "completed",
+ "started_at": "2024-01-01T10:00:00.000Z",
+ "completed_at": "2024-01-01T10:05:00.000Z",
+ "created_at": "2024-01-01T09:59:00.000Z",
+ },
+ ],
+ }
+ )
+
+ params: resend.Automations.Runs.ListParams = {"status": "completed"}
+ runs = resend.Automations.Runs.list(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", params=params
+ )
+ assert len(runs["data"]) == 1
+ assert runs["data"][0]["status"] == "completed"
+
+ def test_automations_get_run(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "automation_run",
+ "id": "run_123",
+ "status": "completed",
+ "started_at": "2024-01-01T10:00:00.000Z",
+ "completed_at": "2024-01-01T10:05:00.000Z",
+ "created_at": "2024-01-01T09:59:00.000Z",
+ "steps": [
+ {
+ "key": "trigger_1",
+ "type": "trigger",
+ "status": "completed",
+ "started_at": "2024-01-01T10:00:00.000Z",
+ "completed_at": "2024-01-01T10:00:01.000Z",
+ "output": None,
+ "error": None,
+ "created_at": "2024-01-01T09:59:00.000Z",
+ },
+ {
+ "key": "email_1",
+ "type": "send_email",
+ "status": "completed",
+ "started_at": "2024-01-01T10:01:00.000Z",
+ "completed_at": "2024-01-01T10:01:02.000Z",
+ "output": None,
+ "error": None,
+ "created_at": "2024-01-01T09:59:00.000Z",
+ },
+ ],
+ }
+ )
+
+ run = resend.Automations.Runs.get(
+ "b6d24b8e-af0b-4c3c-be0c-359bbd97381e", "run_123"
+ )
+ assert run["object"] == "automation_run"
+ assert run["id"] == "run_123"
+ assert run["status"] == "completed"
+ assert run["started_at"] == "2024-01-01T10:00:00.000Z"
+ assert run["completed_at"] == "2024-01-01T10:05:00.000Z"
+ assert len(run["steps"]) == 2
+ assert run["steps"][0]["key"] == "trigger_1"
+ assert run["steps"][0]["type"] == "trigger"
+ assert run["steps"][0]["status"] == "completed"
+ assert run["steps"][1]["key"] == "email_1"
+ assert run["steps"][1]["type"] == "send_email"
diff --git a/tests/events_async_test.py b/tests/events_async_test.py
new file mode 100644
index 0000000..6c1e765
--- /dev/null
+++ b/tests/events_async_test.py
@@ -0,0 +1,148 @@
+import pytest
+
+import resend
+from resend.exceptions import NoContentError
+from tests.conftest import AsyncResendBaseTest
+
+# flake8: noqa
+
+pytestmark = pytest.mark.asyncio
+
+
+class TestResendEventsAsync(AsyncResendBaseTest):
+ async def test_events_create_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ }
+ )
+
+ params: resend.Events.CreateParams = {
+ "name": "user.signed_up",
+ }
+ event = await resend.Events.create_async(params)
+ assert event["object"] == "event"
+ assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf"
+
+ async def test_events_create_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ params: resend.Events.CreateParams = {"name": "user.signed_up"}
+ with pytest.raises(NoContentError):
+ _ = await resend.Events.create_async(params)
+
+ async def test_events_get_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ "name": "user.signed_up",
+ "schema": {"plan": "string"},
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": None,
+ }
+ )
+
+ event = await resend.Events.get_async("user.signed_up")
+ assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf"
+ assert event["name"] == "user.signed_up"
+
+ async def test_events_get_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Events.get_async("user.signed_up")
+
+ async def test_events_update_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ }
+ )
+
+ params: resend.Events.UpdateParams = {
+ "identifier": "user.signed_up",
+ "schema": {"plan": "string"},
+ }
+ event = await resend.Events.update_async(params)
+ assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf"
+
+ async def test_events_update_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ params: resend.Events.UpdateParams = {
+ "identifier": "user.signed_up",
+ "schema": None,
+ }
+ with pytest.raises(NoContentError):
+ _ = await resend.Events.update_async(params)
+
+ async def test_events_remove_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ "deleted": True,
+ }
+ )
+
+ resp = await resend.Events.remove_async("user.signed_up")
+ assert resp["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf"
+ assert resp["deleted"] is True
+
+ async def test_events_remove_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Events.remove_async("user.signed_up")
+
+ async def test_events_send_async_with_contact_id(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "event": "user.signed_up",
+ }
+ )
+
+ params: resend.Events.SendParams = {
+ "event": "user.signed_up",
+ "contact_id": "78b8d3bc-a55a-45a3-aee6-6ec0a5e13d7e",
+ }
+ resp = await resend.Events.send_async(params)
+ assert resp["object"] == "event"
+ assert resp["event"] == "user.signed_up"
+
+ async def test_events_send_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ params: resend.Events.SendParams = {
+ "event": "user.signed_up",
+ "email": "user@example.com",
+ }
+ with pytest.raises(NoContentError):
+ _ = await resend.Events.send_async(params)
+
+ async def test_events_list_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": False,
+ "data": [
+ {
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ "name": "user.signed_up",
+ "schema": None,
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": None,
+ },
+ ],
+ }
+ )
+
+ events = await resend.Events.list_async()
+ assert events["object"] == "list"
+ assert events["has_more"] is False
+ assert len(events["data"]) == 1
+ assert events["data"][0]["name"] == "user.signed_up"
+
+ async def test_events_list_async_raises_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Events.list_async()
diff --git a/tests/events_test.py b/tests/events_test.py
new file mode 100644
index 0000000..d6e3514
--- /dev/null
+++ b/tests/events_test.py
@@ -0,0 +1,245 @@
+import resend
+from tests.conftest import ResendBaseTest
+
+# flake8: noqa
+
+
+class TestResendEvents(ResendBaseTest):
+ def test_events_create(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ }
+ )
+
+ params: resend.Events.CreateParams = {
+ "name": "user.signed_up",
+ }
+ event = resend.Events.create(params)
+ assert event["object"] == "event"
+ assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf"
+
+ def test_events_create_with_schema(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ }
+ )
+
+ params: resend.Events.CreateParams = {
+ "name": "user.signed_up",
+ "schema": {
+ "plan": "string",
+ "trial_days": "number",
+ "is_enterprise": "boolean",
+ },
+ }
+ event = resend.Events.create(params)
+ assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf"
+
+ def test_events_get_by_id(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ "name": "user.signed_up",
+ "schema": {"plan": "string"},
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": None,
+ }
+ )
+
+ event = resend.Events.get("56261eea-8f8b-4381-83c6-79fa7120f1cf")
+ assert event["object"] == "event"
+ assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf"
+ assert event["name"] == "user.signed_up"
+ assert event["schema"] == {"plan": "string"}
+ assert event["created_at"] == "2024-01-01T00:00:00.000Z"
+ assert event["updated_at"] is None
+
+ def test_events_get_by_name(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ "name": "user.signed_up",
+ "schema": None,
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": None,
+ }
+ )
+
+ event = resend.Events.get("user.signed_up")
+ assert event["name"] == "user.signed_up"
+ assert event["schema"] is None
+
+ def test_events_update(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ }
+ )
+
+ params: resend.Events.UpdateParams = {
+ "identifier": "user.signed_up",
+ "schema": {"plan": "string", "amount": "number"},
+ }
+ event = resend.Events.update(params)
+ assert event["object"] == "event"
+ assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf"
+
+ def test_events_update_clear_schema(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ }
+ )
+
+ params: resend.Events.UpdateParams = {
+ "identifier": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ "schema": None,
+ }
+ event = resend.Events.update(params)
+ assert event["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf"
+
+ def test_events_remove(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ "deleted": True,
+ }
+ )
+
+ resp = resend.Events.remove("56261eea-8f8b-4381-83c6-79fa7120f1cf")
+ assert resp["object"] == "event"
+ assert resp["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf"
+ assert resp["deleted"] is True
+
+ def test_events_remove_by_name(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ "deleted": True,
+ }
+ )
+
+ resp = resend.Events.remove("user.signed_up")
+ assert resp["deleted"] is True
+
+ def test_events_send_with_contact_id(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "event": "user.signed_up",
+ }
+ )
+
+ params: resend.Events.SendParams = {
+ "event": "user.signed_up",
+ "contact_id": "78b8d3bc-a55a-45a3-aee6-6ec0a5e13d7e",
+ }
+ resp = resend.Events.send(params)
+ assert resp["object"] == "event"
+ assert resp["event"] == "user.signed_up"
+
+ def test_events_send_with_email(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "event": "user.signed_up",
+ }
+ )
+
+ params: resend.Events.SendParams = {
+ "event": "user.signed_up",
+ "email": "user@example.com",
+ }
+ resp = resend.Events.send(params)
+ assert resp["event"] == "user.signed_up"
+
+ def test_events_send_with_payload(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "event",
+ "event": "user.signed_up",
+ }
+ )
+
+ params: resend.Events.SendParams = {
+ "event": "user.signed_up",
+ "email": "user@example.com",
+ "payload": {"plan": "pro", "trial_days": 14},
+ }
+ resp = resend.Events.send(params)
+ assert resp["event"] == "user.signed_up"
+
+ def test_events_list(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": False,
+ "data": [
+ {
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ "name": "user.signed_up",
+ "schema": {"plan": "string"},
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": None,
+ },
+ {
+ "id": "67372ffb-9g9c-5492-94d7-80gb8231g2dg",
+ "name": "user.upgraded",
+ "schema": None,
+ "created_at": "2024-02-01T00:00:00.000Z",
+ "updated_at": "2024-02-15T00:00:00.000Z",
+ },
+ ],
+ }
+ )
+
+ events = resend.Events.list()
+ assert events["object"] == "list"
+ assert events["has_more"] is False
+ assert len(events["data"]) == 2
+
+ first = events["data"][0]
+ assert first["id"] == "56261eea-8f8b-4381-83c6-79fa7120f1cf"
+ assert first["name"] == "user.signed_up"
+ assert first["schema"] == {"plan": "string"}
+ assert first["updated_at"] is None
+
+ second = events["data"][1]
+ assert second["name"] == "user.upgraded"
+ assert second["schema"] is None
+ assert second["updated_at"] == "2024-02-15T00:00:00.000Z"
+
+ def test_events_list_with_pagination_params(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": True,
+ "data": [
+ {
+ "id": "56261eea-8f8b-4381-83c6-79fa7120f1cf",
+ "name": "user.signed_up",
+ "schema": None,
+ "created_at": "2024-01-01T00:00:00.000Z",
+ "updated_at": None,
+ }
+ ],
+ }
+ )
+
+ params: resend.Events.ListParams = {
+ "limit": 10,
+ "after": "some-cursor",
+ }
+ events = resend.Events.list(params=params)
+ assert events["has_more"] is True
+ assert len(events["data"]) == 1