diff --git a/examples/async/inboxes_async.py b/examples/async/inboxes_async.py
new file mode 100644
index 0000000..efcd85a
--- /dev/null
+++ b/examples/async/inboxes_async.py
@@ -0,0 +1,68 @@
+import asyncio
+import os
+
+import resend
+
+if not os.environ["RESEND_API_KEY"]:
+ raise EnvironmentError("RESEND_API_KEY is missing")
+
+
+async def main() -> None:
+ created: resend.Inboxes.CreateResponse = await resend.Inboxes.create_async(
+ {"email_address": "support@acme.com", "name": "Customer Support"}
+ )
+ inbox_id = created["id"]
+ print(f"Created inbox: {inbox_id}")
+
+ inbox: resend.Inbox = await resend.Inboxes.get_async(inbox_id)
+ print(f"Retrieved inbox: {inbox['email_address']}")
+
+ label: resend.Inboxes.Labels.CreateResponse = (
+ await resend.Inboxes.Labels.create_async(
+ {"inbox_id": inbox_id, "name": "Urgent", "color": "#E93D82"}
+ )
+ )
+
+ threads: resend.Inboxes.Threads.SearchResponse = (
+ await resend.Inboxes.Threads.search_async(
+ inbox_id, {"query": "refund", "folders": ["inbox"]}
+ )
+ )
+ print(f"Found {len(threads['data'])} matching threads")
+
+ if threads["data"]:
+ thread_id = threads["data"][0]["id"]
+ await resend.Inboxes.Threads.Labels.add_async(
+ {"inbox_id": inbox_id, "thread_id": thread_id, "labels": [label["id"]]}
+ )
+ emails: resend.Inboxes.Threads.Emails.ListResponse = (
+ await resend.Inboxes.Threads.Emails.list_async(inbox_id, thread_id)
+ )
+ reply: resend.Inboxes.Threads.Emails.ReplyResponse = (
+ await resend.Inboxes.Threads.Emails.reply_async(
+ {
+ "inbox_id": inbox_id,
+ "thread_id": thread_id,
+ "email_id": emails["data"][0]["id"],
+ "text": "Thanks, we are on it.",
+ }
+ )
+ )
+ print(f"Sent reply: {reply['id']}")
+
+ draft: resend.Inboxes.Drafts.CreateResponse = (
+ await resend.Inboxes.Drafts.create_async(
+ {"inbox_id": inbox_id, "to": "ada@example.org", "text": "Hello"}
+ )
+ )
+ sent: resend.Inboxes.Drafts.SendResponse = await resend.Inboxes.Drafts.send_async(
+ inbox_id, draft["id"]
+ )
+ print(f"Sent draft as email: {sent['id']}")
+
+ await resend.Inboxes.Labels.remove_async(inbox_id, label["id"])
+ await resend.Inboxes.remove_async(inbox_id)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/inboxes.py b/examples/inboxes.py
new file mode 100644
index 0000000..ef7abc1
--- /dev/null
+++ b/examples/inboxes.py
@@ -0,0 +1,169 @@
+import os
+
+import resend
+
+if not os.environ["RESEND_API_KEY"]:
+ raise EnvironmentError("RESEND_API_KEY is missing")
+
+create_params: resend.Inboxes.CreateParams = {
+ "email_address": "support@acme.com",
+ "name": "Customer Support",
+ "from_name": "Ada from Support",
+}
+created: resend.Inboxes.CreateResponse = resend.Inboxes.create(create_params)
+inbox_id = created["id"]
+print(f"Created inbox: {inbox_id}")
+
+inbox: resend.Inbox = resend.Inboxes.get("support@acme.com")
+print(f"Retrieved inbox {inbox['email_address']} with {inbox['unread']} unread threads")
+
+inboxes: resend.Inboxes.ListResponse = resend.Inboxes.list({"limit": 10})
+print(f"Found {len(inboxes['data'])} inboxes")
+
+update_params: resend.Inboxes.UpdateParams = {"id": inbox_id, "name": "Support"}
+resend.Inboxes.update(update_params)
+
+agent: resend.InboxAgentSettings = resend.Inboxes.Agent.get(inbox_id)
+print(f"Agent actions: {agent['actions']}")
+agent_params: resend.Inboxes.Agent.UpdateParams = {
+ "inbox_id": inbox_id,
+ "tone": "concise_casual",
+ "actions": ["draft_reply", "add_labels"],
+}
+resend.Inboxes.Agent.update(agent_params)
+
+label_params: resend.Inboxes.Labels.CreateParams = {
+ "inbox_id": inbox_id,
+ "name": "Urgent",
+ "color": "#E93D82",
+}
+label: resend.Inboxes.Labels.CreateResponse = resend.Inboxes.Labels.create(
+ label_params
+)
+label_update: resend.Inboxes.Labels.UpdateParams = {
+ "inbox_id": inbox_id,
+ "id": label["id"],
+ "color": "#F76B15",
+}
+resend.Inboxes.Labels.update(label_update)
+labels: resend.Inboxes.Labels.ListResponse = resend.Inboxes.Labels.list(inbox_id)
+print(f"Found {len(labels['data'])} labels")
+
+threads: resend.Inboxes.Threads.ListResponse = resend.Inboxes.Threads.list(
+ inbox_id, {"folders": ["inbox", "sent"], "read": False, "limit": 10}
+)
+print(f"Found {len(threads['data'])} unread threads")
+
+search_params: resend.Inboxes.Threads.SearchParams = {
+ "query": "refund",
+ "has_attachment": False,
+}
+results: resend.Inboxes.Threads.SearchResponse = resend.Inboxes.Threads.search(
+ inbox_id, search_params
+)
+for result in results["data"]:
+ print(f"{result['subject']}: matched email {result['matched_email_id']}")
+
+if threads["data"]:
+ thread_id = threads["data"][0]["id"]
+ thread: resend.InboxThreadSummary = resend.Inboxes.Threads.get(inbox_id, thread_id)
+ print(f"Thread {thread['id']} is in {thread['folder']}")
+
+ thread_update: resend.Inboxes.Threads.UpdateParams = {
+ "inbox_id": inbox_id,
+ "id": thread_id,
+ "read": True,
+ }
+ resend.Inboxes.Threads.update(thread_update)
+
+ add_labels: resend.Inboxes.Threads.Labels.AddParams = {
+ "inbox_id": inbox_id,
+ "thread_id": thread_id,
+ "labels": [label["id"]],
+ }
+ thread_labels: resend.Inboxes.Threads.Labels.ListResponse = (
+ resend.Inboxes.Threads.Labels.add(add_labels)
+ )
+ print(f"Thread has {len(thread_labels['data'])} labels")
+ resend.Inboxes.Threads.Labels.remove(inbox_id, thread_id, label["id"])
+
+ emails: resend.Inboxes.Threads.Emails.ListResponse = (
+ resend.Inboxes.Threads.Emails.list(inbox_id, thread_id)
+ )
+ email_id = emails["data"][0]["id"]
+ email: resend.InboxThreadEmail = resend.Inboxes.Threads.Emails.get(
+ inbox_id, thread_id, email_id
+ )
+ print(f"First email from {email['from']}: {email['subject']}")
+
+ reply_params: resend.Inboxes.Threads.Emails.ReplyParams = {
+ "inbox_id": inbox_id,
+ "thread_id": thread_id,
+ "email_id": email_id,
+ "text": "Thanks, we are on it.",
+ "attachments": [{"filename": "note.txt", "content": "VGhhbmtzIQ=="}],
+ }
+ reply: resend.Inboxes.Threads.Emails.ReplyResponse = (
+ resend.Inboxes.Threads.Emails.reply(reply_params)
+ )
+ print(f"Sent reply: {reply['id']}")
+
+ forward_params: resend.Inboxes.Threads.Emails.ForwardParams = {
+ "inbox_id": inbox_id,
+ "thread_id": thread_id,
+ "email_id": email_id,
+ "to": ["colleague@example.org"],
+ "text": "Flagging this for you.",
+ }
+ forward: resend.Inboxes.Threads.Emails.ForwardResponse = (
+ resend.Inboxes.Threads.Emails.forward(forward_params)
+ )
+ print(f"Sent forward: {forward['id']}")
+
+ resend.Inboxes.Threads.remove(inbox_id, thread_id)
+
+draft_params: resend.Inboxes.Drafts.CreateParams = {
+ "inbox_id": inbox_id,
+ "to": ["ada@example.org"],
+ "subject": "Refund for order 1041",
+ "text": "Refund issued for order 1041.",
+ "attachments": [
+ {
+ "content": "UmVmdW5kIHJlY2VpcHQ=",
+ "filename": "receipt.txt",
+ "content_type": "text/plain",
+ }
+ ],
+}
+draft: resend.Inboxes.Drafts.CreateResponse = resend.Inboxes.Drafts.create(
+ draft_params
+)
+draft_update: resend.Inboxes.Drafts.UpdateParams = {
+ "inbox_id": inbox_id,
+ "id": draft["id"],
+ "html": "
Refund issued for order 1041.
",
+}
+updated: resend.Inboxes.Drafts.UpdateResponse = resend.Inboxes.Drafts.update(
+ draft_update
+)
+updated_draft: resend.InboxDraft = resend.Inboxes.Drafts.get(inbox_id, updated["id"])
+print(f"Draft {updated_draft['id']} is a {updated_draft['type']} draft")
+print(f"Draft attachments: {[a['filename'] for a in updated_draft['attachments']]}")
+
+drafts: resend.Inboxes.Drafts.ListResponse = resend.Inboxes.Drafts.list(inbox_id)
+print(f"Found {len(drafts['data'])} drafts")
+
+send_options: resend.Inboxes.Drafts.SendOptions = {
+ "idempotency_key": f"send-draft-{draft['id']}",
+}
+send_params: resend.Inboxes.Drafts.SendParams = {
+ "headers": {"X-Ticket-Id": "4821"},
+}
+sent: resend.Inboxes.Drafts.SendResponse = resend.Inboxes.Drafts.send(
+ inbox_id, draft["id"], send_params, send_options
+)
+print(f"Sent draft as email: {sent['id']}")
+
+resend.Inboxes.Labels.remove(inbox_id, label["id"])
+removed: resend.Inboxes.RemoveResponse = resend.Inboxes.remove(inbox_id)
+print(f"Removed inbox: {removed['deleted']}")
diff --git a/resend/__init__.py b/resend/__init__.py
index 0510536..6e5aa88 100644
--- a/resend/__init__.py
+++ b/resend/__init__.py
@@ -53,6 +53,26 @@
from .http_client_async import \
AsyncHTTPClient # Okay to import AsyncHTTPClient since it is just an interface.
from .http_client_requests import RequestsClient
+from .inboxes._inbox import (Inbox, InboxListItem, InboxMessageFolder,
+ MoveThreadFolder)
+from .inboxes._inboxes import Inboxes
+from .inboxes.agent._inbox_agent import InboxAgent
+from .inboxes.agent._inbox_agent_settings import (InboxAgentAction,
+ InboxAgentSettings,
+ InboxAgentTone)
+from .inboxes.drafts._inbox_draft import (InboxDraft, InboxDraftAttachment,
+ InboxDraftListItem, InboxDraftType)
+from .inboxes.drafts._inbox_drafts import InboxDrafts
+from .inboxes.labels._inbox_label import InboxLabel
+from .inboxes.labels._inbox_labels import InboxLabels
+from .inboxes.threads._inbox_thread import (InboxThread, InboxThreadLabel,
+ InboxThreadSearchResult,
+ InboxThreadSummary)
+from .inboxes.threads._inbox_threads import InboxThreads
+from .inboxes.threads.emails._inbox_thread_email import (
+ InboxThreadEmail, InboxThreadEmailAttachment)
+from .inboxes.threads.emails._inbox_thread_emails import InboxThreadEmails
+from .inboxes.threads.labels._inbox_thread_labels import InboxThreadLabels
from .logs._log import Log
from .logs._logs import Logs
from .oauth_grants._oauth_grant import OAuthGrant, OAuthGrantClient
@@ -91,6 +111,29 @@
EmailFailedEvent, EmailOpenedEvent,
EmailReceivedEvent, EmailScheduledEvent,
EmailSentEvent, EmailSuppressedEvent,
+ InboxCreatedEvent, InboxDeletedEvent,
+ InboxDeletedEventData,
+ InboxDraftCreatedEvent,
+ InboxDraftDeletedEvent,
+ InboxDraftEventData, InboxDraftSentEvent,
+ InboxDraftSentEventData,
+ InboxDraftUpdatedEvent,
+ InboxEmailReceivedEvent,
+ InboxEmailSentEvent, InboxEventData,
+ InboxEventDraft, InboxEventEmail,
+ InboxEventEmailAttachment,
+ InboxEventSource,
+ InboxThreadAssignedEvent,
+ InboxThreadAssignedEventData,
+ InboxThreadCreatedEvent,
+ InboxThreadEmailEventData,
+ InboxThreadFolderUpdatedEvent,
+ InboxThreadFolderUpdatedEventData,
+ InboxThreadLabelsUpdatedEvent,
+ InboxThreadLabelsUpdatedEventData,
+ InboxThreadUnassignedEvent,
+ InboxThreadUnassignedEventData,
+ InboxUpdatedEvent,
ReceivedEmailEventData,
SuppressionAddedEvent,
SuppressionEventData,
@@ -141,6 +184,13 @@
"Suppressions",
"SuppressionsBatch",
"Usage",
+ "Inboxes",
+ "InboxAgent",
+ "InboxLabels",
+ "InboxThreads",
+ "InboxThreadEmails",
+ "InboxThreadLabels",
+ "InboxDrafts",
# Types
"Audience",
"Automation",
@@ -229,6 +279,51 @@
"TopicCreatedEvent",
"TopicUpdatedEvent",
"TopicDeletedEvent",
+ "InboxEventSource",
+ "InboxEventEmail",
+ "InboxEventEmailAttachment",
+ "InboxEventDraft",
+ "InboxEventData",
+ "InboxDeletedEventData",
+ "InboxThreadEmailEventData",
+ "InboxThreadFolderUpdatedEventData",
+ "InboxThreadAssignedEventData",
+ "InboxThreadUnassignedEventData",
+ "InboxThreadLabelsUpdatedEventData",
+ "InboxDraftEventData",
+ "InboxDraftSentEventData",
+ "InboxCreatedEvent",
+ "InboxUpdatedEvent",
+ "InboxDeletedEvent",
+ "InboxThreadCreatedEvent",
+ "InboxEmailReceivedEvent",
+ "InboxEmailSentEvent",
+ "InboxThreadFolderUpdatedEvent",
+ "InboxThreadAssignedEvent",
+ "InboxThreadUnassignedEvent",
+ "InboxThreadLabelsUpdatedEvent",
+ "InboxDraftCreatedEvent",
+ "InboxDraftUpdatedEvent",
+ "InboxDraftSentEvent",
+ "InboxDraftDeletedEvent",
+ "Inbox",
+ "InboxListItem",
+ "InboxMessageFolder",
+ "MoveThreadFolder",
+ "InboxAgentAction",
+ "InboxAgentSettings",
+ "InboxAgentTone",
+ "InboxLabel",
+ "InboxThread",
+ "InboxThreadLabel",
+ "InboxThreadSearchResult",
+ "InboxThreadSummary",
+ "InboxThreadEmail",
+ "InboxThreadEmailAttachment",
+ "InboxDraft",
+ "InboxDraftAttachment",
+ "InboxDraftListItem",
+ "InboxDraftType",
"Topic",
"OAuthGrant",
"OAuthGrantClient",
diff --git a/resend/inboxes/__init__.py b/resend/inboxes/__init__.py
new file mode 100644
index 0000000..3c9da8e
--- /dev/null
+++ b/resend/inboxes/__init__.py
@@ -0,0 +1,53 @@
+from resend.inboxes._inbox import (Inbox, InboxListItem, InboxMessageFolder,
+ MoveThreadFolder)
+from resend.inboxes._inboxes import Inboxes
+from resend.inboxes.agent._inbox_agent import InboxAgent
+from resend.inboxes.agent._inbox_agent_settings import (InboxAgentAction,
+ InboxAgentSettings,
+ InboxAgentTone)
+from resend.inboxes.drafts._inbox_draft import (InboxDraft,
+ InboxDraftAttachment,
+ InboxDraftListItem,
+ InboxDraftType)
+from resend.inboxes.drafts._inbox_drafts import InboxDrafts
+from resend.inboxes.labels._inbox_label import InboxLabel
+from resend.inboxes.labels._inbox_labels import InboxLabels
+from resend.inboxes.threads._inbox_thread import (InboxThread,
+ InboxThreadLabel,
+ InboxThreadSearchResult,
+ InboxThreadSummary)
+from resend.inboxes.threads._inbox_threads import InboxThreads
+from resend.inboxes.threads.emails._inbox_thread_email import (
+ InboxThreadEmail, InboxThreadEmailAttachment)
+from resend.inboxes.threads.emails._inbox_thread_emails import \
+ InboxThreadEmails
+from resend.inboxes.threads.labels._inbox_thread_labels import \
+ InboxThreadLabels
+
+__all__ = [
+ "Inboxes",
+ "InboxAgent",
+ "InboxLabels",
+ "InboxThreads",
+ "InboxThreadEmails",
+ "InboxThreadLabels",
+ "InboxDrafts",
+ "Inbox",
+ "InboxListItem",
+ "InboxMessageFolder",
+ "MoveThreadFolder",
+ "InboxAgentAction",
+ "InboxAgentSettings",
+ "InboxAgentTone",
+ "InboxLabel",
+ "InboxThread",
+ "InboxThreadLabel",
+ "InboxThreadSearchResult",
+ "InboxThreadSummary",
+ "InboxThreadEmail",
+ "InboxThreadEmailAttachment",
+ "InboxDraft",
+ "InboxDraftAttachment",
+ "InboxDraftListItem",
+ "InboxDraftType",
+]
diff --git a/resend/inboxes/_inbox.py b/resend/inboxes/_inbox.py
new file mode 100644
index 0000000..126ed4b
--- /dev/null
+++ b/resend/inboxes/_inbox.py
@@ -0,0 +1,117 @@
+from typing import Optional
+
+from typing_extensions import Literal, TypedDict
+
+from resend._base_response import BaseResponse
+
+InboxMessageFolder = Literal["inbox", "archive", "spam", "sent", "trash"]
+"""
+A folder of an inbox. Every thread is in exactly one folder. ``inbox`` holds
+threads with at least one received email. ``archive`` and ``spam`` hold threads
+moved there. ``trash`` holds threads scheduled for deletion in 30 days. ``sent``
+holds threads whose emails are all outbound, and is assigned automatically.
+"""
+
+MoveThreadFolder = Literal["inbox", "archive", "spam", "trash"]
+"""
+A folder a thread can be moved to. ``sent`` is assigned automatically and
+cannot be set.
+"""
+
+
+class Inbox(BaseResponse):
+ """
+ Inbox is the full inbox object, as returned when you retrieve an inbox.
+ """
+
+ object: str
+ """
+ The object type. Always ``inbox``.
+ """
+ id: str
+ """
+ The ID of the inbox.
+ """
+ name: str
+ """
+ Internal name for the inbox. Recipients do not see it.
+ Falls back to the inbox email address.
+ """
+ email_address: str
+ """
+ The address of the inbox.
+ """
+ domain_id: str
+ """
+ The ID of the domain that owns the address.
+ """
+ receiving_address: Optional[str]
+ """
+ The address to forward mail to when forwarding is enabled. None otherwise.
+ """
+ from_name: Optional[str]
+ """
+ The name recipients see when mail is sent from this inbox. None when not set.
+ """
+ unread: int
+ """
+ The number of unread threads in the Inbox folder.
+ """
+ drafts: int
+ """
+ The number of unsent drafts.
+ """
+ last_received: Optional[str]
+ """
+ ISO 8601 timestamp of the newest received email in the Inbox folder.
+ None when the folder has no received email.
+ """
+ created_at: str
+ """
+ ISO 8601 timestamp when the inbox was created.
+ """
+ updated_at: str
+ """
+ ISO 8601 timestamp when the inbox was last updated.
+ """
+
+
+class InboxListItem(TypedDict):
+ """
+ InboxListItem is an inbox as it appears in the list of inboxes.
+ It carries fewer fields than a single inbox.
+ """
+
+ id: str
+ """
+ The ID of the inbox.
+ """
+ name: Optional[str]
+ """
+ Internal name for the inbox. Recipients do not see it.
+ """
+ email_address: str
+ """
+ The address of the inbox.
+ """
+ from_name: Optional[str]
+ """
+ The name recipients see when mail is sent from this inbox. None when not set.
+ """
+ unread: int
+ """
+ The number of unread threads in the Inbox folder.
+ """
+ last_received: Optional[str]
+ """
+ ISO 8601 timestamp of the newest received email in the Inbox folder.
+ None when the folder has no received email.
+ """
+ created_at: str
+ """
+ ISO 8601 timestamp when the inbox was created.
+ """
+ updated_at: str
+ """
+ ISO 8601 timestamp when the inbox was last updated.
+ """
diff --git a/resend/inboxes/_inboxes.py b/resend/inboxes/_inboxes.py
new file mode 100644
index 0000000..cd3a470
--- /dev/null
+++ b/resend/inboxes/_inboxes.py
@@ -0,0 +1,431 @@
+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.inboxes._inbox import Inbox, InboxListItem
+from resend.inboxes._query import inbox_path, without_keys
+from resend.pagination_helper import PaginationHelper
+
+from .agent._inbox_agent import InboxAgent
+from .drafts._inbox_drafts import InboxDrafts
+from .labels._inbox_labels import InboxLabels
+from .threads._inbox_threads import InboxThreads
+
+# Async imports (optional - only available with pip install resend[async])
+try:
+ from resend.async_request import AsyncRequest
+except ImportError:
+ pass
+
+
+class Inboxes:
+ """
+ Inboxes manages inboxes, which send and receive email at an address on one
+ of your domains, and their agent settings, labels, threads and drafts.
+ """
+
+ class Agent(InboxAgent):
+ pass
+
+ class Labels(InboxLabels):
+ pass
+
+ class Threads(InboxThreads):
+ pass
+
+ class Drafts(InboxDrafts):
+ pass
+
+ class CreateParams(TypedDict):
+ """
+ CreateParams are the parameters for creating an inbox.
+
+ Attributes:
+ email_address (str): The inbox email address on one of your domains.
+ name (NotRequired[str]): Internal name for the inbox.
+ from_name (NotRequired[str]): The name recipients see when mail is sent from this inbox.
+ forwarding (NotRequired[bool]): Provision a receiving address instead of an MX record.
+ """
+
+ email_address: str
+ """
+ The inbox email address on one of your domains. The API trims and
+ lowercases it. The part before the ``@`` is at most 64 characters and
+ contains only letters, numbers, and ``. _ % + -``. It cannot start or end
+ with a dot or contain two dots in a row.
+ """
+ name: NotRequired[str]
+ """
+ Internal name for the inbox. Recipients do not see it. Max 64 characters.
+ Defaults to ``email_address``.
+ """
+ from_name: NotRequired[str]
+ """
+ The name recipients see when mail is sent from this inbox. A plain name,
+ not a ``Name `` address. Max 78 characters. It cannot contain a
+ colon, ``<``, ``>``, line breaks, or other control characters. An email
+ address inside the name must be on a domain the team has verified for
+ sending. An empty string leaves it unset.
+ """
+ forwarding: NotRequired[bool]
+ """
+ When True, Resend provisions a receiving address so you can receive mail
+ without adding an MX record. The address is returned in ``receiving_address``.
+ Defaults to False.
+ """
+
+ class CreateResponse(BaseResponse):
+ """
+ CreateResponse is the response of creating an inbox.
+
+ Attributes:
+ object (str): The object type, always "inbox"
+ id (str): The ID of the inbox
+ """
+
+ object: str
+ """
+ The object type, always "inbox".
+ """
+ id: str
+ """
+ The ID of the inbox.
+ """
+
+ class ListParams(TypedDict):
+ """
+ ListParams are the pagination parameters for listing inboxes.
+
+ Attributes:
+ limit (NotRequired[int]): Number of inboxes to retrieve. Maximum is 100, minimum is 1.
+ after (NotRequired[str]): The ID after which to retrieve more inboxes.
+ before (NotRequired[str]): The ID before which to retrieve more inboxes.
+ """
+
+ limit: NotRequired[int]
+ """
+ Number of inboxes to retrieve. Maximum is 100, and minimum is 1.
+ """
+ after: NotRequired[str]
+ """
+ The ID after which we'll retrieve more inboxes (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 inboxes (for pagination).
+ This ID will not be included in the returned list.
+ Cannot be used with the after parameter.
+ """
+
+ class ListResponse(BaseResponse):
+ """
+ ListResponse is a page of the inboxes of the team.
+
+ Attributes:
+ object (str): The object type, always "list"
+ has_more (bool): Whether there are more results available
+ data (List[InboxListItem]): The inboxes, newest first
+ """
+
+ object: str
+ """
+ The object type, always "list".
+ """
+ has_more: bool
+ """
+ Whether there are more results available for pagination.
+ """
+ data: List[InboxListItem]
+ """
+ The inboxes on this page, newest first.
+ """
+
+ class UpdateParams(TypedDict):
+ """
+ UpdateParams are the parameters for updating an inbox.
+ Provide at least one of ``name`` or ``from_name``.
+
+ Attributes:
+ id (str): The Inbox ID or the inbox email address.
+ name (NotRequired[str]): New internal name for the inbox.
+ from_name (NotRequired[str]): The name recipients see when mail is sent from this inbox.
+ """
+
+ id: str
+ """
+ The Inbox ID or the inbox email address.
+ """
+ name: NotRequired[str]
+ """
+ New internal name for the inbox. Recipients do not see it.
+ Between 1 and 64 characters after trimming.
+ """
+ from_name: NotRequired[str]
+ """
+ The name recipients see when mail is sent from this inbox. A plain name,
+ not a ``Name `` address. Max 78 characters. It cannot contain a
+ colon, ``<``, ``>``, line breaks, or other control characters. An email
+ address inside the name must be on a domain the team has verified for
+ sending. An empty string removes it.
+ """
+
+ class UpdateResponse(BaseResponse):
+ """
+ UpdateResponse is the response of updating an inbox.
+
+ Attributes:
+ object (str): The object type, always "inbox"
+ id (str): The ID of the inbox
+ """
+
+ object: str
+ """
+ The object type, always "inbox".
+ """
+ id: str
+ """
+ The ID of the inbox.
+ """
+
+ class RemoveResponse(BaseResponse):
+ """
+ RemoveResponse is the response of removing an inbox.
+
+ Attributes:
+ object (str): The object type, always "inbox"
+ id (str): The ID of the inbox
+ deleted (bool): Whether the inbox was deleted
+ """
+
+ object: str
+ """
+ The object type, always "inbox".
+ """
+ id: str
+ """
+ The ID of the inbox.
+ """
+ deleted: bool
+ """
+ Indicates whether the inbox was successfully deleted.
+ """
+
+ @classmethod
+ def create(cls, params: CreateParams) -> CreateResponse:
+ """
+ Create an inbox that sends and receives email at an address on one of your
+ domains. The domain must belong to the team. Without ``forwarding``, the
+ domain must have receiving enabled, with DKIM and SPF verified and the
+ inbound MX record verified or pending. With ``forwarding`` set to True, the
+ domain only needs DKIM and SPF verified for sending, and Resend generates a
+ ``receiving_address`` on the team inbound domain. A domain that does not
+ meet these rules returns a 422 validation_error. An address or receiving
+ address that is already in use returns a 409. A team that has reached the
+ inbox limit of its plan returns a 403 validation_error. A blocked account
+ returns a 451.
+ see more: https://resend.com/docs/api-reference/inboxes/create-inbox
+
+ Args:
+ params (CreateParams): The inbox parameters
+
+ Returns:
+ CreateResponse: The object type and the ID of the inbox
+ """
+ path = "/inboxes"
+ resp = request.Request[Inboxes.CreateResponse](
+ path=path, params=cast(Dict[Any, Any], params), verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def list(cls, params: Optional[ListParams] = None) -> ListResponse:
+ """
+ Retrieve the inboxes of the team, newest first. The list items carry fewer
+ fields than a single inbox. Retrieve an inbox to get all its fields. The
+ ``after`` and ``before`` cursors take an inbox ID. A value that is not a
+ UUID returns a 422, and an unknown ID returns a 404.
+ see more: https://resend.com/docs/api-reference/inboxes/list-inboxes
+
+ Args:
+ params (Optional[ListParams]): Optional pagination parameters
+
+ Returns:
+ ListResponse: A page of the inboxes of the team
+ """
+ query_params = cast(Dict[Any, Any], params) if params else None
+ path = PaginationHelper.build_paginated_path("/inboxes", query_params)
+ resp = request.Request[Inboxes.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def get(cls, inbox_id: str) -> Inbox:
+ """
+ Retrieve a single inbox by ID or email address.
+ see more: https://resend.com/docs/api-reference/inboxes/get-inbox
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address, such as
+ ``support@acme.com``. A value that contains ``@`` must be a valid
+ email address, and the match is case-insensitive. Any other value
+ must be a UUID.
+
+ Returns:
+ Inbox: The inbox object
+ """
+ path = inbox_path(inbox_id)
+ resp = request.Request[Inbox](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def update(cls, params: UpdateParams) -> UpdateResponse:
+ """
+ Update the internal name of an inbox or the name recipients see when mail
+ is sent from it. At least one of ``name`` or ``from_name`` is required.
+ see more: https://resend.com/docs/api-reference/inboxes/update-inbox
+
+ Args:
+ params (UpdateParams): The inbox ID and the fields to change
+
+ Returns:
+ UpdateResponse: The object type and the ID of the inbox
+ """
+ path = inbox_path(params["id"])
+ resp = request.Request[Inboxes.UpdateResponse](
+ path=path, params=without_keys(params, "id"), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def remove(cls, inbox_id: str) -> RemoveResponse:
+ """
+ Remove an inbox. An inbox that does not exist or was already removed
+ returns a 404 not_found.
+ see more: https://resend.com/docs/api-reference/inboxes/delete-inbox
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+
+ Returns:
+ RemoveResponse: The inbox ID and its deleted status
+ """
+ path = inbox_path(inbox_id)
+ resp = request.Request[Inboxes.RemoveResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def create_async(cls, params: CreateParams) -> CreateResponse:
+ """
+ Create an inbox that sends and receives email at an address on one of your
+ domains (async). The domain must belong to the team. Without ``forwarding``,
+ the domain must have receiving enabled, with DKIM and SPF verified and the
+ inbound MX record verified or pending. With ``forwarding`` set to True, the
+ domain only needs DKIM and SPF verified for sending, and Resend generates a
+ ``receiving_address`` on the team inbound domain. A domain that does not
+ meet these rules returns a 422 validation_error. An address or receiving
+ address that is already in use returns a 409. A team that has reached the
+ inbox limit of its plan returns a 403 validation_error. A blocked account
+ returns a 451.
+ see more: https://resend.com/docs/api-reference/inboxes/create-inbox
+
+ Args:
+ params (CreateParams): The inbox parameters
+
+ Returns:
+ CreateResponse: The object type and the ID of the inbox
+ """
+ path = "/inboxes"
+ resp = await AsyncRequest[Inboxes.CreateResponse](
+ path=path, params=cast(Dict[Any, Any], params), verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def list_async(cls, params: Optional[ListParams] = None) -> ListResponse:
+ """
+ Retrieve the inboxes of the team, newest first (async). The list items
+ carry fewer fields than a single inbox. Retrieve an inbox to get all its
+ fields. The ``after`` and ``before`` cursors take an inbox ID. A value that
+ is not a UUID returns a 422, and an unknown ID returns a 404.
+ see more: https://resend.com/docs/api-reference/inboxes/list-inboxes
+
+ Args:
+ params (Optional[ListParams]): Optional pagination parameters
+
+ Returns:
+ ListResponse: A page of the inboxes of the team
+ """
+ query_params = cast(Dict[Any, Any], params) if params else None
+ path = PaginationHelper.build_paginated_path("/inboxes", query_params)
+ resp = await AsyncRequest[Inboxes.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def get_async(cls, inbox_id: str) -> Inbox:
+ """
+ Retrieve a single inbox by ID or email address (async).
+ see more: https://resend.com/docs/api-reference/inboxes/get-inbox
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address, such as
+ ``support@acme.com``. A value that contains ``@`` must be a valid
+ email address, and the match is case-insensitive. Any other value
+ must be a UUID.
+
+ Returns:
+ Inbox: The inbox object
+ """
+ path = inbox_path(inbox_id)
+ resp = await AsyncRequest[Inbox](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def update_async(cls, params: UpdateParams) -> UpdateResponse:
+ """
+ Update the internal name of an inbox or the name recipients see when mail
+ is sent from it (async). At least one of ``name`` or ``from_name`` is required.
+ see more: https://resend.com/docs/api-reference/inboxes/update-inbox
+
+ Args:
+ params (UpdateParams): The inbox ID and the fields to change
+
+ Returns:
+ UpdateResponse: The object type and the ID of the inbox
+ """
+ path = inbox_path(params["id"])
+ resp = await AsyncRequest[Inboxes.UpdateResponse](
+ path=path, params=without_keys(params, "id"), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def remove_async(cls, inbox_id: str) -> RemoveResponse:
+ """
+ Remove an inbox (async). An inbox that does not exist or was already
+ removed returns a 404 not_found.
+ see more: https://resend.com/docs/api-reference/inboxes/delete-inbox
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+
+ Returns:
+ RemoveResponse: The inbox ID and its deleted status
+ """
+ path = inbox_path(inbox_id)
+ resp = await AsyncRequest[Inboxes.RemoveResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
diff --git a/resend/inboxes/_query.py b/resend/inboxes/_query.py
new file mode 100644
index 0000000..edd2bf2
--- /dev/null
+++ b/resend/inboxes/_query.py
@@ -0,0 +1,51 @@
+from typing import Any, Dict, Mapping, Optional
+from urllib.parse import quote
+
+from resend.pagination_helper import PaginationHelper
+
+
+def build_inbox_query_path(path: str, params: Optional[Mapping[str, Any]] = None) -> str:
+ """
+ Build a path with the query parameters of an inbox list or search call.
+ Lists become comma-separated values, and empty lists are left out.
+ Booleans become the literal ``true`` or ``false`` that the API accepts.
+
+ Args:
+ path: The base API path (e.g., "/inboxes/{inbox_id}/threads")
+ params: Optional query parameters
+
+ Returns:
+ The path with the query parameters appended, if any are set
+ """
+ if not params:
+ return path
+
+ query: Dict[str, Any] = {}
+ for key, value in params.items():
+ if value is None:
+ continue
+ if isinstance(value, bool):
+ query[key] = "true" if value else "false"
+ elif isinstance(value, (list, tuple)):
+ if value:
+ query[key] = ",".join(str(item) for item in value)
+ else:
+ query[key] = value
+
+ return PaginationHelper.build_paginated_path(path, query)
+
+
+def without_keys(params: Mapping[str, Any], *keys: str) -> Dict[str, Any]:
+ """
+ Return a copy of the params without the given keys. The SDK puts the
+ path IDs in the params dict, but the API takes them in the path only.
+ """
+ return {k: v for k, v in params.items() if k not in keys}
+
+
+def inbox_path(inbox_id: str) -> str:
+ """
+ Return the path of an inbox. The API takes the Inbox ID or the inbox email
+ address, and an address must be percent-encoded to fit in the path.
+ """
+ return f"/inboxes/{quote(inbox_id, safe='')}"
diff --git a/resend/inboxes/agent/__init__.py b/resend/inboxes/agent/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resend/inboxes/agent/_inbox_agent.py b/resend/inboxes/agent/_inbox_agent.py
new file mode 100644
index 0000000..4c5193b
--- /dev/null
+++ b/resend/inboxes/agent/_inbox_agent.py
@@ -0,0 +1,151 @@
+from typing import List, Optional
+
+from typing_extensions import NotRequired, TypedDict
+
+from resend import request
+from resend._base_response import BaseResponse
+from resend.inboxes._query import inbox_path, without_keys
+
+from ._inbox_agent_settings import (InboxAgentAction, InboxAgentSettings,
+ InboxAgentTone)
+
+# Async imports (optional - only available with pip install resend[async])
+try:
+ from resend.async_request import AsyncRequest
+except ImportError:
+ pass
+
+
+class InboxAgent:
+ """
+ InboxAgent manages the agent settings of an inbox.
+ """
+
+ class UpdateParams(TypedDict):
+ """
+ UpdateParams are the parameters for updating the agent settings of an inbox.
+ At least one of ``instructions``, ``tone``, or ``actions`` must be
+ provided. Omitted fields keep their current value.
+
+ Attributes:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ instructions (NotRequired[Optional[str]]): Instructions for the agent.
+ tone (NotRequired[Optional[InboxAgentTone]]): The tone the agent writes in.
+ actions (NotRequired[List[InboxAgentAction]]): The actions the agent can take.
+ """
+
+ inbox_id: str
+ """
+ The Inbox ID or the inbox email address.
+ """
+ instructions: NotRequired[Optional[str]]
+ """
+ Instructions the agent follows when it handles the threads of the inbox.
+ Max 4000 characters. Leading and trailing whitespace is trimmed, and the
+ trimmed value must not be empty. Send None to clear the instructions.
+ """
+ tone: NotRequired[Optional[InboxAgentTone]]
+ """
+ The tone the agent writes in. Send None to clear the tone.
+ """
+ actions: NotRequired[List[InboxAgentAction]]
+ """
+ The actions the agent can take. Replaces the whole set. Duplicates are
+ removed. Send an empty list to disable all actions.
+ """
+
+ class UpdateResponse(BaseResponse):
+ """
+ UpdateResponse is the response of updating the agent settings of an inbox.
+
+ Attributes:
+ object (str): The object type. Always "inbox_agent".
+ id (str): The ID of the inbox agent.
+ """
+
+ object: str
+ """
+ The object type. Always ``inbox_agent``.
+ """
+ id: str
+ """
+ The ID of the inbox agent.
+ """
+
+ @classmethod
+ def get(cls, inbox_id: str) -> InboxAgentSettings:
+ """
+ Retrieve the agent settings of an inbox. An inbox without a configured
+ agent returns empty settings: ``instructions`` and ``tone`` are None and
+ ``actions`` is an empty list.
+ see more: https://resend.com/docs/api-reference/inboxes/get-agent
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+
+ Returns:
+ InboxAgentSettings: The agent settings of the inbox
+ """
+ path = f"{inbox_path(inbox_id)}/agent"
+ resp = request.Request[InboxAgentSettings](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def update(cls, params: UpdateParams) -> UpdateResponse:
+ """
+ Update the agent settings of an inbox. The agent is created when the
+ inbox has none. Omitted fields keep their current value.
+ see more: https://resend.com/docs/api-reference/inboxes/update-agent
+
+ Args:
+ params (UpdateParams): The inbox ID and the settings to change
+
+ Returns:
+ UpdateResponse: The object type and the ID of the inbox agent
+ """
+ path = f"{inbox_path(params['inbox_id'])}/agent"
+ resp = request.Request[InboxAgent.UpdateResponse](
+ path=path, params=without_keys(params, "inbox_id"), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def get_async(cls, inbox_id: str) -> InboxAgentSettings:
+ """
+ Retrieve the agent settings of an inbox (async). An inbox without a
+ configured agent returns empty settings: ``instructions`` and ``tone``
+ are None and ``actions`` is an empty list.
+ see more: https://resend.com/docs/api-reference/inboxes/get-agent
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+
+ Returns:
+ InboxAgentSettings: The agent settings of the inbox
+ """
+ path = f"{inbox_path(inbox_id)}/agent"
+ resp = await AsyncRequest[InboxAgentSettings](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def update_async(cls, params: UpdateParams) -> UpdateResponse:
+ """
+ Update the agent settings of an inbox (async). The agent is created when
+ the inbox has none. Omitted fields keep their current value.
+ see more: https://resend.com/docs/api-reference/inboxes/update-agent
+
+ Args:
+ params (UpdateParams): The inbox ID and the settings to change
+
+ Returns:
+ UpdateResponse: The object type and the ID of the inbox agent
+ """
+ path = f"{inbox_path(params['inbox_id'])}/agent"
+ resp = await AsyncRequest[InboxAgent.UpdateResponse](
+ path=path, params=without_keys(params, "inbox_id"), verb="patch"
+ ).perform_with_content()
+ return resp
diff --git a/resend/inboxes/agent/_inbox_agent_settings.py b/resend/inboxes/agent/_inbox_agent_settings.py
new file mode 100644
index 0000000..8ebfca3
--- /dev/null
+++ b/resend/inboxes/agent/_inbox_agent_settings.py
@@ -0,0 +1,52 @@
+from typing import List, Optional
+
+from typing_extensions import Literal
+
+from resend._base_response import BaseResponse
+
+InboxAgentAction = Literal[
+ "draft_reply",
+ "forward_thread",
+ "add_labels",
+ "assign_thread",
+ "mark_as_spam",
+ "archive",
+ "move_to_trash",
+]
+"""
+An action the inbox agent can take.
+"""
+
+InboxAgentTone = Literal[
+ "concise_professional",
+ "concise_casual",
+ "expanded_professional",
+ "expanded_casual",
+]
+"""
+A tone preset the inbox agent writes in.
+"""
+
+
+class InboxAgentSettings(BaseResponse):
+ """
+ InboxAgentSettings are the agent settings of an inbox.
+ """
+
+ object: str
+ """
+ The object type. Always ``inbox_agent``.
+ """
+ instructions: Optional[str]
+ """
+ Instructions the agent follows when it handles the threads of the inbox.
+ None when no instructions are set.
+ """
+ tone: Optional[InboxAgentTone]
+ """
+ The tone the agent writes in. None when no tone is set.
+ """
+ actions: List[InboxAgentAction]
+ """
+ The actions the agent can take. Empty when no actions are enabled.
+ """
diff --git a/resend/inboxes/drafts/__init__.py b/resend/inboxes/drafts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resend/inboxes/drafts/_inbox_draft.py b/resend/inboxes/drafts/_inbox_draft.py
new file mode 100644
index 0000000..375c520
--- /dev/null
+++ b/resend/inboxes/drafts/_inbox_draft.py
@@ -0,0 +1,155 @@
+from typing import List, Optional
+
+from typing_extensions import Literal, TypedDict
+
+from resend._base_response import BaseResponse
+
+InboxDraftType = Literal["standalone", "reply"]
+"""
+``reply`` when the draft replies to an email in an existing thread.
+``standalone`` when the draft starts a new thread.
+"""
+
+
+class InboxDraftAttachment(TypedDict):
+ """
+ InboxDraftAttachment is a file stored on a draft.
+ """
+
+ id: str
+ """
+ The ID of the attachment.
+ """
+ filename: str
+ """
+ The filename of the attachment.
+ """
+ content_type: str
+ """
+ The MIME type of the attachment.
+ """
+ size: int
+ """
+ The size of the attachment in bytes.
+ """
+
+
+class InboxDraft(BaseResponse):
+ """
+ InboxDraft is an unsent or sent draft of an inbox.
+ """
+
+ object: str
+ """
+ The object type. Always ``inbox_draft``.
+ """
+ id: str
+ """
+ The ID of the draft.
+ """
+ type: InboxDraftType
+ """
+ ``reply`` when the draft replies to an email in an existing thread.
+ ``standalone`` when the draft starts a new thread.
+ """
+ to: Optional[List[str]]
+ """
+ The recipients. None or empty when no recipients are set.
+ """
+ cc: List[str]
+ """
+ The CC recipients.
+ """
+ bcc: List[str]
+ """
+ The BCC recipients.
+ """
+ subject: Optional[str]
+ """
+ The subject.
+ """
+ html: Optional[str]
+ """
+ The HTML body.
+ """
+ text: Optional[str]
+ """
+ The plain text body.
+ """
+ thread_id: Optional[str]
+ """
+ The Thread ID when the draft is a reply. None otherwise.
+ """
+ reply_to_email_id: Optional[str]
+ """
+ The Email ID the draft replies to. None otherwise.
+ """
+ email_id: Optional[str]
+ """
+ The ID of the outbound email queued when the draft was sent.
+ None until the draft is sent.
+ """
+ created_at: str
+ """
+ Timestamp when the draft was created.
+ """
+ updated_at: str
+ """
+ Timestamp when the draft was last saved.
+ """
+ attachments: List[InboxDraftAttachment]
+ """
+ The files stored on the draft.
+ """
+
+
+class InboxDraftListItem(TypedDict):
+ """
+ InboxDraftListItem is a draft as it appears in the list of drafts.
+ """
+
+ id: str
+ """
+ The ID of the draft.
+ """
+ type: InboxDraftType
+ """
+ ``reply`` when the draft replies to an email in an existing thread.
+ ``standalone`` when the draft starts a new thread.
+ """
+ to: Optional[List[str]]
+ """
+ The recipients. None or empty when no recipients are set.
+ """
+ cc: List[str]
+ """
+ The CC recipients.
+ """
+ bcc: List[str]
+ """
+ The BCC recipients.
+ """
+ subject: Optional[str]
+ """
+ The subject.
+ """
+ snippet: Optional[str]
+ """
+ A short preview of the body.
+ """
+ thread_id: Optional[str]
+ """
+ The Thread ID when the draft is a reply. None otherwise.
+ """
+ reply_to_email_id: Optional[str]
+ """
+ The Email ID the draft replies to. None otherwise.
+ """
+ updated_at: str
+ """
+ Timestamp when the draft was last saved.
+ """
+ attachments: List[InboxDraftAttachment]
+ """
+ The files stored on the draft.
+ """
diff --git a/resend/inboxes/drafts/_inbox_drafts.py b/resend/inboxes/drafts/_inbox_drafts.py
new file mode 100644
index 0000000..701601e
--- /dev/null
+++ b/resend/inboxes/drafts/_inbox_drafts.py
@@ -0,0 +1,671 @@
+from typing import Any, Dict, List, Optional, Union, cast
+
+from typing_extensions import NotRequired, TypedDict
+
+from resend import request
+from resend._base_response import BaseResponse
+from resend.inboxes._query import (build_inbox_query_path, inbox_path,
+ without_keys)
+
+from ._inbox_draft import InboxDraft, InboxDraftListItem
+
+# Async imports (optional - only available with pip install resend[async])
+try:
+ from resend.async_request import AsyncRequest
+except ImportError:
+ pass
+
+
+class InboxDrafts:
+ """
+ InboxDrafts manages the drafts of an inbox.
+ """
+
+ class AttachmentParams(TypedDict):
+ """
+ AttachmentParams is a file to store on a draft. Each one needs
+ ``content``, ``filename`` and ``content_type``. A ``path`` or
+ ``content_id`` returns a 422.
+
+ Attributes:
+ content (str): The content of the file, encoded in base64.
+ filename (str): The name of the file.
+ content_type (str): The MIME type of the file.
+ """
+
+ content: str
+ """
+ The content of the file, encoded in base64. It cannot be empty.
+ """
+ filename: str
+ """
+ The name of the file.
+ """
+ content_type: str
+ """
+ The MIME type of the file, for example ``application/pdf``.
+ """
+
+ class ListParams(TypedDict):
+ """
+ ListParams are the pagination parameters for listing drafts.
+ The ``after`` and ``before`` cursors take a Draft ID from the current page.
+
+ Attributes:
+ limit (NotRequired[int]): Number of drafts to retrieve. Maximum is 100, minimum is 1.
+ after (NotRequired[str]): The Draft ID after which to retrieve more drafts.
+ before (NotRequired[str]): The Draft ID before which to retrieve more drafts.
+ """
+
+ limit: NotRequired[int]
+ """
+ Number of drafts to retrieve. Maximum is 100, and minimum is 1.
+ """
+ after: NotRequired[str]
+ """
+ The Draft ID after which we'll retrieve more drafts (for pagination).
+ Cannot be used with the before parameter.
+ """
+ before: NotRequired[str]
+ """
+ The Draft ID before which we'll retrieve more drafts (for pagination).
+ Cannot be used with the after parameter.
+ """
+
+ class ListResponse(BaseResponse):
+ """
+ ListResponse is a page of the unsent drafts on an inbox.
+
+ Attributes:
+ object (str): The object type, always "list"
+ has_more (bool): Whether there are more results available
+ data (List[InboxDraftListItem]): The unsent drafts, most recently updated first
+ """
+
+ object: str
+ """
+ The object type, always "list".
+ """
+ has_more: bool
+ """
+ Whether there are more results available for pagination.
+ """
+ data: List[InboxDraftListItem]
+ """
+ The unsent drafts on this page, most recently updated first.
+ """
+
+ class CreateParams(TypedDict):
+ """
+ CreateParams are the parameters for creating a draft. At least one of
+ ``to``, ``cc``, ``bcc``, ``subject``, ``text``, ``html`` or ``attachments``
+ must be non-empty.
+ The combined recipients of ``to``, ``cc`` and ``bcc`` cannot exceed 50.
+ ``thread_id`` and ``reply_to_email_id`` must be sent together or not at all.
+
+ Attributes:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ to (NotRequired[Optional[Union[str, List[str]]]]): The recipients.
+ cc (NotRequired[Optional[Union[str, List[str]]]]): The CC recipients.
+ bcc (NotRequired[Optional[Union[str, List[str]]]]): The BCC recipients.
+ subject (NotRequired[Optional[str]]): The subject.
+ html (NotRequired[Optional[str]]): The HTML body.
+ text (NotRequired[Optional[str]]): The plain text body.
+ attachments (NotRequired[List[AttachmentParams]]): Files to store on the draft.
+ thread_id (NotRequired[str]): The Thread ID to reply in.
+ reply_to_email_id (NotRequired[str]): The Email ID to reply to.
+ """
+
+ inbox_id: str
+ """
+ The Inbox ID or the inbox email address.
+ """
+ to: NotRequired[Optional[Union[str, List[str]]]]
+ """
+ The recipients. For multiple addresses, send a list of strings. Each value
+ can include a friendly name, for example ``Ada Lovelace ``.
+ """
+ cc: NotRequired[Optional[Union[str, List[str]]]]
+ """
+ The CC recipients. For multiple addresses, send a list of strings.
+ """
+ bcc: NotRequired[Optional[Union[str, List[str]]]]
+ """
+ The BCC recipients. For multiple addresses, send a list of strings.
+ """
+ subject: NotRequired[Optional[str]]
+ """
+ The subject. Max 2000 characters. A string that is only whitespace, or
+ that contains a line break, is rejected.
+ """
+ html: NotRequired[Optional[str]]
+ """
+ The HTML body.
+ """
+ text: NotRequired[Optional[str]]
+ """
+ The plain text body.
+ """
+ attachments: NotRequired[List["InboxDrafts.AttachmentParams"]]
+ """
+ Files to store on the draft. Each one needs ``content``, ``filename`` and
+ ``content_type``. A ``path`` or ``content_id`` returns a 422. The files on
+ a draft can total at most 25 MB. An empty file or a blocked file type
+ returns a 422.
+ """
+ thread_id: NotRequired[str]
+ """
+ The Thread ID to reply in. Must be sent with ``reply_to_email_id``.
+ """
+ reply_to_email_id: NotRequired[str]
+ """
+ The Email ID to reply to, as returned in ``data[].id`` by the list thread
+ emails endpoint. Must be sent with ``thread_id``.
+ """
+
+ class CreateOptions(TypedDict):
+ """
+ CreateOptions is the class that wraps the options for the create method.
+
+ Attributes:
+ idempotency_key (NotRequired[str]): Unique key that ensures the same operation is not processed multiple times.
+ Allows for safe retries without duplicating operations.
+ If provided, will be sent as the `Idempotency-Key` header.
+ """
+
+ idempotency_key: NotRequired[str]
+ """
+ Unique key that ensures the same operation is not processed multiple times.
+ Allows for safe retries without duplicating operations.
+ If provided, will be sent as the `Idempotency-Key` header.
+ Between 1 and 256 characters, so the draft is created only once. A retry with the
+ same key within 24 hours replays the first successful (2xx) response,
+ with the ``idempotent-replayed: true`` header. A failed request is not
+ stored, so you can retry it with the same key. Reusing the key with a
+ different body, or while the first request is still running, returns a 409.
+ """
+
+ class CreateResponse(BaseResponse):
+ """
+ CreateResponse is the response of creating a draft.
+
+ Attributes:
+ object (str): The object type, always "inbox_draft"
+ id (str): The ID of the draft
+ """
+
+ object: str
+ """
+ The object type, always "inbox_draft".
+ """
+ id: str
+ """
+ The ID of the draft.
+ """
+
+ class UpdateParams(TypedDict):
+ """
+ UpdateParams are the parameters for updating a draft. Send at least one of
+ ``to``, ``cc``, ``bcc``, ``subject``, ``text``, ``html`` or ``attachments``.
+ Omitted fields are left unchanged. Setting ``text`` or ``html`` replaces the whole body, so
+ the body part you omit becomes None. The merged draft must contain at least
+ one non-empty field, and its combined recipients cannot exceed 50.
+
+ Attributes:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ id (str): The Draft ID.
+ to (NotRequired[Optional[Union[str, List[str]]]]): The recipients.
+ cc (NotRequired[Optional[Union[str, List[str]]]]): The CC recipients.
+ bcc (NotRequired[Optional[Union[str, List[str]]]]): The BCC recipients.
+ subject (NotRequired[Optional[str]]): The subject.
+ html (NotRequired[Optional[str]]): The HTML body.
+ text (NotRequired[Optional[str]]): The plain text body.
+ attachments (NotRequired[Optional[List[AttachmentParams]]]): Files that replace
+ every attachment on the draft.
+ """
+
+ inbox_id: str
+ """
+ The Inbox ID or the inbox email address.
+ """
+ id: str
+ """
+ The Draft ID.
+ """
+ to: NotRequired[Optional[Union[str, List[str]]]]
+ """
+ The recipients. For multiple addresses, send a list of strings. Each value
+ can include a friendly name. Pass None to clear.
+ """
+ cc: NotRequired[Optional[Union[str, List[str]]]]
+ """
+ The CC recipients. Pass None or ``[]`` to clear.
+ """
+ bcc: NotRequired[Optional[Union[str, List[str]]]]
+ """
+ The BCC recipients. Pass None or ``[]`` to clear.
+ """
+ subject: NotRequired[Optional[str]]
+ """
+ The subject. Max 2000 characters. Must not contain a line break.
+ Pass None to clear.
+ """
+ html: NotRequired[Optional[str]]
+ """
+ The HTML body. Pass None to clear.
+ """
+ text: NotRequired[Optional[str]]
+ """
+ The plain text body. Pass None to clear.
+ """
+ attachments: NotRequired[Optional[List["InboxDrafts.AttachmentParams"]]]
+ """
+ Files to store on the draft. Each one needs ``content``, ``filename`` and
+ ``content_type``. A ``path`` or ``content_id`` returns a 422. The files on
+ a draft can total at most 25 MB. An empty file or a blocked file type
+ returns a 422. Sending the field replaces every attachment on the draft.
+ Send ``[]`` or None to remove them all.
+ """
+
+ class UpdateResponse(BaseResponse):
+ """
+ UpdateResponse is the response of updating a draft.
+
+ Attributes:
+ object (str): The object type, always "inbox_draft"
+ id (str): The ID of the draft
+ """
+
+ object: str
+ """
+ The object type, always "inbox_draft".
+ """
+ id: str
+ """
+ The ID of the draft.
+ """
+
+ class RemoveResponse(BaseResponse):
+ """
+ RemoveResponse is the response of removing a draft.
+
+ Attributes:
+ object (str): The object type, always "inbox_draft"
+ id (str): The ID of the draft
+ deleted (bool): Whether the draft was deleted
+ """
+
+ object: str
+ """
+ The object type, always "inbox_draft".
+ """
+ id: str
+ """
+ The ID of the draft.
+ """
+ deleted: bool
+ """
+ Indicates whether the draft was successfully deleted.
+ """
+
+ class SendParams(TypedDict):
+ """
+ SendParams are the optional body parameters for sending a draft.
+
+ Attributes:
+ headers (NotRequired[Dict[str, str]]): Custom headers to be added to the email.
+ """
+
+ headers: NotRequired[Dict[str, str]]
+ """
+ Custom headers to be added to the email. The ``In-Reply-To``,
+ ``References`` and ``Reply-To`` headers are set by Resend and cannot be
+ set here. Sending one of them, in any letter case, returns a 422. Header
+ names cannot be empty, and names and values cannot contain line breaks
+ or null characters, or the request returns a 422.
+ """
+
+ class SendOptions(TypedDict):
+ """
+ SendOptions is the class that wraps the options for the send method.
+
+ Attributes:
+ idempotency_key (NotRequired[str]): Unique key that ensures the same operation is not processed multiple times.
+ Allows for safe retries without duplicating operations.
+ If provided, will be sent as the `Idempotency-Key` header.
+ """
+
+ idempotency_key: NotRequired[str]
+ """
+ Unique key that ensures the same operation is not processed multiple times.
+ Allows for safe retries without duplicating operations.
+ If provided, will be sent as the `Idempotency-Key` header.
+ Between 1 and 256 characters, so the draft is sent only once. A retry with the
+ same key within 24 hours replays the first successful (2xx) response,
+ with the ``idempotent-replayed: true`` header. A failed request is not
+ stored, so you can retry it with the same key. Reusing the key with a
+ different body, or while the first request is still running, returns a 409.
+ """
+
+ class SendResponse(BaseResponse):
+ """
+ SendResponse is the response of sending a draft.
+
+ Attributes:
+ id (str): The ID of the sent email
+ """
+
+ id: str
+ """
+ The ID of the sent email. Use it with the emails endpoints.
+ """
+
+ @classmethod
+ def list(cls, inbox_id: str, params: Optional[ListParams] = None) -> ListResponse:
+ """
+ Retrieve the unsent drafts on an inbox, most recently updated first.
+ A cursor that is not an unsent draft on this inbox returns a 404.
+ see more: https://resend.com/docs/api-reference/inboxes/list-drafts
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ params (Optional[ListParams]): Optional pagination parameters
+
+ Returns:
+ ListResponse: A page of the unsent drafts
+ """
+ path = build_inbox_query_path(
+ f"{inbox_path(inbox_id)}/drafts",
+ cast(Dict[Any, Any], params) if params else None,
+ )
+ resp = request.Request[InboxDrafts.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def create(
+ cls, params: CreateParams, options: Optional[CreateOptions] = None
+ ) -> CreateResponse:
+ """
+ Create an unsent draft on an inbox. Omit ``thread_id`` and
+ ``reply_to_email_id`` for a draft that starts a new thread. Pass both
+ together for a reply draft in an existing thread. A reply draft without
+ ``to`` is addressed to the sender of the email it replies to, or to its
+ Reply-To when set. For an email the inbox sent, it defaults to that email's
+ ``to`` recipients. Send ``to: []`` to leave it empty. Attachments are
+ stored on the draft, so each one needs ``content``, ``filename`` and
+ ``content_type``. Only one unsent reply draft can target an email, so a
+ reply draft request for an email that already has one returns the ID of
+ the existing draft. That request ignores every other field, except
+ ``attachments``, which replaces the attachments of the existing draft. A
+ reply draft created without ``subject`` stores an empty subject and gets
+ no ``Re:`` prefix, so set one before you send it.
+ see more: https://resend.com/docs/api-reference/inboxes/create-draft
+
+ Args:
+ params (CreateParams): The inbox ID and the content of the draft
+ options (Optional[CreateOptions]): The create options, ie: idempotency_key
+
+ Returns:
+ CreateResponse: The object type and the ID of the draft
+ """
+ path = f"{inbox_path(params['inbox_id'])}/drafts"
+ resp = request.Request[InboxDrafts.CreateResponse](
+ path=path,
+ params=without_keys(params, "inbox_id"),
+ verb="post",
+ options=cast(Dict[Any, Any], options),
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def get(cls, inbox_id: str, draft_id: str) -> InboxDraft:
+ """
+ Retrieve a single draft.
+ see more: https://resend.com/docs/api-reference/inboxes/get-draft
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ draft_id (str): The Draft ID.
+
+ Returns:
+ InboxDraft: The draft
+ """
+ path = f"{inbox_path(inbox_id)}/drafts/{draft_id}"
+ resp = request.Request[InboxDraft](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def update(cls, params: UpdateParams) -> UpdateResponse:
+ """
+ Update the content of an unsent draft. Omitted fields are left unchanged.
+ Sending ``attachments`` replaces every attachment on the draft. A request
+ that sets ``text`` or ``html`` replaces the whole body. The body part you
+ omit from that request becomes None. A draft that was already sent, or
+ that changed during the request, returns a 409.
+ see more: https://resend.com/docs/api-reference/inboxes/update-draft
+
+ Args:
+ params (UpdateParams): The inbox ID, the draft ID and the fields to change
+
+ Returns:
+ UpdateResponse: The object type and the ID of the draft
+ """
+ path = f"{inbox_path(params['inbox_id'])}/drafts/{params['id']}"
+ resp = request.Request[InboxDrafts.UpdateResponse](
+ path=path, params=without_keys(params, "inbox_id", "id"), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def remove(cls, inbox_id: str, draft_id: str) -> RemoveResponse:
+ """
+ Discard an unsent draft. A draft that was already sent, or that changed
+ during the request, returns a 409.
+ see more: https://resend.com/docs/api-reference/inboxes/delete-draft
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ draft_id (str): The Draft ID.
+
+ Returns:
+ RemoveResponse: The draft ID and its deleted status
+ """
+ path = f"{inbox_path(inbox_id)}/drafts/{draft_id}"
+ resp = request.Request[InboxDrafts.RemoveResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def send(
+ cls,
+ inbox_id: str,
+ draft_id: str,
+ params: Optional[SendParams] = None,
+ options: Optional[SendOptions] = None,
+ ) -> SendResponse:
+ """
+ Send a draft from the inbox address. A reply draft is sent in its thread.
+ A draft without a thread starts a new thread. The draft must have at least
+ one ``to`` recipient, a non-empty ``subject``, and ``html`` or ``text``,
+ or the request returns a 422. A draft that was already sent returns a 409.
+ The request body is optional and takes only ``headers``.
+ see more: https://resend.com/docs/api-reference/inboxes/send-draft
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ draft_id (str): The Draft ID.
+ params (Optional[SendParams]): Optional body parameters, ie: headers
+ options (Optional[SendOptions]): The send options, ie: idempotency_key
+
+ Returns:
+ SendResponse: The ID of the sent email
+ """
+ path = f"{inbox_path(inbox_id)}/drafts/{draft_id}/send"
+ resp = request.Request[InboxDrafts.SendResponse](
+ path=path,
+ params=cast(Dict[Any, Any], params) if params else {},
+ verb="post",
+ options=cast(Dict[Any, Any], options),
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def list_async(
+ cls, inbox_id: str, params: Optional[ListParams] = None
+ ) -> ListResponse:
+ """
+ Retrieve the unsent drafts on an inbox, most recently updated first (async).
+ A cursor that is not an unsent draft on this inbox returns a 404.
+ see more: https://resend.com/docs/api-reference/inboxes/list-drafts
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ params (Optional[ListParams]): Optional pagination parameters
+
+ Returns:
+ ListResponse: A page of the unsent drafts
+ """
+ path = build_inbox_query_path(
+ f"{inbox_path(inbox_id)}/drafts",
+ cast(Dict[Any, Any], params) if params else None,
+ )
+ resp = await AsyncRequest[InboxDrafts.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def create_async(
+ cls, params: CreateParams, options: Optional[CreateOptions] = None
+ ) -> CreateResponse:
+ """
+ Create an unsent draft on an inbox (async). Omit ``thread_id`` and
+ ``reply_to_email_id`` for a draft that starts a new thread. Pass both
+ together for a reply draft in an existing thread. A reply draft without
+ ``to`` is addressed to the sender of the email it replies to, or to its
+ Reply-To when set. For an email the inbox sent, it defaults to that email's
+ ``to`` recipients. Send ``to: []`` to leave it empty. Attachments are
+ stored on the draft, so each one needs ``content``, ``filename`` and
+ ``content_type``. Only one unsent reply draft can target an email, so a
+ reply draft request for an email that already has one returns the ID of
+ the existing draft. That request ignores every other field, except
+ ``attachments``, which replaces the attachments of the existing draft. A
+ reply draft created without ``subject`` stores an empty subject and gets
+ no ``Re:`` prefix, so set one before you send it.
+ see more: https://resend.com/docs/api-reference/inboxes/create-draft
+
+ Args:
+ params (CreateParams): The inbox ID and the content of the draft
+ options (Optional[CreateOptions]): The create options, ie: idempotency_key
+
+ Returns:
+ CreateResponse: The object type and the ID of the draft
+ """
+ path = f"{inbox_path(params['inbox_id'])}/drafts"
+ resp = await AsyncRequest[InboxDrafts.CreateResponse](
+ path=path,
+ params=without_keys(params, "inbox_id"),
+ verb="post",
+ options=cast(Dict[Any, Any], options),
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def get_async(cls, inbox_id: str, draft_id: str) -> InboxDraft:
+ """
+ Retrieve a single draft (async).
+ see more: https://resend.com/docs/api-reference/inboxes/get-draft
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ draft_id (str): The Draft ID.
+
+ Returns:
+ InboxDraft: The draft
+ """
+ path = f"{inbox_path(inbox_id)}/drafts/{draft_id}"
+ resp = await AsyncRequest[InboxDraft](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def update_async(cls, params: UpdateParams) -> UpdateResponse:
+ """
+ Update the content of an unsent draft (async). Omitted fields are left
+ unchanged. Sending ``attachments`` replaces every attachment on the draft.
+ A request that sets ``text`` or ``html`` replaces the whole body.
+ The body part you omit from that request becomes None. A draft that was
+ already sent, or that changed during the request, returns a 409.
+ see more: https://resend.com/docs/api-reference/inboxes/update-draft
+
+ Args:
+ params (UpdateParams): The inbox ID, the draft ID and the fields to change
+
+ Returns:
+ UpdateResponse: The object type and the ID of the draft
+ """
+ path = f"{inbox_path(params['inbox_id'])}/drafts/{params['id']}"
+ resp = await AsyncRequest[InboxDrafts.UpdateResponse](
+ path=path, params=without_keys(params, "inbox_id", "id"), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def remove_async(cls, inbox_id: str, draft_id: str) -> RemoveResponse:
+ """
+ Discard an unsent draft (async). A draft that was already sent, or that
+ changed during the request, returns a 409.
+ see more: https://resend.com/docs/api-reference/inboxes/delete-draft
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ draft_id (str): The Draft ID.
+
+ Returns:
+ RemoveResponse: The draft ID and its deleted status
+ """
+ path = f"{inbox_path(inbox_id)}/drafts/{draft_id}"
+ resp = await AsyncRequest[InboxDrafts.RemoveResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def send_async(
+ cls,
+ inbox_id: str,
+ draft_id: str,
+ params: Optional[SendParams] = None,
+ options: Optional[SendOptions] = None,
+ ) -> SendResponse:
+ """
+ Send a draft from the inbox address (async). A reply draft is sent in its
+ thread. A draft without a thread starts a new thread. The draft must have
+ at least one ``to`` recipient, a non-empty ``subject``, and ``html`` or
+ ``text``, or the request returns a 422. A draft that was already sent
+ returns a 409. The request body is optional and takes only ``headers``.
+ see more: https://resend.com/docs/api-reference/inboxes/send-draft
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ draft_id (str): The Draft ID.
+ params (Optional[SendParams]): Optional body parameters, ie: headers
+ options (Optional[SendOptions]): The send options, ie: idempotency_key
+
+ Returns:
+ SendResponse: The ID of the sent email
+ """
+ path = f"{inbox_path(inbox_id)}/drafts/{draft_id}/send"
+ resp = await AsyncRequest[InboxDrafts.SendResponse](
+ path=path,
+ params=cast(Dict[Any, Any], params) if params else {},
+ verb="post",
+ options=cast(Dict[Any, Any], options),
+ ).perform_with_content()
+ return resp
diff --git a/resend/inboxes/labels/__init__.py b/resend/inboxes/labels/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resend/inboxes/labels/_inbox_label.py b/resend/inboxes/labels/_inbox_label.py
new file mode 100644
index 0000000..6def41f
--- /dev/null
+++ b/resend/inboxes/labels/_inbox_label.py
@@ -0,0 +1,24 @@
+from typing_extensions import TypedDict
+
+
+class InboxLabel(TypedDict):
+ """
+ InboxLabel is a named, colored tag that belongs to one inbox.
+ """
+
+ id: str
+ """
+ The ID of the label.
+ """
+ name: str
+ """
+ The name of the label.
+ """
+ color: str
+ """
+ The color of the label, as a ``#RRGGBB`` hex color in uppercase.
+ """
+ created_at: str
+ """
+ Timestamp when the label was created.
+ """
diff --git a/resend/inboxes/labels/_inbox_labels.py b/resend/inboxes/labels/_inbox_labels.py
new file mode 100644
index 0000000..fa9f74f
--- /dev/null
+++ b/resend/inboxes/labels/_inbox_labels.py
@@ -0,0 +1,309 @@
+from typing import List
+
+from typing_extensions import NotRequired, TypedDict
+
+from resend import request
+from resend._base_response import BaseResponse
+from resend.inboxes._query import inbox_path, without_keys
+
+from ._inbox_label import InboxLabel
+
+# Async imports (optional - only available with pip install resend[async])
+try:
+ from resend.async_request import AsyncRequest
+except ImportError:
+ pass
+
+
+class InboxLabels:
+ """
+ InboxLabels manages the labels of an inbox. A label is a named, colored tag
+ that belongs to one inbox.
+ """
+
+ class ListResponse(BaseResponse):
+ """
+ ListResponse is the list of labels on an inbox. The list is not paginated.
+
+ Attributes:
+ object (str): The object type, always "list"
+ data (List[InboxLabel]): The labels on the inbox, oldest first
+ """
+
+ object: str
+ """
+ The object type, always "list".
+ """
+ data: List[InboxLabel]
+ """
+ The labels on the inbox, oldest first.
+ """
+
+ class CreateParams(TypedDict):
+ """
+ CreateParams are the parameters for creating a label on an inbox.
+
+ Attributes:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ name (str): The name of the label.
+ color (NotRequired[str]): The color of the label, as a #RRGGBB hex color.
+ """
+
+ inbox_id: str
+ """
+ The Inbox ID or the inbox email address.
+ """
+ name: str
+ """
+ The name of the label. Max 64 characters. Leading and trailing whitespace
+ is trimmed. Must not match another label on the inbox, compared without case.
+ """
+ color: NotRequired[str]
+ """
+ The color of the label, as a ``#RRGGBB`` hex color. It is stored in uppercase.
+ When omitted, a random color from the dashboard palette is used.
+ """
+
+ class CreateResponse(BaseResponse):
+ """
+ CreateResponse is the response of creating a label.
+
+ Attributes:
+ object (str): The object type, always "inbox_label"
+ id (str): The ID of the label
+ """
+
+ object: str
+ """
+ The object type, always "inbox_label".
+ """
+ id: str
+ """
+ The ID of the label.
+ """
+
+ class UpdateParams(TypedDict):
+ """
+ UpdateParams are the parameters for updating a label.
+ Omitted fields keep their current value.
+
+ Attributes:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ id (str): The Label ID.
+ name (NotRequired[str]): A new name for the label.
+ color (NotRequired[str]): A new color for the label, as a #RRGGBB hex color.
+ """
+
+ inbox_id: str
+ """
+ The Inbox ID or the inbox email address.
+ """
+ id: str
+ """
+ The Label ID.
+ """
+ name: NotRequired[str]
+ """
+ A new name for the label. Max 64 characters. Leading and trailing whitespace
+ is trimmed. Must not match another label on the inbox, compared without case.
+ """
+ color: NotRequired[str]
+ """
+ A new color for the label, as a ``#RRGGBB`` hex color. It is stored in uppercase.
+ """
+
+ class UpdateResponse(BaseResponse):
+ """
+ UpdateResponse is the response of updating a label.
+
+ Attributes:
+ object (str): The object type, always "inbox_label"
+ id (str): The ID of the updated label
+ """
+
+ object: str
+ """
+ The object type, always "inbox_label".
+ """
+ id: str
+ """
+ The ID of the updated label.
+ """
+
+ class RemoveResponse(BaseResponse):
+ """
+ RemoveResponse is the response of removing a label.
+
+ Attributes:
+ object (str): The object type, always "inbox_label"
+ id (str): The ID of the removed label
+ deleted (bool): Whether the label was deleted
+ """
+
+ object: str
+ """
+ The object type, always "inbox_label".
+ """
+ id: str
+ """
+ The ID of the removed label.
+ """
+ deleted: bool
+ """
+ Indicates whether the label was deleted.
+ """
+
+ @classmethod
+ def list(cls, inbox_id: str) -> ListResponse:
+ """
+ Retrieve every label on an inbox, oldest first. The list is not paginated.
+ see more: https://resend.com/docs/api-reference/inboxes/list-labels
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+
+ Returns:
+ ListResponse: The labels on the inbox
+ """
+ path = f"{inbox_path(inbox_id)}/labels"
+ resp = request.Request[InboxLabels.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def create(cls, params: CreateParams) -> CreateResponse:
+ """
+ Create a label on an inbox. An inbox can have at most 100 labels. Label
+ names are unique per inbox, compared without case. A duplicate name or a
+ full inbox returns a 422 validation_error.
+ see more: https://resend.com/docs/api-reference/inboxes/create-label
+
+ Args:
+ params (CreateParams): The inbox ID, the label name and an optional color
+
+ Returns:
+ CreateResponse: The object type and the ID of the label
+ """
+ path = f"{inbox_path(params['inbox_id'])}/labels"
+ resp = request.Request[InboxLabels.CreateResponse](
+ path=path, params=without_keys(params, "inbox_id"), verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def update(cls, params: UpdateParams) -> UpdateResponse:
+ """
+ Rename or recolor a label. Omitted fields keep their current value. A new
+ name must not match another label on the inbox, compared without case, or
+ the request returns a 422 validation_error.
+ see more: https://resend.com/docs/api-reference/inboxes/update-label
+
+ Args:
+ params (UpdateParams): The inbox ID, the label ID and the fields to change
+
+ Returns:
+ UpdateResponse: The object type and the ID of the updated label
+ """
+ path = f"{inbox_path(params['inbox_id'])}/labels/{params['id']}"
+ resp = request.Request[InboxLabels.UpdateResponse](
+ path=path, params=without_keys(params, "inbox_id", "id"), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def remove(cls, inbox_id: str, label_id: str) -> RemoveResponse:
+ """
+ Delete a label and remove it from every thread that carries it.
+ see more: https://resend.com/docs/api-reference/inboxes/delete-label
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ label_id (str): The Label ID.
+
+ Returns:
+ RemoveResponse: The removed label ID and its deleted status
+ """
+ path = f"{inbox_path(inbox_id)}/labels/{label_id}"
+ resp = request.Request[InboxLabels.RemoveResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def list_async(cls, inbox_id: str) -> ListResponse:
+ """
+ Retrieve every label on an inbox, oldest first (async).
+ The list is not paginated.
+ see more: https://resend.com/docs/api-reference/inboxes/list-labels
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+
+ Returns:
+ ListResponse: The labels on the inbox
+ """
+ path = f"{inbox_path(inbox_id)}/labels"
+ resp = await AsyncRequest[InboxLabels.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def create_async(cls, params: CreateParams) -> CreateResponse:
+ """
+ Create a label on an inbox (async). An inbox can have at most 100 labels.
+ Label names are unique per inbox, compared without case. A duplicate name
+ or a full inbox returns a 422 validation_error.
+ see more: https://resend.com/docs/api-reference/inboxes/create-label
+
+ Args:
+ params (CreateParams): The inbox ID, the label name and an optional color
+
+ Returns:
+ CreateResponse: The object type and the ID of the label
+ """
+ path = f"{inbox_path(params['inbox_id'])}/labels"
+ resp = await AsyncRequest[InboxLabels.CreateResponse](
+ path=path, params=without_keys(params, "inbox_id"), verb="post"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def update_async(cls, params: UpdateParams) -> UpdateResponse:
+ """
+ Rename or recolor a label (async). Omitted fields keep their current value.
+ A new name must not match another label on the inbox, compared without
+ case, or the request returns a 422 validation_error.
+ see more: https://resend.com/docs/api-reference/inboxes/update-label
+
+ Args:
+ params (UpdateParams): The inbox ID, the label ID and the fields to change
+
+ Returns:
+ UpdateResponse: The object type and the ID of the updated label
+ """
+ path = f"{inbox_path(params['inbox_id'])}/labels/{params['id']}"
+ resp = await AsyncRequest[InboxLabels.UpdateResponse](
+ path=path, params=without_keys(params, "inbox_id", "id"), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def remove_async(cls, inbox_id: str, label_id: str) -> RemoveResponse:
+ """
+ Delete a label and remove it from every thread that carries it (async).
+ see more: https://resend.com/docs/api-reference/inboxes/delete-label
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ label_id (str): The Label ID.
+
+ Returns:
+ RemoveResponse: The removed label ID and its deleted status
+ """
+ path = f"{inbox_path(inbox_id)}/labels/{label_id}"
+ resp = await AsyncRequest[InboxLabels.RemoveResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
diff --git a/resend/inboxes/threads/__init__.py b/resend/inboxes/threads/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resend/inboxes/threads/_inbox_thread.py b/resend/inboxes/threads/_inbox_thread.py
new file mode 100644
index 0000000..74a7934
--- /dev/null
+++ b/resend/inboxes/threads/_inbox_thread.py
@@ -0,0 +1,137 @@
+from typing import List, Optional
+
+from typing_extensions import TypedDict
+
+from resend._base_response import BaseResponse
+from resend.inboxes._inbox import InboxMessageFolder
+
+
+class InboxThreadLabel(TypedDict):
+ """
+ InboxThreadLabel is a label attached to a thread.
+ """
+
+ id: str
+ """
+ The ID of the label.
+ """
+ name: str
+ """
+ The name of the label.
+ """
+ color: str
+ """
+ The color of the label, as a ``#RRGGBB`` hex color in uppercase.
+ """
+
+
+# Functional syntax required because ``from`` is a reserved keyword.
+_InboxThreadFrom = TypedDict(
+ "_InboxThreadFrom",
+ {
+ "from": Optional[str],
+ },
+)
+
+
+class InboxThread(_InboxThreadFrom):
+ """
+ InboxThread is a thread as it appears in the list of an inbox's threads.
+ The ``from`` key holds the sender email address, and can include the display name.
+ """
+
+ id: str
+ """
+ The ID of the thread.
+ """
+ subject: Optional[str]
+ """
+ The subject of the thread.
+ """
+ to: List[str]
+ """
+ The recipients of the most recent email. For a received email, these are
+ the addresses it was delivered to, not the full ``To`` header.
+ """
+ cc: List[str]
+ """
+ The CC recipients of the most recent email.
+ """
+ bcc: List[str]
+ """
+ The BCC recipients of the most recent email. Always empty for a received email.
+ """
+ labels: List[InboxThreadLabel]
+ """
+ The labels attached to the thread.
+ """
+ email_count: int
+ """
+ The number of emails in the thread.
+ """
+ has_attachment: bool
+ """
+ Whether the most recent email in the thread has an attachment.
+ """
+ has_draft: bool
+ """
+ Whether the thread has an unsent draft.
+ """
+ read: bool
+ """
+ True only when every email in the thread is read.
+ """
+ received_at: str
+ """
+ Timestamp when the thread was last active.
+ """
+ folder: InboxMessageFolder
+ """
+ The folder the thread is in. Every thread is in exactly one folder.
+ """
+
+
+class InboxThreadSearchResult(InboxThread):
+ """
+ InboxThreadSearchResult is a thread returned by the search threads endpoint.
+ """
+
+ matched_email_id: Optional[str]
+ """
+ The ID of the email in the thread that matched the search. When ``query``
+ has words or phrases, it is the most relevant match. Otherwise, it is the
+ newest. It is the same ``id`` that the list thread emails endpoint returns,
+ so use it as ``email_id`` with the get, reply and forward endpoints.
+ None when the search index returns no matched email for the thread.
+ """
+
+
+class InboxThreadSummary(BaseResponse):
+ """
+ InboxThreadSummary is the summary of a single thread.
+ """
+
+ object: str
+ """
+ The object type. Always ``inbox_thread``.
+ """
+ id: str
+ """
+ The ID of the thread.
+ """
+ subject: Optional[str]
+ """
+ The subject of the thread.
+ """
+ folder: InboxMessageFolder
+ """
+ The folder the thread is in.
+ """
+ labels: List[InboxThreadLabel]
+ """
+ The labels attached to the thread.
+ """
+ read: bool
+ """
+ True only when every email in the thread is read.
+ """
diff --git a/resend/inboxes/threads/_inbox_threads.py b/resend/inboxes/threads/_inbox_threads.py
new file mode 100644
index 0000000..c8a13e1
--- /dev/null
+++ b/resend/inboxes/threads/_inbox_threads.py
@@ -0,0 +1,498 @@
+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.inboxes._inbox import InboxMessageFolder, MoveThreadFolder
+from resend.inboxes._query import (build_inbox_query_path, inbox_path,
+ without_keys)
+
+from ._inbox_thread import (InboxThread, InboxThreadSearchResult,
+ InboxThreadSummary)
+from .emails._inbox_thread_emails import InboxThreadEmails
+from .labels._inbox_thread_labels import InboxThreadLabels
+
+# Async imports (optional - only available with pip install resend[async])
+try:
+ from resend.async_request import AsyncRequest
+except ImportError:
+ pass
+
+
+class _ListParams(TypedDict):
+ folders: NotRequired[List[InboxMessageFolder]]
+ """
+ Folders to list, for example ``["inbox", "sent"]``. A thread is returned when
+ any of its emails is in one of the folders, so a thread in ``inbox`` with an
+ email you sent matches both ``inbox`` and ``sent``. Defaults to ``inbox``, or
+ to ``inbox,archive,sent`` when ``labels`` is set. ``spam`` and ``trash`` are
+ only included when you name them.
+ """
+ labels: NotRequired[List[str]]
+ """
+ Label IDs. Returns threads with any of these labels. Up to 50 IDs.
+ An ID that does not exist in the inbox returns a 404.
+ """
+ read: NotRequired[bool]
+ """
+ True returns threads where every email is read. False returns threads with
+ at least one unread email.
+ """
+ limit: NotRequired[int]
+ """
+ Number of threads to retrieve. Maximum is 100, and minimum is 1.
+ """
+ after: NotRequired[str]
+ """
+ The thread ID after which we'll retrieve more threads (for pagination).
+ Cannot be used with the before parameter.
+ """
+ before: NotRequired[str]
+ """
+ The thread ID before which we'll retrieve more threads (for pagination).
+ Cannot be used with the after parameter.
+ """
+
+
+# Functional syntax required because ``from`` is a reserved keyword.
+_SearchParamsFrom = TypedDict(
+ "_SearchParamsFrom",
+ {
+ "from": NotRequired[List[str]],
+ },
+)
+
+
+class _SearchParams(_ListParams, _SearchParamsFrom):
+ query: NotRequired[str]
+ """
+ Free text to search for in the subject, body, sender, recipients, and
+ attachment names. Every word must match. Wrap words in double quotes to match
+ an exact phrase. Start a word or quoted phrase with ``-`` to exclude it.
+ Max 256 characters and 32 words.
+ """
+ to: NotRequired[List[str]]
+ """
+ Recipients, matched the same way as ``from``. Up to 20 values.
+ """
+ cc: NotRequired[List[str]]
+ """
+ CC recipients, matched the same way as ``from``. Up to 20 values.
+ """
+ bcc: NotRequired[List[str]]
+ """
+ BCC recipients, matched the same way as ``from``. Up to 20 values.
+ """
+ has_attachment: NotRequired[bool]
+ """
+ True matches emails with an attachment. False matches emails without one.
+ """
+ start_date: NotRequired[str]
+ """
+ Matches emails sent on or after this date. Accepts a date like ``2026-09-01``
+ or an ISO 8601 timestamp, in UTC unless the timestamp has an offset.
+ """
+ end_date: NotRequired[str]
+ """
+ Matches emails sent on or before this date. Same formats as ``start_date``.
+ A date covers that whole day. A timestamp is used as given. An ``end_date``
+ before ``start_date`` returns a 422 validation_error.
+ """
+
+
+class InboxThreads:
+ """
+ InboxThreads manages the threads of an inbox. A thread is a conversation
+ made up of one or more emails.
+ """
+
+ class Emails(InboxThreadEmails):
+ pass
+
+ class Labels(InboxThreadLabels):
+ pass
+
+ class ListParams(_ListParams):
+ """
+ ListParams are the filter and pagination parameters for listing threads.
+
+ Attributes:
+ folders (NotRequired[List[InboxMessageFolder]]): Folders to list.
+ labels (NotRequired[List[str]]): Label IDs. Returns threads with any of these labels.
+ read (NotRequired[bool]): Filter by read state.
+ limit (NotRequired[int]): Number of threads to retrieve. Maximum is 100, minimum is 1.
+ after (NotRequired[str]): The thread ID after which to retrieve more threads.
+ before (NotRequired[str]): The thread ID before which to retrieve more threads.
+ """
+
+ class SearchParams(_SearchParams):
+ """
+ SearchParams are the search, filter and pagination parameters for searching
+ threads. Different parameters combine, so a thread must match all of them.
+ ``folders``, ``labels``, and ``read`` apply to the thread. ``query``,
+ ``from``, ``to``, ``cc``, ``bcc``, ``has_attachment``, ``start_date``, and
+ ``end_date`` must all match the same email in the thread.
+
+ Attributes:
+ query (NotRequired[str]): Free text to search for.
+ from (NotRequired[List[str]]): Senders. Each value matches part of the
+ sender's address or name, without case. Up to 20 values.
+ to (NotRequired[List[str]]): Recipients.
+ cc (NotRequired[List[str]]): CC recipients.
+ bcc (NotRequired[List[str]]): BCC recipients.
+ has_attachment (NotRequired[bool]): Filter by attachment.
+ start_date (NotRequired[str]): Matches emails sent on or after this date.
+ end_date (NotRequired[str]): Matches emails sent on or before this date.
+ folders (NotRequired[List[InboxMessageFolder]]): Folders to search.
+ labels (NotRequired[List[str]]): Label IDs.
+ read (NotRequired[bool]): Filter by read state.
+ limit (NotRequired[int]): Number of threads to retrieve.
+ after (NotRequired[str]): The thread ID after which to retrieve more threads.
+ before (NotRequired[str]): The thread ID before which to retrieve more threads.
+ """
+
+ class ListResponse(BaseResponse):
+ """
+ ListResponse is a page of the threads of an inbox.
+
+ Attributes:
+ object (str): The object type, always "list"
+ has_more (bool): Whether there are more results available
+ data (List[InboxThread]): The matching threads, most recently active first
+ """
+
+ object: str
+ """
+ The object type, always "list".
+ """
+ has_more: bool
+ """
+ Whether there are more results available for pagination.
+ """
+ data: List[InboxThread]
+ """
+ The matching threads, most recently active first.
+ """
+
+ class SearchResponse(BaseResponse):
+ """
+ SearchResponse is a page of the threads that match a search.
+
+ Attributes:
+ object (str): The object type, always "list"
+ has_more (bool): Whether there are more results available
+ data (List[InboxThreadSearchResult]): The matching threads, most recently active first
+ """
+
+ object: str
+ """
+ The object type, always "list".
+ """
+ has_more: bool
+ """
+ Whether there are more results available for pagination.
+ """
+ data: List[InboxThreadSearchResult]
+ """
+ The matching threads, most recently active first.
+ """
+
+ class UpdateParams(TypedDict):
+ """
+ UpdateParams are the parameters for updating a thread. At least one of
+ ``read`` or ``folder`` is required. To change the labels of a thread,
+ use ``Inboxes.Threads.Labels``.
+
+ Attributes:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ id (str): The Thread ID.
+ read (NotRequired[bool]): Marks every email in the thread read or unread.
+ folder (NotRequired[MoveThreadFolder]): Moves the thread to this folder.
+ """
+
+ inbox_id: str
+ """
+ The Inbox ID or the inbox email address.
+ """
+ id: str
+ """
+ The Thread ID.
+ """
+ read: NotRequired[bool]
+ """
+ Marks every email in the thread read or unread.
+ """
+ folder: NotRequired[MoveThreadFolder]
+ """
+ Moves the thread to this folder. ``sent`` is assigned automatically and
+ cannot be set. Moving a thread out of ``trash`` restores it and clears its
+ deletion deadline.
+ """
+
+ class RemoveResponse(BaseResponse):
+ """
+ RemoveResponse is the response of removing a thread.
+
+ Attributes:
+ object (str): The object type, always "inbox_thread"
+ id (str): The ID of the thread
+ deleted (bool): True when this call moved the thread to trash
+ """
+
+ object: str
+ """
+ The object type, always "inbox_thread".
+ """
+ id: str
+ """
+ The ID of the thread.
+ """
+ deleted: bool
+ """
+ True when this call moved the thread to ``trash``.
+ False when the thread was already there.
+ """
+
+ @classmethod
+ def list(cls, inbox_id: str, params: Optional[ListParams] = None) -> ListResponse:
+ """
+ Retrieve the threads in an inbox, most recently active first. The ``after``
+ and ``before`` cursors take a thread ID, and only one of them can be passed.
+ A cursor thread that is not in the selected folders returns a 404.
+ To find threads by text, sender, recipient, attachment, or date, use ``search``.
+ see more: https://resend.com/docs/api-reference/inboxes/list-threads
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ params (Optional[ListParams]): Optional filter and pagination parameters
+
+ Returns:
+ ListResponse: A page of the threads of the inbox
+ """
+ path = build_inbox_query_path(
+ f"{inbox_path(inbox_id)}/threads",
+ cast(Dict[Any, Any], params) if params else None,
+ )
+ resp = request.Request[InboxThreads.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def search(
+ cls, inbox_id: str, params: Optional[SearchParams] = None
+ ) -> SearchResponse:
+ """
+ Find threads by the text of their emails, who sent or received them,
+ whether they have attachments, and their dates. Results are ordered by
+ latest activity, newest first, and paginate like ``list``. Search can be
+ up to about a minute behind new emails. When any email filter (``query``,
+ ``from``, ``to``, ``cc``, ``bcc``, ``has_attachment``, ``start_date`` or
+ ``end_date``) is set, the matching email must itself be in one of
+ ``folders``. For ``sent``, that means an email the inbox sent. When more
+ than 10,000 threads match, search covers the 10,000 with the newest
+ matching email.
+ see more: https://resend.com/docs/api-reference/inboxes/search-threads
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ params (Optional[SearchParams]): Optional search, filter and pagination parameters
+
+ Returns:
+ SearchResponse: A page of the matching threads
+ """
+ path = build_inbox_query_path(
+ f"{inbox_path(inbox_id)}/threads/search",
+ cast(Dict[Any, Any], params) if params else None,
+ )
+ resp = request.Request[InboxThreads.SearchResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def get(cls, inbox_id: str, thread_id: str) -> InboxThreadSummary:
+ """
+ Retrieve the summary of a thread. The emails in the thread are listed
+ with ``Inboxes.Threads.Emails.list``.
+ see more: https://resend.com/docs/api-reference/inboxes/get-thread
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+
+ Returns:
+ InboxThreadSummary: The thread summary
+ """
+ path = f"{inbox_path(inbox_id)}/threads/{thread_id}"
+ resp = request.Request[InboxThreadSummary](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def update(cls, params: UpdateParams) -> InboxThreadSummary:
+ """
+ Mark a thread read or unread, or move it to a folder. At least one of
+ ``read`` or ``folder`` is required. To change the labels of a thread, use
+ ``Inboxes.Threads.Labels``. Moving a thread out of ``trash`` restores it
+ and clears its deletion deadline. The changes apply in this order:
+ ``read``, then ``folder``. They are not atomic.
+ see more: https://resend.com/docs/api-reference/inboxes/update-thread
+
+ Args:
+ params (UpdateParams): The inbox ID, the thread ID and the changes
+
+ Returns:
+ InboxThreadSummary: The updated thread summary
+ """
+ path = f"{inbox_path(params['inbox_id'])}/threads/{params['id']}"
+ resp = request.Request[InboxThreadSummary](
+ path=path, params=without_keys(params, "inbox_id", "id"), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def remove(cls, inbox_id: str, thread_id: str) -> RemoveResponse:
+ """
+ Move a thread to ``trash`` and schedule it for deletion in 30 days. This is
+ reversible. To restore the thread, update its ``folder`` to ``inbox``,
+ ``archive``, or ``spam``, which also clears the deletion deadline.
+ see more: https://resend.com/docs/api-reference/inboxes/delete-thread
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+
+ Returns:
+ RemoveResponse: The thread ID and whether this call moved it to trash
+ """
+ path = f"{inbox_path(inbox_id)}/threads/{thread_id}"
+ resp = request.Request[InboxThreads.RemoveResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def list_async(
+ cls, inbox_id: str, params: Optional[ListParams] = None
+ ) -> ListResponse:
+ """
+ Retrieve the threads in an inbox, most recently active first (async).
+ The ``after`` and ``before`` cursors take a thread ID, and only one of them
+ can be passed. A cursor thread that is not in the selected folders returns
+ a 404. To find threads by text, sender, recipient, attachment, or date,
+ use ``search_async``.
+ see more: https://resend.com/docs/api-reference/inboxes/list-threads
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ params (Optional[ListParams]): Optional filter and pagination parameters
+
+ Returns:
+ ListResponse: A page of the threads of the inbox
+ """
+ path = build_inbox_query_path(
+ f"{inbox_path(inbox_id)}/threads",
+ cast(Dict[Any, Any], params) if params else None,
+ )
+ resp = await AsyncRequest[InboxThreads.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def search_async(
+ cls, inbox_id: str, params: Optional[SearchParams] = None
+ ) -> SearchResponse:
+ """
+ Find threads by the text of their emails, who sent or received them,
+ whether they have attachments, and their dates (async). Results are
+ ordered by latest activity, newest first, and paginate like ``list``.
+ Search can be up to about a minute behind new emails. When any email
+ filter (``query``, ``from``, ``to``, ``cc``, ``bcc``, ``has_attachment``,
+ ``start_date`` or ``end_date``) is set, the matching email must itself be
+ in one of ``folders``. For ``sent``, that means an email the inbox sent.
+ When more than 10,000 threads match, search covers the 10,000 with the
+ newest matching email.
+ see more: https://resend.com/docs/api-reference/inboxes/search-threads
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ params (Optional[SearchParams]): Optional search, filter and pagination parameters
+
+ Returns:
+ SearchResponse: A page of the matching threads
+ """
+ path = build_inbox_query_path(
+ f"{inbox_path(inbox_id)}/threads/search",
+ cast(Dict[Any, Any], params) if params else None,
+ )
+ resp = await AsyncRequest[InboxThreads.SearchResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def get_async(cls, inbox_id: str, thread_id: str) -> InboxThreadSummary:
+ """
+ Retrieve the summary of a thread (async). The emails in the thread are
+ listed with ``Inboxes.Threads.Emails.list_async``.
+ see more: https://resend.com/docs/api-reference/inboxes/get-thread
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+
+ Returns:
+ InboxThreadSummary: The thread summary
+ """
+ path = f"{inbox_path(inbox_id)}/threads/{thread_id}"
+ resp = await AsyncRequest[InboxThreadSummary](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def update_async(cls, params: UpdateParams) -> InboxThreadSummary:
+ """
+ Mark a thread read or unread, or move it to a folder (async). At least
+ one of ``read`` or ``folder`` is required. To change the labels of a
+ thread, use ``Inboxes.Threads.Labels``. Moving a thread out of ``trash``
+ restores it and clears its deletion deadline. The changes apply in this
+ order: ``read``, then ``folder``. They are not atomic.
+ see more: https://resend.com/docs/api-reference/inboxes/update-thread
+
+ Args:
+ params (UpdateParams): The inbox ID, the thread ID and the changes
+
+ Returns:
+ InboxThreadSummary: The updated thread summary
+ """
+ path = f"{inbox_path(params['inbox_id'])}/threads/{params['id']}"
+ resp = await AsyncRequest[InboxThreadSummary](
+ path=path, params=without_keys(params, "inbox_id", "id"), verb="patch"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def remove_async(cls, inbox_id: str, thread_id: str) -> RemoveResponse:
+ """
+ Move a thread to ``trash`` and schedule it for deletion in 30 days (async).
+ This is reversible. To restore the thread, update its ``folder`` to
+ ``inbox``, ``archive``, or ``spam``, which also clears the deletion deadline.
+ see more: https://resend.com/docs/api-reference/inboxes/delete-thread
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+
+ Returns:
+ RemoveResponse: The thread ID and whether this call moved it to trash
+ """
+ path = f"{inbox_path(inbox_id)}/threads/{thread_id}"
+ resp = await AsyncRequest[InboxThreads.RemoveResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
diff --git a/resend/inboxes/threads/emails/__init__.py b/resend/inboxes/threads/emails/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resend/inboxes/threads/emails/_inbox_thread_email.py b/resend/inboxes/threads/emails/_inbox_thread_email.py
new file mode 100644
index 0000000..9e43c7f
--- /dev/null
+++ b/resend/inboxes/threads/emails/_inbox_thread_email.py
@@ -0,0 +1,96 @@
+from typing import List, Optional
+
+from typing_extensions import Literal, TypedDict
+
+from resend._base_response import BaseResponse
+
+
+class InboxThreadEmailAttachment(TypedDict):
+ """
+ InboxThreadEmailAttachment is an attachment of a thread email.
+ """
+
+ id: str
+ """
+ The ID of the attachment.
+ """
+ filename: Optional[str]
+ """
+ The filename of the attachment.
+ """
+ size: Optional[int]
+ """
+ The size of the attachment in bytes.
+ """
+
+
+# Functional syntax required because ``from`` is a reserved keyword.
+_InboxThreadEmailFrom = TypedDict(
+ "_InboxThreadEmailFrom",
+ {
+ "from": str,
+ },
+)
+
+
+class InboxThreadEmail(_InboxThreadEmailFrom, BaseResponse):
+ """
+ InboxThreadEmail is a message of an inbox thread.
+ The ``from`` key holds the sender email address. It includes the display
+ name when the sender has one, in the format "Name ".
+ """
+
+ id: str
+ """
+ The ID of the message.
+ """
+ direction: Literal["inbound", "outbound"]
+ """
+ Whether the inbox received the message (``inbound``) or sent it (``outbound``).
+ A message the inbox sent to itself is ``inbound``, so a reply to it goes to its sender.
+ """
+ to: List[str]
+ """
+ The recipients of the message. For a received message, these are the
+ addresses it was delivered to, not the full ``To`` header.
+ """
+ cc: List[str]
+ """
+ The CC recipients of the message.
+ """
+ bcc: List[str]
+ """
+ The BCC recipients of the message. Always empty for a received message.
+ """
+ reply_to: List[str]
+ """
+ The Reply-To addresses of the message.
+ """
+ subject: Optional[str]
+ """
+ The subject of the message.
+ """
+ message_id: Optional[str]
+ """
+ The Message-ID header of the message.
+ """
+ html: Optional[str]
+ """
+ The HTML body.
+ """
+ text: Optional[str]
+ """
+ The plain-text body.
+ """
+ attachments: List[InboxThreadEmailAttachment]
+ """
+ The attachments of the message.
+ """
+ read: bool
+ """
+ Whether the message has been read.
+ """
+ received_at: str
+ """
+ Timestamp when the inbox received the message, or when the inbox sent it.
+ """
diff --git a/resend/inboxes/threads/emails/_inbox_thread_emails.py b/resend/inboxes/threads/emails/_inbox_thread_emails.py
new file mode 100644
index 0000000..8c2354c
--- /dev/null
+++ b/resend/inboxes/threads/emails/_inbox_thread_emails.py
@@ -0,0 +1,540 @@
+from typing import Any, Dict, List, Optional, Union, cast
+
+from typing_extensions import NotRequired, TypedDict
+
+from resend import request
+from resend._base_response import BaseResponse
+from resend.emails._attachment import Attachment, RemoteAttachment
+from resend.inboxes._query import (build_inbox_query_path, inbox_path,
+ without_keys)
+
+from ._inbox_thread_email import InboxThreadEmail
+
+# Async imports (optional - only available with pip install resend[async])
+try:
+ from resend.async_request import AsyncRequest
+except ImportError:
+ pass
+
+
+class InboxThreadEmails:
+ """
+ InboxThreadEmails manages the messages of an inbox thread.
+ """
+
+ class ListParams(TypedDict):
+ """
+ ListParams are the pagination parameters for listing the messages of a thread.
+ Pass the ``id`` of the last message as ``after`` to get the next, newer page.
+ Pass the ``id`` of the first message as ``before`` to get the previous, older page.
+
+ Attributes:
+ limit (NotRequired[int]): Number of messages to retrieve. Maximum is 100, minimum is 1.
+ after (NotRequired[str]): The message ID after which to retrieve more messages.
+ before (NotRequired[str]): The message ID before which to retrieve more messages.
+ """
+
+ limit: NotRequired[int]
+ """
+ Number of messages to retrieve. Maximum is 100, and minimum is 1.
+ """
+ after: NotRequired[str]
+ """
+ The message ID after which we'll retrieve more messages (for pagination).
+ Cannot be used with the before parameter.
+ """
+ before: NotRequired[str]
+ """
+ The message ID before which we'll retrieve more messages (for pagination).
+ Cannot be used with the after parameter.
+ """
+
+ class ListResponse(BaseResponse):
+ """
+ ListResponse is a page of the messages of a thread.
+
+ Attributes:
+ object (str): The object type, always "list"
+ has_more (bool): Whether there are more results available
+ data (List[InboxThreadEmail]): The messages on this page, oldest first
+ """
+
+ object: str
+ """
+ The object type, always "list".
+ """
+ has_more: bool
+ """
+ Whether there are more results available for pagination.
+ """
+ data: List[InboxThreadEmail]
+ """
+ The messages of the thread on this page, oldest first.
+ """
+
+ class ReplyParams(TypedDict):
+ """
+ ReplyParams are the parameters for replying to a thread email.
+ At least one of ``html`` or ``text`` must contain more than whitespace.
+
+ Attributes:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+ email_id (str): The Email ID of the message to reply to.
+ html (NotRequired[str]): The HTML body of the reply.
+ text (NotRequired[str]): The plain-text body of the reply.
+ cc (NotRequired[Union[str, List[str]]]): CC recipients.
+ bcc (NotRequired[Union[str, List[str]]]): BCC recipients.
+ subject (NotRequired[str]): The subject of the reply.
+ reply_all (NotRequired[bool]): Reply to everyone on the original message.
+ headers (NotRequired[Dict[str, str]]): Custom headers to be added to the email.
+ attachments (NotRequired[List[Union[Attachment, RemoteAttachment]]]): Files to attach.
+ """
+
+ inbox_id: str
+ """
+ The Inbox ID or the inbox email address.
+ """
+ thread_id: str
+ """
+ The Thread ID.
+ """
+ email_id: str
+ """
+ The Email ID of the message to reply to, as returned in ``data[].id``
+ by the list thread emails endpoint.
+ """
+ html: NotRequired[str]
+ """
+ The HTML body of the reply.
+ """
+ text: NotRequired[str]
+ """
+ The plain-text body of the reply.
+ """
+ cc: NotRequired[Union[str, List[str]]]
+ """
+ CC recipient email address. For multiple addresses, send a list of strings.
+ Without ``reply_all``, the reply copies only these addresses. With
+ ``reply_all``, these addresses are merged with the copied recipients,
+ without duplicates. ``cc`` and ``bcc`` combined must be fewer than 50
+ addresses. After the ``to`` recipients are added, the reply cannot exceed
+ 50 recipients.
+ """
+ bcc: NotRequired[Union[str, List[str]]]
+ """
+ BCC recipient email address. For multiple addresses, send a list of strings.
+ BCC recipients of the original message are never copied.
+ """
+ subject: NotRequired[str]
+ """
+ The subject of the reply. Cannot contain a line break. When omitted, the
+ thread subject is used, prefixed with ``Re:`` if it does not already start with it.
+ """
+ reply_all: NotRequired[bool]
+ """
+ Reply to everyone on the original message. For a message the inbox
+ received, the reply goes to its ``Reply-To`` addresses, or to its sender
+ when it has none, and copies the other addresses of its ``To`` header and
+ its ``cc`` recipients in ``cc``. For a message the inbox sent, the reply
+ goes to that message's ``Reply-To`` addresses when they are not the inbox
+ address, otherwise to its ``to`` recipients, and copies its ``cc``
+ recipients. The inbox address is always excluded, and duplicates are
+ removed. Defaults to False.
+ """
+ headers: NotRequired[Dict[str, str]]
+ """
+ Custom headers to be added to the email. The ``In-Reply-To``,
+ ``References`` and ``Reply-To`` headers are set by Resend and cannot be
+ set here. Sending one of them, in any letter case, returns a 422. Header
+ names cannot be empty, and names and values cannot contain line breaks
+ or null characters, or the request returns a 422.
+ """
+ attachments: NotRequired[List[Union[Attachment, RemoteAttachment]]]
+ """
+ Files to attach, as on ``Emails.send``. Each one needs ``content`` or
+ ``path``. The ``content`` of all files can total at most 25 MB. A
+ ``path`` file does not count toward the 25 MB, because it is fetched when
+ the email is sent.
+ An empty file, a blocked file type, or a ``path`` that is not an ``http``
+ or ``https`` URL or points to ``localhost`` returns a 422.
+ """
+
+ class ReplyOptions(TypedDict):
+ """
+ ReplyOptions is the class that wraps the options for the reply method.
+
+ Attributes:
+ idempotency_key (NotRequired[str]): Unique key that ensures the same operation is not processed multiple times.
+ Allows for safe retries without duplicating operations.
+ If provided, will be sent as the `Idempotency-Key` header.
+ """
+
+ idempotency_key: NotRequired[str]
+ """
+ Unique key that ensures the same operation is not processed multiple times.
+ Allows for safe retries without duplicating operations.
+ If provided, will be sent as the `Idempotency-Key` header.
+ Between 1 and 256 characters, so the reply is sent only once. A retry with the
+ same key within 24 hours replays the first successful (2xx) response,
+ with the ``idempotent-replayed: true`` header. A failed request is not
+ stored, so you can retry it with the same key. Reusing the key with a
+ different body, or while the first request is still running, returns a 409.
+ """
+
+ class ReplyResponse(BaseResponse):
+ """
+ ReplyResponse is the response of replying to a thread email.
+
+ Attributes:
+ id (str): The ID of the sent email
+ """
+
+ id: str
+ """
+ The ID of the sent email. Use it with the emails endpoints.
+ """
+
+ class ForwardParams(TypedDict):
+ """
+ ForwardParams are the parameters for forwarding a thread email.
+
+ Attributes:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+ email_id (str): The Email ID of the message to forward.
+ to (Union[str, List[str]]): The recipients.
+ cc (NotRequired[Union[str, List[str]]]): CC recipients.
+ bcc (NotRequired[Union[str, List[str]]]): BCC recipients.
+ html (NotRequired[str]): An HTML note sent above the quoted original.
+ text (NotRequired[str]): A plain-text note sent above the quoted original.
+ subject (NotRequired[str]): The subject of the forward.
+ headers (NotRequired[Dict[str, str]]): Custom headers to be added to the email.
+ attachments (NotRequired[List[Union[Attachment, RemoteAttachment]]]): Files to attach.
+ """
+
+ inbox_id: str
+ """
+ The Inbox ID or the inbox email address.
+ """
+ thread_id: str
+ """
+ The Thread ID.
+ """
+ email_id: str
+ """
+ The Email ID of the message to forward, as returned in ``data[].id``
+ by the list thread emails endpoint.
+ """
+ to: Union[str, List[str]]
+ """
+ Recipient email address. For multiple addresses, send a list of strings.
+ Must name at least one address. ``to``, ``cc`` and ``bcc`` combined cannot
+ exceed 50 recipients.
+ """
+ cc: NotRequired[Union[str, List[str]]]
+ """
+ CC recipient email address. For multiple addresses, send a list of strings.
+ Counts toward the 50-recipient limit.
+ """
+ bcc: NotRequired[Union[str, List[str]]]
+ """
+ BCC recipient email address. For multiple addresses, send a list of strings.
+ Counts toward the 50-recipient limit.
+ """
+ html: NotRequired[str]
+ """
+ An HTML note sent above the quoted original.
+ """
+ text: NotRequired[str]
+ """
+ A plain-text note sent above the quoted original.
+ """
+ subject: NotRequired[str]
+ """
+ The subject of the forward. Cannot contain a line break. When omitted, the
+ thread subject is used, prefixed with ``Fwd:`` if it does not already start with it.
+ """
+ headers: NotRequired[Dict[str, str]]
+ """
+ Custom headers to be added to the email. The ``In-Reply-To``,
+ ``References`` and ``Reply-To`` headers are set by Resend and cannot be
+ set here. Sending one of them, in any letter case, returns a 422. Header
+ names cannot be empty, and names and values cannot contain line breaks
+ or null characters, or the request returns a 422.
+ """
+ attachments: NotRequired[List[Union[Attachment, RemoteAttachment]]]
+ """
+ Files to attach, as on ``Emails.send``. Each one needs ``content`` or
+ ``path``. They share one limit with the attachments of the original
+ message, which the forward also sends. Together they can have at most
+ 25 MB of ``content``. A ``path`` file does not count toward the 25 MB,
+ because it is fetched when the email is sent.
+ An empty file, a blocked file type, or a ``path`` that is not an ``http``
+ or ``https`` URL or points to ``localhost`` returns a 422.
+ """
+
+ class ForwardOptions(TypedDict):
+ """
+ ForwardOptions is the class that wraps the options for the forward method.
+
+ Attributes:
+ idempotency_key (NotRequired[str]): Unique key that ensures the same operation is not processed multiple times.
+ Allows for safe retries without duplicating operations.
+ If provided, will be sent as the `Idempotency-Key` header.
+ """
+
+ idempotency_key: NotRequired[str]
+ """
+ Unique key that ensures the same operation is not processed multiple times.
+ Allows for safe retries without duplicating operations.
+ If provided, will be sent as the `Idempotency-Key` header.
+ Between 1 and 256 characters, so the forward is sent only once. A retry with the
+ same key within 24 hours replays the first successful (2xx) response,
+ with the ``idempotent-replayed: true`` header. A failed request is not
+ stored, so you can retry it with the same key. Reusing the key with a
+ different body, or while the first request is still running, returns a 409.
+ """
+
+ class ForwardResponse(BaseResponse):
+ """
+ ForwardResponse is the response of forwarding a thread email.
+
+ Attributes:
+ id (str): The ID of the sent email
+ """
+
+ id: str
+ """
+ The ID of the sent email. Use it with the emails endpoints.
+ """
+
+ @classmethod
+ def list(
+ cls, inbox_id: str, thread_id: str, params: Optional[ListParams] = None
+ ) -> ListResponse:
+ """
+ Retrieve the messages of a thread, oldest first. You cannot combine
+ ``after`` and ``before``. A cursor that is not a message of the thread
+ returns a 404.
+ see more: https://resend.com/docs/api-reference/inboxes/list-thread-emails
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+ params (Optional[ListParams]): Optional pagination parameters
+
+ Returns:
+ ListResponse: A page of the messages of the thread
+ """
+ path = build_inbox_query_path(
+ f"{inbox_path(inbox_id)}/threads/{thread_id}/emails",
+ cast(Dict[Any, Any], params) if params else None,
+ )
+ resp = request.Request[InboxThreadEmails.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def get(cls, inbox_id: str, thread_id: str, email_id: str) -> InboxThreadEmail:
+ """
+ Retrieve a single thread email.
+ see more: https://resend.com/docs/api-reference/inboxes/get-thread-email
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+ email_id (str): The Email ID, as returned in ``data[].id`` by the list
+ thread emails endpoint.
+
+ Returns:
+ InboxThreadEmail: The thread email
+ """
+ path = f"{inbox_path(inbox_id)}/threads/{thread_id}/emails/{email_id}"
+ resp = request.Request[InboxThreadEmail](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def reply(
+ cls, params: ReplyParams, options: Optional[ReplyOptions] = None
+ ) -> ReplyResponse:
+ """
+ Send a reply from the inbox address right away, with no stored draft.
+ The ``to`` recipients come from the message you reply to. For a message
+ the inbox received, the reply goes to its ``Reply-To`` addresses, or to
+ its sender when it has no ``Reply-To``. For a message the inbox sent, the
+ reply goes to that message's ``to`` recipients. Set ``reply_all`` to also
+ copy the other recipients of the message. The reply threads under the
+ message through the ``In-Reply-To`` and ``References`` headers.
+ see more: https://resend.com/docs/api-reference/inboxes/reply-thread-email
+
+ Args:
+ params (ReplyParams): The path IDs and the content of the reply
+ options (Optional[ReplyOptions]): The reply options, ie: idempotency_key
+
+ Returns:
+ ReplyResponse: The ID of the sent email
+ """
+ path = (
+ f"{inbox_path(params['inbox_id'])}/threads/{params['thread_id']}"
+ f"/emails/{params['email_id']}/reply"
+ )
+ resp = request.Request[InboxThreadEmails.ReplyResponse](
+ path=path,
+ params=without_keys(params, "inbox_id", "thread_id", "email_id"),
+ verb="post",
+ options=cast(Dict[Any, Any], options),
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def forward(
+ cls, params: ForwardParams, options: Optional[ForwardOptions] = None
+ ) -> ForwardResponse:
+ """
+ Forward a message from the inbox address right away, with no stored draft.
+ The original message is quoted under a forwarded-message banner, below the
+ optional ``html`` or ``text`` note. The attachments of the original message
+ are sent with the forward, together with any ``attachments`` you add.
+ The request fails when they exceed 25 MB of ``content`` in total.
+ An original attachment that was sent from a URL (``path``) cannot be
+ forwarded, and the request returns a 422.
+ see more: https://resend.com/docs/api-reference/inboxes/forward-thread-email
+
+ Args:
+ params (ForwardParams): The path IDs, the recipients and the note
+ options (Optional[ForwardOptions]): The forward options, ie: idempotency_key
+
+ Returns:
+ ForwardResponse: The ID of the sent email
+ """
+ path = (
+ f"{inbox_path(params['inbox_id'])}/threads/{params['thread_id']}"
+ f"/emails/{params['email_id']}/forward"
+ )
+ resp = request.Request[InboxThreadEmails.ForwardResponse](
+ path=path,
+ params=without_keys(params, "inbox_id", "thread_id", "email_id"),
+ verb="post",
+ options=cast(Dict[Any, Any], options),
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def list_async(
+ cls, inbox_id: str, thread_id: str, params: Optional[ListParams] = None
+ ) -> ListResponse:
+ """
+ Retrieve the messages of a thread, oldest first (async). You cannot
+ combine ``after`` and ``before``. A cursor that is not a message of the
+ thread returns a 404.
+ see more: https://resend.com/docs/api-reference/inboxes/list-thread-emails
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+ params (Optional[ListParams]): Optional pagination parameters
+
+ Returns:
+ ListResponse: A page of the messages of the thread
+ """
+ path = build_inbox_query_path(
+ f"{inbox_path(inbox_id)}/threads/{thread_id}/emails",
+ cast(Dict[Any, Any], params) if params else None,
+ )
+ resp = await AsyncRequest[InboxThreadEmails.ListResponse](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def get_async(
+ cls, inbox_id: str, thread_id: str, email_id: str
+ ) -> InboxThreadEmail:
+ """
+ Retrieve a single thread email (async).
+ see more: https://resend.com/docs/api-reference/inboxes/get-thread-email
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+ email_id (str): The Email ID, as returned in ``data[].id`` by the list
+ thread emails endpoint.
+
+ Returns:
+ InboxThreadEmail: The thread email
+ """
+ path = f"{inbox_path(inbox_id)}/threads/{thread_id}/emails/{email_id}"
+ resp = await AsyncRequest[InboxThreadEmail](
+ path=path, params={}, verb="get"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def reply_async(
+ cls, params: ReplyParams, options: Optional[ReplyOptions] = None
+ ) -> ReplyResponse:
+ """
+ Send a reply from the inbox address right away, with no stored draft (async).
+ The ``to`` recipients come from the message you reply to. For a message
+ the inbox received, the reply goes to its ``Reply-To`` addresses, or to
+ its sender when it has no ``Reply-To``. For a message the inbox sent, the
+ reply goes to that message's ``to`` recipients. Set ``reply_all`` to also
+ copy the other recipients of the message. The reply threads under the
+ message through the ``In-Reply-To`` and ``References`` headers.
+ see more: https://resend.com/docs/api-reference/inboxes/reply-thread-email
+
+ Args:
+ params (ReplyParams): The path IDs and the content of the reply
+ options (Optional[ReplyOptions]): The reply options, ie: idempotency_key
+
+ Returns:
+ ReplyResponse: The ID of the sent email
+ """
+ path = (
+ f"{inbox_path(params['inbox_id'])}/threads/{params['thread_id']}"
+ f"/emails/{params['email_id']}/reply"
+ )
+ resp = await AsyncRequest[InboxThreadEmails.ReplyResponse](
+ path=path,
+ params=without_keys(params, "inbox_id", "thread_id", "email_id"),
+ verb="post",
+ options=cast(Dict[Any, Any], options),
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def forward_async(
+ cls, params: ForwardParams, options: Optional[ForwardOptions] = None
+ ) -> ForwardResponse:
+ """
+ Forward a message from the inbox address right away, with no stored
+ draft (async). The original message is quoted under a forwarded-message
+ banner, below the optional ``html`` or ``text`` note. The attachments of
+ the original message are sent with the forward, together with any
+ ``attachments`` you add. The request fails when they exceed 25 MB of
+ ``content`` in total. An original attachment that was sent from a URL
+ (``path``) cannot be forwarded, and the request returns a 422.
+ see more: https://resend.com/docs/api-reference/inboxes/forward-thread-email
+
+ Args:
+ params (ForwardParams): The path IDs, the recipients and the note
+ options (Optional[ForwardOptions]): The forward options, ie: idempotency_key
+
+ Returns:
+ ForwardResponse: The ID of the sent email
+ """
+ path = (
+ f"{inbox_path(params['inbox_id'])}/threads/{params['thread_id']}"
+ f"/emails/{params['email_id']}/forward"
+ )
+ resp = await AsyncRequest[InboxThreadEmails.ForwardResponse](
+ path=path,
+ params=without_keys(params, "inbox_id", "thread_id", "email_id"),
+ verb="post",
+ options=cast(Dict[Any, Any], options),
+ ).perform_with_content()
+ return resp
diff --git a/resend/inboxes/threads/labels/__init__.py b/resend/inboxes/threads/labels/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/resend/inboxes/threads/labels/_inbox_thread_labels.py b/resend/inboxes/threads/labels/_inbox_thread_labels.py
new file mode 100644
index 0000000..f542979
--- /dev/null
+++ b/resend/inboxes/threads/labels/_inbox_thread_labels.py
@@ -0,0 +1,226 @@
+from typing import List
+
+from typing_extensions import TypedDict
+
+from resend import request
+from resend._base_response import BaseResponse
+from resend.inboxes._query import inbox_path, without_keys
+from resend.inboxes.threads._inbox_thread import InboxThreadLabel
+
+# Async imports (optional - only available with pip install resend[async])
+try:
+ from resend.async_request import AsyncRequest
+except ImportError:
+ pass
+
+
+class InboxThreadLabels:
+ """
+ InboxThreadLabels manages the labels on an inbox thread. To create, rename
+ or delete the labels themselves, use ``Inboxes.Labels``.
+ """
+
+ class ListResponse(BaseResponse):
+ """
+ ListResponse lists every label on the thread after the change.
+
+ Attributes:
+ object (str): The object type, always "list"
+ data (List[InboxThreadLabel]): The labels on the thread
+ """
+
+ object: str
+ """
+ The object type, always "list".
+ """
+ data: List[InboxThreadLabel]
+ """
+ The labels on the thread after the change.
+ """
+
+ class AddParams(TypedDict):
+ """
+ AddParams are the parameters for adding labels to a thread.
+
+ Attributes:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+ labels (List[str]): The IDs of the labels to apply to the thread.
+ """
+
+ inbox_id: str
+ """
+ The Inbox ID or the inbox email address.
+ """
+ thread_id: str
+ """
+ The Thread ID.
+ """
+ labels: List[str]
+ """
+ The IDs of the labels to apply to the thread. Between 1 and 10 IDs,
+ each a label of the inbox.
+ """
+
+ class SetParams(TypedDict):
+ """
+ SetParams are the parameters for replacing the labels of a thread.
+
+ Attributes:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+ labels (List[str]): The IDs of the labels the thread carries after the change.
+ """
+
+ inbox_id: str
+ """
+ The Inbox ID or the inbox email address.
+ """
+ thread_id: str
+ """
+ The Thread ID.
+ """
+ labels: List[str]
+ """
+ The IDs of the labels the thread carries after the change. Up to 100 IDs,
+ each a label of the inbox. Pass an empty list to take every label off
+ the thread.
+ """
+
+ @classmethod
+ def add(cls, params: AddParams) -> ListResponse:
+ """
+ Apply one or more labels to a thread. Labels already on the thread stay
+ on it. A label ID that is not a label of the inbox returns a 404, and
+ nothing changes. To replace the labels of the thread, use ``set``.
+ see more: https://resend.com/docs/api-reference/inboxes/add-thread-labels
+
+ Args:
+ params (AddParams): The inbox ID, the thread ID and the label IDs
+
+ Returns:
+ ListResponse: Every label on the thread after the change
+ """
+ path = f"{inbox_path(params['inbox_id'])}/threads/{params['thread_id']}/labels"
+ resp = request.Request[InboxThreadLabels.ListResponse](
+ path=path,
+ params=without_keys(params, "inbox_id", "thread_id"),
+ verb="post",
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def set(cls, params: SetParams) -> ListResponse:
+ """
+ Replace every label on a thread with the given labels, in one atomic
+ change. The labels themselves are kept. An empty list takes every label
+ off the thread. A label ID that is not a label of the inbox returns a
+ 404, and nothing changes.
+ see more: https://resend.com/docs/api-reference/inboxes/set-thread-labels
+
+ Args:
+ params (SetParams): The inbox ID, the thread ID and the label IDs
+
+ Returns:
+ ListResponse: Every label on the thread after the change
+ """
+ path = f"{inbox_path(params['inbox_id'])}/threads/{params['thread_id']}/labels"
+ resp = request.Request[InboxThreadLabels.ListResponse](
+ path=path,
+ params=without_keys(params, "inbox_id", "thread_id"),
+ verb="put",
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ def remove(cls, inbox_id: str, thread_id: str, label_id: str) -> ListResponse:
+ """
+ Take one label off a thread. The label itself is kept, and stays on every
+ other thread that carries it. A label that is not on the thread returns
+ the current labels. To delete the label, use ``Inboxes.Labels.remove``.
+ see more: https://resend.com/docs/api-reference/inboxes/remove-thread-label
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+ label_id (str): The Label ID.
+
+ Returns:
+ ListResponse: Every label on the thread after the change
+ """
+ path = f"{inbox_path(inbox_id)}/threads/{thread_id}/labels/{label_id}"
+ resp = request.Request[InboxThreadLabels.ListResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def add_async(cls, params: AddParams) -> ListResponse:
+ """
+ Apply one or more labels to a thread (async). Labels already on the
+ thread stay on it. A label ID that is not a label of the inbox returns a
+ 404, and nothing changes. To replace the labels of the thread, use
+ ``set_async``.
+ see more: https://resend.com/docs/api-reference/inboxes/add-thread-labels
+
+ Args:
+ params (AddParams): The inbox ID, the thread ID and the label IDs
+
+ Returns:
+ ListResponse: Every label on the thread after the change
+ """
+ path = f"{inbox_path(params['inbox_id'])}/threads/{params['thread_id']}/labels"
+ resp = await AsyncRequest[InboxThreadLabels.ListResponse](
+ path=path,
+ params=without_keys(params, "inbox_id", "thread_id"),
+ verb="post",
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def set_async(cls, params: SetParams) -> ListResponse:
+ """
+ Replace every label on a thread with the given labels, in one atomic
+ change (async). The labels themselves are kept. An empty list takes every
+ label off the thread. A label ID that is not a label of the inbox returns
+ a 404, and nothing changes.
+ see more: https://resend.com/docs/api-reference/inboxes/set-thread-labels
+
+ Args:
+ params (SetParams): The inbox ID, the thread ID and the label IDs
+
+ Returns:
+ ListResponse: Every label on the thread after the change
+ """
+ path = f"{inbox_path(params['inbox_id'])}/threads/{params['thread_id']}/labels"
+ resp = await AsyncRequest[InboxThreadLabels.ListResponse](
+ path=path,
+ params=without_keys(params, "inbox_id", "thread_id"),
+ verb="put",
+ ).perform_with_content()
+ return resp
+
+ @classmethod
+ async def remove_async(
+ cls, inbox_id: str, thread_id: str, label_id: str
+ ) -> ListResponse:
+ """
+ Take one label off a thread (async). The label itself is kept, and stays
+ on every other thread that carries it. A label that is not on the thread
+ returns the current labels. To delete the label, use
+ ``Inboxes.Labels.remove_async``.
+ see more: https://resend.com/docs/api-reference/inboxes/remove-thread-label
+
+ Args:
+ inbox_id (str): The Inbox ID or the inbox email address.
+ thread_id (str): The Thread ID.
+ label_id (str): The Label ID.
+
+ Returns:
+ ListResponse: Every label on the thread after the change
+ """
+ path = f"{inbox_path(inbox_id)}/threads/{thread_id}/labels/{label_id}"
+ resp = await AsyncRequest[InboxThreadLabels.ListResponse](
+ path=path, params={}, verb="delete"
+ ).perform_with_content()
+ return resp
diff --git a/resend/version.py b/resend/version.py
index 4ccbcac..79982a3 100644
--- a/resend/version.py
+++ b/resend/version.py
@@ -1,4 +1,4 @@
-__version__ = "2.49.1"
+__version__ = "2.50.0"
def get_version() -> str:
diff --git a/resend/webhooks/_webhook.py b/resend/webhooks/_webhook.py
index 4f916f9..49c3375 100644
--- a/resend/webhooks/_webhook.py
+++ b/resend/webhooks/_webhook.py
@@ -33,6 +33,21 @@
"topic.created",
"topic.updated",
"topic.deleted",
+ # Inbox events
+ "inbox.created",
+ "inbox.updated",
+ "inbox.deleted",
+ "inbox.thread.created",
+ "inbox.email.received",
+ "inbox.email.sent",
+ "inbox.thread.folder.updated",
+ "inbox.thread.assigned",
+ "inbox.thread.unassigned",
+ "inbox.thread.labels.updated",
+ "inbox.draft.created",
+ "inbox.draft.updated",
+ "inbox.draft.sent",
+ "inbox.draft.deleted",
]
diff --git a/resend/webhooks/_webhook_event.py b/resend/webhooks/_webhook_event.py
index 5407e81..fe2b776 100644
--- a/resend/webhooks/_webhook_event.py
+++ b/resend/webhooks/_webhook_event.py
@@ -2,6 +2,10 @@
from typing_extensions import Literal, NotRequired, TypedDict
+from resend.inboxes._inbox import Inbox, MoveThreadFolder
+from resend.inboxes.drafts._inbox_draft import InboxDraftAttachment
+from resend.inboxes.threads._inbox_thread import (InboxThreadLabel,
+ InboxThreadSummary)
from resend.suppressions._suppression import SuppressionListItem
# Functional syntax required because ``from`` is a reserved keyword.
@@ -429,6 +433,414 @@ class TopicEventData(TypedDict):
"""
+InboxEventSource = Literal["api", "dashboard", "agent", "system"]
+"""
+What made the change on an inbox webhook event.
+"""
+
+
+class InboxEventEmailAttachment(TypedDict):
+ """
+ An attachment on an email in an inbox webhook event.
+ """
+
+ id: str
+ """
+ The ID of the attachment.
+ """
+ filename: Optional[str]
+ """
+ The filename of the attachment.
+ """
+ size: Optional[int]
+ """
+ The size of the attachment in bytes.
+ """
+
+
+class InboxEventEmail(_FromField):
+ """
+ The email carried by an inbox webhook event. It has no ``html`` or ``text``.
+ The ``from`` key holds the sender, as an address or as ``Name ``.
+ """
+
+ id: str
+ """
+ The ID of the email.
+ """
+ direction: Literal["inbound", "outbound"]
+ """
+ Whether the email was received by the inbox or sent from it.
+ """
+ to: List[str]
+ """
+ The recipients of the email.
+ """
+ cc: List[str]
+ """
+ The CC recipients of the email.
+ """
+ bcc: List[str]
+ """
+ The BCC recipients of the email.
+ """
+ reply_to: List[str]
+ """
+ The Reply-To addresses.
+ """
+ subject: Optional[str]
+ """
+ The subject of the email.
+ """
+ message_id: Optional[str]
+ """
+ The Message-ID header of the email.
+ """
+ attachments: List[InboxEventEmailAttachment]
+ """
+ The attachments on the email.
+ """
+ read: bool
+ """
+ Whether the email has been read.
+ """
+ received_at: str
+ """
+ Timestamp when the email arrived or was sent.
+ """
+
+
+class InboxEventDraft(TypedDict):
+ """
+ The draft carried by an inbox webhook event. It has no ``html`` or ``text``.
+ """
+
+ object: str
+ """
+ The object type. Always ``inbox_draft``.
+ """
+ id: str
+ """
+ The ID of the draft.
+ """
+ type: Literal["standalone", "reply"]
+ """
+ ``standalone`` for a new conversation, or ``reply``.
+ """
+ to: Optional[List[str]]
+ """
+ The recipients.
+ """
+ cc: List[str]
+ """
+ The CC recipients.
+ """
+ bcc: List[str]
+ """
+ The BCC recipients.
+ """
+ subject: Optional[str]
+ """
+ The subject.
+ """
+ thread_id: Optional[str]
+ """
+ The ID of the thread when this draft is a reply. None otherwise.
+ """
+ reply_to_email_id: Optional[str]
+ """
+ The ID of the email this draft replies to. None otherwise.
+ """
+ email_id: Optional[str]
+ """
+ The ID of the queued outbound email after send. None until then.
+ """
+ created_at: str
+ """
+ Timestamp when the draft was created.
+ """
+ updated_at: str
+ """
+ Timestamp when the draft was last saved.
+ """
+ attachments: List[InboxDraftAttachment]
+ """
+ The files stored on the draft.
+ """
+
+
+class InboxEventData(TypedDict):
+ """
+ ``data`` payload for ``inbox.created`` and ``inbox.updated`` events.
+ """
+
+ source: InboxEventSource
+ """
+ What made the change.
+ """
+ inbox_id: str
+ """
+ The ID of the inbox.
+ """
+ inbox: NotRequired[Inbox]
+ """
+ The inbox as it is when the webhook is sent. Absent when the inbox no longer exists.
+ """
+
+
+class InboxDeletedEventData(TypedDict):
+ """
+ ``data`` payload for ``inbox.deleted`` events.
+ """
+
+ source: InboxEventSource
+ """
+ What made the change.
+ """
+ inbox_id: str
+ """
+ The ID of the inbox.
+ """
+
+
+class InboxThreadEmailEventData(TypedDict):
+ """
+ ``data`` payload for ``inbox.thread.created``, ``inbox.email.received`` and
+ ``inbox.email.sent`` events.
+ """
+
+ source: InboxEventSource
+ """
+ What made the change.
+ """
+ inbox_id: str
+ """
+ The ID of the inbox.
+ """
+ thread_id: str
+ """
+ The ID of the thread.
+ """
+ email_id: str
+ """
+ The ID of the email.
+ """
+ thread: NotRequired[InboxThreadSummary]
+ """
+ The thread as it is when the webhook is sent. Absent when the thread no longer exists.
+ """
+ email: NotRequired[InboxEventEmail]
+ """
+ The email as it is when the webhook is sent, without ``html`` or ``text``.
+ Fetch the body with ``Inboxes.Threads.Emails.get``. Absent when the email no
+ longer exists.
+ """
+
+
+# Functional syntax required because ``from`` is a reserved keyword.
+_InboxThreadFolderMove = TypedDict(
+ "_InboxThreadFolderMove",
+ {
+ "from": MoveThreadFolder,
+ "to": MoveThreadFolder,
+ },
+)
+
+
+class InboxThreadFolderUpdatedEventData(_InboxThreadFolderMove):
+ """
+ ``data`` payload for ``inbox.thread.folder.updated`` events. The ``from`` key
+ holds the folder the thread was in, and ``to`` the folder it moved to.
+ """
+
+ source: InboxEventSource
+ """
+ What made the change.
+ """
+ inbox_id: str
+ """
+ The ID of the inbox.
+ """
+ thread_id: str
+ """
+ The ID of the thread.
+ """
+ thread: NotRequired[InboxThreadSummary]
+ """
+ The thread as it is when the webhook is sent. Absent when the thread no longer exists.
+ """
+
+
+class InboxThreadAssignedEventData(TypedDict):
+ """
+ ``data`` payload for ``inbox.thread.assigned`` events.
+ """
+
+ assignee_email: Optional[str]
+ """
+ The email of the member the thread is now assigned to.
+ """
+ assigned_by_email: Optional[str]
+ """
+ The email of the member who made the assignment. None when no member made it.
+ """
+ previous_assignee_email: Optional[str]
+ """
+ The email of the member the thread was assigned to before. None when it was unassigned.
+ """
+ source: InboxEventSource
+ """
+ What made the change.
+ """
+ inbox_id: str
+ """
+ The ID of the inbox.
+ """
+ thread_id: str
+ """
+ The ID of the thread.
+ """
+ thread: NotRequired[InboxThreadSummary]
+ """
+ The thread as it is when the webhook is sent. Absent when the thread no longer exists.
+ """
+
+
+class InboxThreadUnassignedEventData(TypedDict):
+ """
+ ``data`` payload for ``inbox.thread.unassigned`` events.
+ """
+
+ previous_assignee_email: Optional[str]
+ """
+ The email of the member the thread was assigned to.
+ """
+ source: InboxEventSource
+ """
+ What made the change.
+ """
+ inbox_id: str
+ """
+ The ID of the inbox.
+ """
+ thread_id: str
+ """
+ The ID of the thread.
+ """
+ thread: NotRequired[InboxThreadSummary]
+ """
+ The thread as it is when the webhook is sent. Absent when the thread no longer exists.
+ """
+
+
+class InboxThreadLabelsUpdatedEventData(TypedDict):
+ """
+ ``data`` payload for ``inbox.thread.labels.updated`` events.
+ One event carries one label, in either ``added`` or ``removed``.
+ """
+
+ added: List[InboxThreadLabel]
+ """
+ The labels applied to the thread.
+ """
+ removed: List[InboxThreadLabel]
+ """
+ The labels removed from the thread.
+ """
+ source: InboxEventSource
+ """
+ What made the change.
+ """
+ inbox_id: str
+ """
+ The ID of the inbox.
+ """
+ thread_id: str
+ """
+ The ID of the thread.
+ """
+ thread: NotRequired[InboxThreadSummary]
+ """
+ The thread as it is when the webhook is sent. Absent when the thread no longer exists.
+ """
+
+
+class InboxDraftEventData(TypedDict):
+ """
+ ``data`` payload for ``inbox.draft.created``, ``inbox.draft.updated`` and
+ ``inbox.draft.deleted`` events.
+ """
+
+ source: InboxEventSource
+ """
+ What made the change.
+ """
+ inbox_id: str
+ """
+ The ID of the inbox.
+ """
+ thread_id: NotRequired[str]
+ """
+ The ID of the thread. Only on reply drafts.
+ """
+ draft_id: str
+ """
+ The ID of the draft.
+ """
+ thread: NotRequired[InboxThreadSummary]
+ """
+ The thread as it is when the webhook is sent. Absent when the thread no
+ longer exists. Only on reply drafts.
+ """
+ draft: NotRequired[InboxEventDraft]
+ """
+ The draft as it is when the webhook is sent, without ``html`` or ``text``.
+ Fetch the body with ``Inboxes.Drafts.get``. Absent when the draft no longer exists.
+ """
+
+
+class InboxDraftSentEventData(TypedDict):
+ """
+ ``data`` payload for ``inbox.draft.sent`` events.
+ """
+
+ source: InboxEventSource
+ """
+ What made the change.
+ """
+ inbox_id: str
+ """
+ The ID of the inbox.
+ """
+ thread_id: str
+ """
+ The ID of the thread.
+ """
+ email_id: str
+ """
+ The ID of the email.
+ """
+ draft_id: str
+ """
+ The ID of the draft.
+ """
+ thread: NotRequired[InboxThreadSummary]
+ """
+ The thread as it is when the webhook is sent. Absent when the thread no longer exists.
+ """
+ email: NotRequired[InboxEventEmail]
+ """
+ The email as it is when the webhook is sent, without ``html`` or ``text``.
+ Fetch the body with ``Inboxes.Threads.Emails.get``. Only when the email is
+ already on the thread when the webhook is sent.
+ """
+ draft: NotRequired[InboxEventDraft]
+ """
+ The draft as it is when the webhook is sent, without ``html`` or ``text``.
+ Fetch the body with ``Inboxes.Drafts.get``. Absent when the draft no longer exists.
+ """
+
+
class EmailSentEvent(TypedDict):
"""Webhook payload for ``email.sent``."""
@@ -613,6 +1025,118 @@ class TopicDeletedEvent(TypedDict):
data: TopicEventData
+class InboxCreatedEvent(TypedDict):
+ """Webhook payload for ``inbox.created``."""
+
+ type: Literal["inbox.created"]
+ created_at: str
+ data: InboxEventData
+
+
+class InboxUpdatedEvent(TypedDict):
+ """Webhook payload for ``inbox.updated``."""
+
+ type: Literal["inbox.updated"]
+ created_at: str
+ data: InboxEventData
+
+
+class InboxDeletedEvent(TypedDict):
+ """Webhook payload for ``inbox.deleted``."""
+
+ type: Literal["inbox.deleted"]
+ created_at: str
+ data: InboxDeletedEventData
+
+
+class InboxThreadCreatedEvent(TypedDict):
+ """Webhook payload for ``inbox.thread.created``."""
+
+ type: Literal["inbox.thread.created"]
+ created_at: str
+ data: InboxThreadEmailEventData
+
+
+class InboxEmailReceivedEvent(TypedDict):
+ """Webhook payload for ``inbox.email.received``."""
+
+ type: Literal["inbox.email.received"]
+ created_at: str
+ data: InboxThreadEmailEventData
+
+
+class InboxEmailSentEvent(TypedDict):
+ """Webhook payload for ``inbox.email.sent``."""
+
+ type: Literal["inbox.email.sent"]
+ created_at: str
+ data: InboxThreadEmailEventData
+
+
+class InboxThreadFolderUpdatedEvent(TypedDict):
+ """Webhook payload for ``inbox.thread.folder.updated``."""
+
+ type: Literal["inbox.thread.folder.updated"]
+ created_at: str
+ data: InboxThreadFolderUpdatedEventData
+
+
+class InboxThreadAssignedEvent(TypedDict):
+ """Webhook payload for ``inbox.thread.assigned``."""
+
+ type: Literal["inbox.thread.assigned"]
+ created_at: str
+ data: InboxThreadAssignedEventData
+
+
+class InboxThreadUnassignedEvent(TypedDict):
+ """Webhook payload for ``inbox.thread.unassigned``."""
+
+ type: Literal["inbox.thread.unassigned"]
+ created_at: str
+ data: InboxThreadUnassignedEventData
+
+
+class InboxThreadLabelsUpdatedEvent(TypedDict):
+ """Webhook payload for ``inbox.thread.labels.updated``."""
+
+ type: Literal["inbox.thread.labels.updated"]
+ created_at: str
+ data: InboxThreadLabelsUpdatedEventData
+
+
+class InboxDraftCreatedEvent(TypedDict):
+ """Webhook payload for ``inbox.draft.created``."""
+
+ type: Literal["inbox.draft.created"]
+ created_at: str
+ data: InboxDraftEventData
+
+
+class InboxDraftUpdatedEvent(TypedDict):
+ """Webhook payload for ``inbox.draft.updated``."""
+
+ type: Literal["inbox.draft.updated"]
+ created_at: str
+ data: InboxDraftEventData
+
+
+class InboxDraftSentEvent(TypedDict):
+ """Webhook payload for ``inbox.draft.sent``."""
+
+ type: Literal["inbox.draft.sent"]
+ created_at: str
+ data: InboxDraftSentEventData
+
+
+class InboxDraftDeletedEvent(TypedDict):
+ """Webhook payload for ``inbox.draft.deleted``."""
+
+ type: Literal["inbox.draft.deleted"]
+ created_at: str
+ data: InboxDraftEventData
+
+
WebhookEventPayload = Union[
EmailSentEvent,
EmailScheduledEvent,
@@ -637,6 +1161,20 @@ class TopicDeletedEvent(TypedDict):
TopicCreatedEvent,
TopicUpdatedEvent,
TopicDeletedEvent,
+ InboxCreatedEvent,
+ InboxUpdatedEvent,
+ InboxDeletedEvent,
+ InboxThreadCreatedEvent,
+ InboxEmailReceivedEvent,
+ InboxEmailSentEvent,
+ InboxThreadFolderUpdatedEvent,
+ InboxThreadAssignedEvent,
+ InboxThreadUnassignedEvent,
+ InboxThreadLabelsUpdatedEvent,
+ InboxDraftCreatedEvent,
+ InboxDraftUpdatedEvent,
+ InboxDraftSentEvent,
+ InboxDraftDeletedEvent,
]
"""
Union of all Resend webhook event payload shapes returned by ``Webhooks.verify``.
diff --git a/tests/inboxes_async_test.py b/tests/inboxes_async_test.py
new file mode 100644
index 0000000..31f0ee8
--- /dev/null
+++ b/tests/inboxes_async_test.py
@@ -0,0 +1,627 @@
+import pytest
+
+import resend
+from resend.exceptions import NoContentError
+from tests.conftest import AsyncResendBaseTest
+from tests.inboxes_test import (DRAFT, DRAFT_ID, EMAIL_ID, INBOX, INBOX_ID,
+ LABEL_ID, SENT_EMAIL_ID, THREAD, THREAD_EMAIL,
+ THREAD_ID, THREAD_LABEL, THREAD_SUMMARY)
+
+# flake8: noqa
+
+pytestmark = pytest.mark.asyncio
+
+
+class TestResendInboxesAsync(AsyncResendBaseTest):
+ async def test_inboxes_create_async(self) -> None:
+ self.set_mock_json({"object": "inbox", "id": INBOX_ID})
+ inbox: resend.Inboxes.CreateResponse = await resend.Inboxes.create_async(
+ {"email_address": "support@acme.com"}
+ )
+ assert inbox["id"] == INBOX_ID
+
+ async def test_should_create_inboxes_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.create_async({"email_address": "support@acme.com"})
+
+ async def test_inboxes_list_async(self) -> None:
+ self.set_mock_json({"object": "list", "has_more": False, "data": []})
+ inboxes: resend.Inboxes.ListResponse = await resend.Inboxes.list_async()
+ assert inboxes["object"] == "list"
+ assert inboxes["data"] == []
+
+ async def test_should_list_inboxes_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.list_async()
+
+ async def test_inboxes_get_async(self) -> None:
+ self.set_mock_json(INBOX)
+ inbox: resend.Inbox = await resend.Inboxes.get_async("support@acme.com")
+ assert inbox["email_address"] == "support@acme.com"
+
+ async def test_should_get_inboxes_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.get_async(INBOX_ID)
+
+ async def test_inboxes_update_async(self) -> None:
+ self.set_mock_json({"object": "inbox", "id": INBOX_ID})
+ inbox: resend.Inboxes.UpdateResponse = await resend.Inboxes.update_async(
+ {"id": INBOX_ID, "name": "Support"}
+ )
+ assert inbox["id"] == INBOX_ID
+
+ async def test_should_update_inboxes_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.update_async({"id": INBOX_ID, "name": "Support"})
+
+ async def test_inboxes_remove_async(self) -> None:
+ self.set_mock_json({"object": "inbox", "id": INBOX_ID, "deleted": True})
+ removed: resend.Inboxes.RemoveResponse = await resend.Inboxes.remove_async(
+ INBOX_ID
+ )
+ assert removed["deleted"] is True
+
+ async def test_should_remove_inboxes_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.remove_async(INBOX_ID)
+
+
+class TestResendInboxAgentAsync(AsyncResendBaseTest):
+ async def test_inbox_agent_get_async(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "inbox_agent",
+ "instructions": "Escalate legal threats.",
+ "tone": None,
+ "actions": ["draft_reply"],
+ }
+ )
+ agent: resend.InboxAgentSettings = await resend.Inboxes.Agent.get_async(
+ INBOX_ID
+ )
+ assert agent["actions"] == ["draft_reply"]
+
+ async def test_should_get_inbox_agent_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Agent.get_async(INBOX_ID)
+
+ async def test_inbox_agent_update_async(self) -> None:
+ self.set_mock_json(
+ {"object": "inbox_agent", "id": "a1f84a4e-6f2b-4f0a-9c1d-8a2e5b3c7d90"}
+ )
+ agent: resend.Inboxes.Agent.UpdateResponse = (
+ await resend.Inboxes.Agent.update_async(
+ {"inbox_id": INBOX_ID, "actions": []}
+ )
+ )
+ assert agent["object"] == "inbox_agent"
+
+ async def test_should_update_inbox_agent_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Agent.update_async(
+ {"inbox_id": INBOX_ID, "tone": None}
+ )
+
+
+class TestResendInboxLabelsAsync(AsyncResendBaseTest):
+ async def test_inbox_labels_list_async(self) -> None:
+ self.set_mock_json({"object": "list", "data": []})
+ labels: resend.Inboxes.Labels.ListResponse = (
+ await resend.Inboxes.Labels.list_async(INBOX_ID)
+ )
+ assert labels["data"] == []
+
+ async def test_should_list_inbox_labels_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Labels.list_async(INBOX_ID)
+
+ async def test_inbox_labels_create_async(self) -> None:
+ self.set_mock_json({"object": "inbox_label", "id": LABEL_ID})
+ label: resend.Inboxes.Labels.CreateResponse = (
+ await resend.Inboxes.Labels.create_async(
+ {"inbox_id": INBOX_ID, "name": "Urgent"}
+ )
+ )
+ assert label["id"] == LABEL_ID
+
+ async def test_should_create_inbox_labels_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Labels.create_async(
+ {"inbox_id": INBOX_ID, "name": "Urgent"}
+ )
+
+ async def test_inbox_labels_update_async(self) -> None:
+ self.set_mock_json({"object": "inbox_label", "id": LABEL_ID})
+ label: resend.Inboxes.Labels.UpdateResponse = (
+ await resend.Inboxes.Labels.update_async(
+ {"inbox_id": INBOX_ID, "id": LABEL_ID, "color": "#F76B15"}
+ )
+ )
+ assert label["id"] == LABEL_ID
+
+ async def test_should_update_inbox_labels_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Labels.update_async(
+ {"inbox_id": INBOX_ID, "id": LABEL_ID, "name": "Billing"}
+ )
+
+ async def test_inbox_labels_remove_async(self) -> None:
+ self.set_mock_json({"object": "inbox_label", "id": LABEL_ID, "deleted": True})
+ removed: resend.Inboxes.Labels.RemoveResponse = (
+ await resend.Inboxes.Labels.remove_async(INBOX_ID, LABEL_ID)
+ )
+ assert removed["deleted"] is True
+
+ async def test_should_remove_inbox_labels_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Labels.remove_async(INBOX_ID, LABEL_ID)
+
+
+class TestResendInboxThreadsAsync(AsyncResendBaseTest):
+ async def test_inbox_threads_list_async(self) -> None:
+ self.set_mock_json({"object": "list", "has_more": False, "data": [THREAD]})
+ threads: resend.Inboxes.Threads.ListResponse = (
+ await resend.Inboxes.Threads.list_async(INBOX_ID, {"labels": [LABEL_ID]})
+ )
+ assert threads["data"][0]["id"] == THREAD_ID
+
+ async def test_should_list_inbox_threads_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.list_async(INBOX_ID)
+
+ async def test_inbox_threads_search_async(self) -> None:
+ result = dict(THREAD)
+ result["matched_email_id"] = None
+ self.set_mock_json({"object": "list", "has_more": False, "data": [result]})
+ threads: resend.Inboxes.Threads.SearchResponse = (
+ await resend.Inboxes.Threads.search_async(INBOX_ID, {"query": "refund"})
+ )
+ assert threads["data"][0]["matched_email_id"] is None
+
+ async def test_should_search_inbox_threads_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.search_async(INBOX_ID)
+
+ async def test_inbox_threads_get_async(self) -> None:
+ self.set_mock_json(THREAD_SUMMARY)
+ thread: resend.InboxThreadSummary = await resend.Inboxes.Threads.get_async(
+ INBOX_ID, THREAD_ID
+ )
+ assert thread["object"] == "inbox_thread"
+
+ async def test_should_get_inbox_threads_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.get_async(INBOX_ID, THREAD_ID)
+
+ async def test_inbox_threads_update_async(self) -> None:
+ self.set_mock_json(THREAD_SUMMARY)
+ thread: resend.InboxThreadSummary = await resend.Inboxes.Threads.update_async(
+ {"inbox_id": INBOX_ID, "id": THREAD_ID, "folder": "spam"}
+ )
+ assert thread["id"] == THREAD_ID
+
+ async def test_should_update_inbox_threads_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.update_async(
+ {"inbox_id": INBOX_ID, "id": THREAD_ID, "read": True}
+ )
+
+ async def test_inbox_threads_remove_async(self) -> None:
+ self.set_mock_json(
+ {"object": "inbox_thread", "id": THREAD_ID, "deleted": False}
+ )
+ removed: resend.Inboxes.Threads.RemoveResponse = (
+ await resend.Inboxes.Threads.remove_async(INBOX_ID, THREAD_ID)
+ )
+ assert removed["deleted"] is False
+
+ async def test_should_remove_inbox_threads_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.remove_async(INBOX_ID, THREAD_ID)
+
+
+class TestResendInboxThreadLabelsAsync(AsyncResendBaseTest):
+ async def test_inbox_thread_labels_add_async(self) -> None:
+ self.set_mock_json({"object": "list", "data": [THREAD_LABEL]})
+ labels: resend.Inboxes.Threads.Labels.ListResponse = (
+ await resend.Inboxes.Threads.Labels.add_async(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "labels": [LABEL_ID]}
+ )
+ )
+ assert labels["data"] == [THREAD_LABEL]
+
+ async def test_should_add_inbox_thread_labels_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.Labels.add_async(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "labels": [LABEL_ID]}
+ )
+
+ async def test_inbox_thread_labels_set_async(self) -> None:
+ self.set_mock_json({"object": "list", "data": [THREAD_LABEL]})
+ labels: resend.Inboxes.Threads.Labels.ListResponse = (
+ await resend.Inboxes.Threads.Labels.set_async(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "labels": [LABEL_ID]}
+ )
+ )
+ assert labels["object"] == "list"
+
+ async def test_should_set_inbox_thread_labels_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.Labels.set_async(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "labels": []}
+ )
+
+ async def test_inbox_thread_labels_remove_async(self) -> None:
+ self.set_mock_json({"object": "list", "data": []})
+ labels: resend.Inboxes.Threads.Labels.ListResponse = (
+ await resend.Inboxes.Threads.Labels.remove_async(
+ INBOX_ID, THREAD_ID, LABEL_ID
+ )
+ )
+ assert labels["data"] == []
+
+ async def test_should_remove_inbox_thread_labels_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.Labels.remove_async(
+ INBOX_ID, THREAD_ID, LABEL_ID
+ )
+
+
+class TestResendInboxThreadEmailsAsync(AsyncResendBaseTest):
+ async def test_inbox_thread_emails_list_async(self) -> None:
+ self.set_mock_json({"object": "list", "has_more": False, "data": [THREAD_EMAIL]})
+ emails: resend.Inboxes.Threads.Emails.ListResponse = (
+ await resend.Inboxes.Threads.Emails.list_async(INBOX_ID, THREAD_ID)
+ )
+ assert emails["data"][0]["id"] == EMAIL_ID
+
+ async def test_should_list_inbox_thread_emails_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.Emails.list_async(INBOX_ID, THREAD_ID)
+
+ async def test_inbox_thread_emails_get_async(self) -> None:
+ self.set_mock_json(THREAD_EMAIL)
+ email: resend.InboxThreadEmail = await resend.Inboxes.Threads.Emails.get_async(
+ INBOX_ID, THREAD_ID, EMAIL_ID
+ )
+ assert email["id"] == EMAIL_ID
+
+ async def test_should_get_inbox_thread_emails_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.Emails.get_async(
+ INBOX_ID, THREAD_ID, EMAIL_ID
+ )
+
+ async def test_inbox_thread_emails_reply_async(self) -> None:
+ self.set_mock_json({"id": SENT_EMAIL_ID})
+ reply: resend.Inboxes.Threads.Emails.ReplyResponse = (
+ await resend.Inboxes.Threads.Emails.reply_async(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "text": "Refund issued for order 1041.",
+ }
+ )
+ )
+ assert reply["id"] == SENT_EMAIL_ID
+ assert self.mock.call_count == 1
+
+ async def test_inbox_thread_emails_reply_async_with_options(self) -> None:
+ self.set_mock_json({"id": SENT_EMAIL_ID})
+ options: resend.Inboxes.Threads.Emails.ReplyOptions = {"idempotency_key": "k"}
+ reply = await resend.Inboxes.Threads.Emails.reply_async(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "email_id": EMAIL_ID, "text": "Hi"},
+ options,
+ )
+ assert reply["id"] == SENT_EMAIL_ID
+ forward = await resend.Inboxes.Threads.Emails.forward_async(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "email_id": EMAIL_ID, "to": "a@example.org"},
+ {"idempotency_key": "k"},
+ )
+ assert forward["id"] == SENT_EMAIL_ID
+ draft = await resend.Inboxes.Drafts.create_async(
+ {"inbox_id": INBOX_ID, "text": "Hi"}, {"idempotency_key": "k"}
+ )
+ assert draft["id"] == SENT_EMAIL_ID
+ sent = await resend.Inboxes.Drafts.send_async(
+ INBOX_ID,
+ DRAFT_ID,
+ {"headers": {"X-Ticket-Id": "4821"}},
+ {"idempotency_key": "k"},
+ )
+ assert sent["id"] == SENT_EMAIL_ID
+
+ async def test_should_reply_inbox_thread_emails_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.Emails.reply_async(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "html": "Hi
",
+ }
+ )
+
+ async def test_inbox_thread_emails_forward_async(self) -> None:
+ self.set_mock_json({"id": SENT_EMAIL_ID})
+ forward: resend.Inboxes.Threads.Emails.ForwardResponse = (
+ await resend.Inboxes.Threads.Emails.forward_async(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "to": ["colleague@example.org"],
+ }
+ )
+ )
+ assert forward["id"] == SENT_EMAIL_ID
+
+ async def test_should_forward_inbox_thread_emails_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Threads.Emails.forward_async(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "to": "colleague@example.org",
+ }
+ )
+
+
+class TestResendInboxDraftsAsync(AsyncResendBaseTest):
+ async def test_inbox_drafts_list_async(self) -> None:
+ self.set_mock_json({"object": "list", "has_more": False, "data": []})
+ drafts: resend.Inboxes.Drafts.ListResponse = (
+ await resend.Inboxes.Drafts.list_async(INBOX_ID)
+ )
+ assert drafts["data"] == []
+
+ async def test_should_list_inbox_drafts_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Drafts.list_async(INBOX_ID)
+
+ async def test_inbox_drafts_create_async(self) -> None:
+ self.set_mock_json({"object": "inbox_draft", "id": DRAFT_ID})
+ draft: resend.Inboxes.Drafts.CreateResponse = (
+ await resend.Inboxes.Drafts.create_async(
+ {"inbox_id": INBOX_ID, "to": "ada@example.org", "text": "Hi"}
+ )
+ )
+ assert draft["id"] == DRAFT_ID
+
+ async def test_should_create_inbox_drafts_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Drafts.create_async(
+ {"inbox_id": INBOX_ID, "text": "Hi"}
+ )
+
+ async def test_inbox_drafts_get_async(self) -> None:
+ self.set_mock_json(DRAFT)
+ draft: resend.InboxDraft = await resend.Inboxes.Drafts.get_async(
+ INBOX_ID, DRAFT_ID
+ )
+ assert draft["id"] == DRAFT_ID
+
+ async def test_should_get_inbox_drafts_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Drafts.get_async(INBOX_ID, DRAFT_ID)
+
+ async def test_inbox_drafts_update_async(self) -> None:
+ self.set_mock_json({"object": "inbox_draft", "id": DRAFT_ID})
+ draft: resend.Inboxes.Drafts.UpdateResponse = (
+ await resend.Inboxes.Drafts.update_async(
+ {"inbox_id": INBOX_ID, "id": DRAFT_ID, "subject": "Refund for order 1041"}
+ )
+ )
+ assert draft == {"object": "inbox_draft", "id": DRAFT_ID}
+
+ async def test_should_update_inbox_drafts_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Drafts.update_async(
+ {"inbox_id": INBOX_ID, "id": DRAFT_ID, "text": None}
+ )
+
+ async def test_inbox_drafts_remove_async(self) -> None:
+ self.set_mock_json({"object": "inbox_draft", "id": DRAFT_ID, "deleted": True})
+ removed: resend.Inboxes.Drafts.RemoveResponse = (
+ await resend.Inboxes.Drafts.remove_async(INBOX_ID, DRAFT_ID)
+ )
+ assert removed["deleted"] is True
+
+ async def test_should_remove_inbox_drafts_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Drafts.remove_async(INBOX_ID, DRAFT_ID)
+
+ async def test_inbox_drafts_send_async(self) -> None:
+ self.set_mock_json({"id": SENT_EMAIL_ID})
+ sent: resend.Inboxes.Drafts.SendResponse = (
+ await resend.Inboxes.Drafts.send_async(INBOX_ID, DRAFT_ID)
+ )
+ assert sent["id"] == SENT_EMAIL_ID
+
+ async def test_should_send_inbox_drafts_async_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with pytest.raises(NoContentError):
+ _ = await resend.Inboxes.Drafts.send_async(INBOX_ID, DRAFT_ID)
+
+
+class TestInboxesAsyncPaths(AsyncResendBaseTest):
+ def last_url(self) -> str:
+ return str(self.mock.call_args.kwargs["url"])
+
+ async def test_every_async_method_calls_its_path(self) -> None:
+ self.set_mock_json({"object": "inbox", "id": INBOX_ID})
+ api = "https://api.resend.com"
+ inbox = f"{api}/inboxes/{INBOX_ID}"
+ thread = f"{inbox}/threads/{THREAD_ID}"
+ email = f"{thread}/emails/{EMAIL_ID}"
+ draft = f"{inbox}/drafts/{DRAFT_ID}"
+
+ await resend.Inboxes.create_async({"email_address": "support@acme.com"})
+ assert self.last_url() == f"{api}/inboxes"
+ await resend.Inboxes.list_async({"limit": 1})
+ assert self.last_url() == f"{api}/inboxes?limit=1"
+ await resend.Inboxes.get_async("support@acme.com")
+ assert self.last_url() == f"{api}/inboxes/support%40acme.com"
+ await resend.Inboxes.update_async({"id": INBOX_ID, "name": "Support"})
+ assert self.last_url() == inbox
+ await resend.Inboxes.remove_async(INBOX_ID)
+ assert self.last_url() == inbox
+
+ await resend.Inboxes.Agent.get_async(INBOX_ID)
+ assert self.last_url() == f"{inbox}/agent"
+ await resend.Inboxes.Agent.update_async({"inbox_id": INBOX_ID, "tone": None})
+ assert self.last_url() == f"{inbox}/agent"
+
+ await resend.Inboxes.Labels.list_async(INBOX_ID)
+ assert self.last_url() == f"{inbox}/labels"
+ await resend.Inboxes.Labels.create_async({"inbox_id": INBOX_ID, "name": "Urgent"})
+ assert self.last_url() == f"{inbox}/labels"
+ await resend.Inboxes.Labels.update_async(
+ {"inbox_id": INBOX_ID, "id": LABEL_ID, "name": "Billing"}
+ )
+ assert self.last_url() == f"{inbox}/labels/{LABEL_ID}"
+ await resend.Inboxes.Labels.remove_async(INBOX_ID, LABEL_ID)
+ assert self.last_url() == f"{inbox}/labels/{LABEL_ID}"
+
+ await resend.Inboxes.Threads.list_async(
+ INBOX_ID, {"folders": ["inbox", "sent"], "read": True}
+ )
+ assert self.last_url() == f"{inbox}/threads?folders=inbox%2Csent&read=true"
+ await resend.Inboxes.Threads.search_async(
+ INBOX_ID, {"to": ["a@example.com"], "has_attachment": False}
+ )
+ assert (
+ self.last_url()
+ == f"{inbox}/threads/search?to=a%40example.com&has_attachment=false"
+ )
+ await resend.Inboxes.Threads.get_async(INBOX_ID, THREAD_ID)
+ assert self.last_url() == thread
+ await resend.Inboxes.Threads.update_async(
+ {"inbox_id": INBOX_ID, "id": THREAD_ID, "read": True}
+ )
+ assert self.last_url() == thread
+ await resend.Inboxes.Threads.remove_async(INBOX_ID, THREAD_ID)
+ assert self.last_url() == thread
+
+ await resend.Inboxes.Threads.Emails.list_async(
+ INBOX_ID, THREAD_ID, {"after": EMAIL_ID}
+ )
+ assert self.last_url() == f"{thread}/emails?after={EMAIL_ID}"
+ await resend.Inboxes.Threads.Emails.get_async(INBOX_ID, THREAD_ID, EMAIL_ID)
+ assert self.last_url() == email
+ await resend.Inboxes.Threads.Emails.reply_async(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "email_id": EMAIL_ID, "text": "Hi"}
+ )
+ assert self.last_url() == f"{email}/reply"
+ await resend.Inboxes.Threads.Emails.forward_async(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "to": "colleague@example.org",
+ }
+ )
+ assert self.last_url() == f"{email}/forward"
+
+ await resend.Inboxes.Drafts.list_async(INBOX_ID, {"limit": 1})
+ assert self.last_url() == f"{inbox}/drafts?limit=1"
+ await resend.Inboxes.Drafts.create_async({"inbox_id": INBOX_ID, "text": "Hi"})
+ assert self.last_url() == f"{inbox}/drafts"
+ await resend.Inboxes.Drafts.get_async(INBOX_ID, DRAFT_ID)
+ assert self.last_url() == draft
+ await resend.Inboxes.Drafts.update_async(
+ {"inbox_id": INBOX_ID, "id": DRAFT_ID, "text": "Hello"}
+ )
+ assert self.last_url() == draft
+ await resend.Inboxes.Drafts.remove_async(INBOX_ID, DRAFT_ID)
+ assert self.last_url() == draft
+ await resend.Inboxes.Drafts.send_async(INBOX_ID, DRAFT_ID)
+ assert self.last_url() == f"{draft}/send"
diff --git a/tests/inboxes_test.py b/tests/inboxes_test.py
new file mode 100644
index 0000000..f045e81
--- /dev/null
+++ b/tests/inboxes_test.py
@@ -0,0 +1,1144 @@
+from typing import Any, Callable, Dict, List, Tuple
+from unittest import TestCase
+from unittest.mock import create_autospec
+
+import resend
+from resend import HTTPClient
+from resend.exceptions import NoContentError
+from tests.conftest import ResendBaseTest
+
+# flake8: noqa
+
+INBOX_ID = "b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1"
+LABEL_ID = "7f9c1d2e-4a6b-4c3d-8e15-9b0a7c6d5e34"
+THREAD_ID = "4d8e2a1c-9b3f-4c6d-8a21-3e5f7c9c0d12"
+EMAIL_ID = "5b1a9f47-2c8d-4e6f-9a03-1d2e3f4a5b60"
+DRAFT_ID = "c3a1e8b4-2d5f-4a7c-9e10-6b8d7f5a4c32"
+SENT_EMAIL_ID = "6a0c8e58-3d9e-4f7a-8b14-2e3f4a5b6c71"
+
+INBOX: Dict[str, Any] = {
+ "object": "inbox",
+ "id": INBOX_ID,
+ "name": "Customer Support",
+ "email_address": "support@acme.com",
+ "domain_id": "d91cd9bd-1176-453e-8fc1-35364d380206",
+ "receiving_address": None,
+ "from_name": "Ada from Support",
+ "unread": 3,
+ "drafts": 2,
+ "last_received": "2026-08-05T14:03:11.229Z",
+ "created_at": "2026-08-01T09:12:44.512Z",
+ "updated_at": "2026-08-02T10:00:00.000Z",
+}
+
+THREAD_LABEL: Dict[str, Any] = {"id": LABEL_ID, "name": "Urgent", "color": "#E93D82"}
+
+THREAD: Dict[str, Any] = {
+ "id": THREAD_ID,
+ "subject": "Refund for order 1041",
+ "from": "ada@example.org",
+ "to": ["support@acme.com"],
+ "cc": [],
+ "bcc": [],
+ "labels": [THREAD_LABEL],
+ "email_count": 2,
+ "has_attachment": True,
+ "has_draft": False,
+ "read": False,
+ "received_at": "2026-08-05T14:03:11.229Z",
+ "folder": "inbox",
+}
+
+THREAD_SUMMARY: Dict[str, Any] = {
+ "object": "inbox_thread",
+ "id": THREAD_ID,
+ "subject": "Refund for order 1041",
+ "folder": "inbox",
+ "labels": [THREAD_LABEL],
+ "read": False,
+}
+
+THREAD_EMAIL: Dict[str, Any] = {
+ "id": EMAIL_ID,
+ "direction": "inbound",
+ "from": "Ada Lovelace ",
+ "to": ["support@acme.com"],
+ "cc": [],
+ "bcc": [],
+ "reply_to": ["replies@example.org"],
+ "subject": "Refund for order 1041",
+ "message_id": "<1041@example.org>",
+ "html": "Could I get a refund for order 1041?
",
+ "text": "Could I get a refund for order 1041?",
+ "attachments": [
+ {
+ "id": "9d4f2b81-6c3a-4e7d-8b12-0a5c6d7e8f90",
+ "filename": "receipt.pdf",
+ "size": 20841,
+ }
+ ],
+ "read": False,
+ "received_at": "2026-08-05T14:03:11.229Z",
+}
+
+DRAFT_ATTACHMENT: Dict[str, Any] = {
+ "id": "5c1e9d2a-7b3f-4e8a-9c6d-2f0a1b3c4d5e",
+ "filename": "receipt.pdf",
+ "content_type": "application/pdf",
+ "size": 20841,
+}
+
+DRAFT: Dict[str, Any] = {
+ "object": "inbox_draft",
+ "id": DRAFT_ID,
+ "type": "standalone",
+ "to": ["ada@example.org"],
+ "cc": [],
+ "bcc": [],
+ "subject": "Refund for order 1041",
+ "html": "Refund issued for order 1041.
",
+ "text": "Refund issued for order 1041.",
+ "thread_id": None,
+ "reply_to_email_id": None,
+ "email_id": None,
+ "created_at": "2026-08-05T14:12:04.110Z",
+ "updated_at": "2026-08-05T14:12:04.110Z",
+ "attachments": [DRAFT_ATTACHMENT],
+}
+
+
+class TestResendInboxes(ResendBaseTest):
+ def test_inboxes_create(self) -> None:
+ self.set_mock_json({"object": "inbox", "id": INBOX_ID})
+ params: resend.Inboxes.CreateParams = {
+ "email_address": "support@acme.com",
+ "name": "Customer Support",
+ "from_name": "Ada from Support",
+ "forwarding": False,
+ }
+ inbox: resend.Inboxes.CreateResponse = resend.Inboxes.create(params)
+ assert inbox["object"] == "inbox"
+ assert inbox["id"] == INBOX_ID
+
+ def test_should_create_inboxes_raise_exception_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.create({"email_address": "support@acme.com"})
+
+ def test_inboxes_list(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": False,
+ "data": [
+ {
+ "id": INBOX_ID,
+ "name": "Customer Support",
+ "email_address": "support@acme.com",
+ "from_name": None,
+ "unread": 3,
+ "last_received": None,
+ "created_at": "2026-08-01T09:12:44.512Z",
+ "updated_at": "2026-08-02T10:00:00.000Z",
+ }
+ ],
+ }
+ )
+ inboxes: resend.Inboxes.ListResponse = resend.Inboxes.list({"limit": 10})
+ assert inboxes["object"] == "list"
+ assert inboxes["has_more"] is False
+ assert inboxes["data"][0]["id"] == INBOX_ID
+ assert inboxes["data"][0]["email_address"] == "support@acme.com"
+ assert inboxes["data"][0]["from_name"] is None
+ assert inboxes["data"][0]["updated_at"] == "2026-08-02T10:00:00.000Z"
+
+ def test_should_list_inboxes_raise_exception_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.list()
+
+ def test_inboxes_get(self) -> None:
+ self.set_mock_json(INBOX)
+ inbox: resend.Inbox = resend.Inboxes.get(INBOX_ID)
+ assert inbox["object"] == "inbox"
+ assert inbox["id"] == INBOX_ID
+ assert inbox["receiving_address"] is None
+ assert inbox["unread"] == 3
+ assert inbox["drafts"] == 2
+ assert inbox["updated_at"] == "2026-08-02T10:00:00.000Z"
+
+ def test_should_get_inboxes_raise_exception_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.get(INBOX_ID)
+
+ def test_inboxes_update(self) -> None:
+ self.set_mock_json({"object": "inbox", "id": INBOX_ID})
+ params: resend.Inboxes.UpdateParams = {"id": INBOX_ID, "name": "Support"}
+ inbox: resend.Inboxes.UpdateResponse = resend.Inboxes.update(params)
+ assert inbox["object"] == "inbox"
+ assert inbox["id"] == INBOX_ID
+
+ def test_should_update_inboxes_raise_exception_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.update({"id": INBOX_ID, "name": "Support"})
+
+ def test_inboxes_remove(self) -> None:
+ self.set_mock_json({"object": "inbox", "id": INBOX_ID, "deleted": True})
+ removed: resend.Inboxes.RemoveResponse = resend.Inboxes.remove(INBOX_ID)
+ assert removed["id"] == INBOX_ID
+ assert removed["deleted"] is True
+
+ def test_should_remove_inboxes_raise_exception_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.remove(INBOX_ID)
+
+
+class TestResendInboxAgent(ResendBaseTest):
+ def test_inbox_agent_get(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "inbox_agent",
+ "instructions": None,
+ "tone": None,
+ "actions": [],
+ }
+ )
+ agent: resend.InboxAgentSettings = resend.Inboxes.Agent.get(INBOX_ID)
+ assert agent["object"] == "inbox_agent"
+ assert agent["instructions"] is None
+ assert agent["actions"] == []
+
+ def test_should_get_inbox_agent_raise_exception_when_no_content(self) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Agent.get(INBOX_ID)
+
+ def test_inbox_agent_update(self) -> None:
+ self.set_mock_json(
+ {"object": "inbox_agent", "id": "a1f84a4e-6f2b-4f0a-9c1d-8a2e5b3c7d90"}
+ )
+ params: resend.Inboxes.Agent.UpdateParams = {
+ "inbox_id": INBOX_ID,
+ "tone": "concise_casual",
+ "actions": ["draft_reply", "archive", "move_to_trash"],
+ }
+ agent: resend.Inboxes.Agent.UpdateResponse = resend.Inboxes.Agent.update(
+ params
+ )
+ assert agent["object"] == "inbox_agent"
+ assert agent["id"] == "a1f84a4e-6f2b-4f0a-9c1d-8a2e5b3c7d90"
+
+ def test_should_update_inbox_agent_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Agent.update({"inbox_id": INBOX_ID, "tone": None})
+
+
+class TestResendInboxLabels(ResendBaseTest):
+ def test_inbox_labels_list(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "data": [
+ {
+ "id": LABEL_ID,
+ "name": "Urgent",
+ "color": "#E93D82",
+ "created_at": "2026-08-05T14:07:42.881Z",
+ }
+ ],
+ }
+ )
+ labels: resend.Inboxes.Labels.ListResponse = resend.Inboxes.Labels.list(
+ INBOX_ID
+ )
+ assert labels["object"] == "list"
+ assert labels["data"][0]["id"] == LABEL_ID
+ assert labels["data"][0]["color"] == "#E93D82"
+
+ def test_should_list_inbox_labels_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Labels.list(INBOX_ID)
+
+ def test_inbox_labels_create(self) -> None:
+ self.set_mock_json({"object": "inbox_label", "id": LABEL_ID})
+ params: resend.Inboxes.Labels.CreateParams = {
+ "inbox_id": INBOX_ID,
+ "name": "Urgent",
+ "color": "#E93D82",
+ }
+ label: resend.Inboxes.Labels.CreateResponse = resend.Inboxes.Labels.create(
+ params
+ )
+ assert label["object"] == "inbox_label"
+ assert label["id"] == LABEL_ID
+
+ def test_should_create_inbox_labels_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Labels.create({"inbox_id": INBOX_ID, "name": "Urgent"})
+
+ def test_inbox_labels_update(self) -> None:
+ self.set_mock_json({"object": "inbox_label", "id": LABEL_ID})
+ params: resend.Inboxes.Labels.UpdateParams = {
+ "inbox_id": INBOX_ID,
+ "id": LABEL_ID,
+ "name": "Billing",
+ "color": "#F76B15",
+ }
+ label: resend.Inboxes.Labels.UpdateResponse = resend.Inboxes.Labels.update(
+ params
+ )
+ assert label["object"] == "inbox_label"
+ assert label["id"] == LABEL_ID
+
+ def test_should_update_inbox_labels_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Labels.update(
+ {"inbox_id": INBOX_ID, "id": LABEL_ID, "name": "Billing"}
+ )
+
+ def test_inbox_labels_remove(self) -> None:
+ self.set_mock_json({"object": "inbox_label", "id": LABEL_ID, "deleted": True})
+ removed: resend.Inboxes.Labels.RemoveResponse = resend.Inboxes.Labels.remove(
+ INBOX_ID, LABEL_ID
+ )
+ assert removed["id"] == LABEL_ID
+ assert removed["deleted"] is True
+
+ def test_should_remove_inbox_labels_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Labels.remove(INBOX_ID, LABEL_ID)
+
+
+class TestResendInboxThreads(ResendBaseTest):
+ def test_inbox_threads_list(self) -> None:
+ self.set_mock_json({"object": "list", "has_more": True, "data": [THREAD]})
+ params: resend.Inboxes.Threads.ListParams = {
+ "folders": ["inbox", "sent"],
+ "read": False,
+ "limit": 10,
+ }
+ threads: resend.Inboxes.Threads.ListResponse = resend.Inboxes.Threads.list(
+ INBOX_ID, params
+ )
+ assert threads["object"] == "list"
+ assert threads["has_more"] is True
+ thread = threads["data"][0]
+ assert thread["id"] == THREAD_ID
+ assert thread["from"] == "ada@example.org"
+ assert thread["labels"][0]["color"] == "#E93D82"
+ assert thread["folder"] == "inbox"
+
+ def test_should_list_inbox_threads_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.list(INBOX_ID)
+
+ def test_inbox_threads_search(self) -> None:
+ result = dict(THREAD)
+ result["matched_email_id"] = EMAIL_ID
+ self.set_mock_json({"object": "list", "has_more": False, "data": [result]})
+ params: resend.Inboxes.Threads.SearchParams = {
+ "query": "invoice -draft",
+ "has_attachment": True,
+ }
+ threads: resend.Inboxes.Threads.SearchResponse = (
+ resend.Inboxes.Threads.search(INBOX_ID, params)
+ )
+ assert threads["object"] == "list"
+ match = threads["data"][0]
+ assert match["matched_email_id"] == EMAIL_ID
+ assert "highlights" not in match
+
+ def test_should_search_inbox_threads_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.search(INBOX_ID)
+
+ def test_inbox_threads_get(self) -> None:
+ self.set_mock_json(THREAD_SUMMARY)
+ thread: resend.InboxThreadSummary = resend.Inboxes.Threads.get(
+ INBOX_ID, THREAD_ID
+ )
+ assert thread["object"] == "inbox_thread"
+ assert thread["id"] == THREAD_ID
+ assert thread["read"] is False
+
+ def test_should_get_inbox_threads_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.get(INBOX_ID, THREAD_ID)
+
+ def test_inbox_threads_update(self) -> None:
+ summary = dict(THREAD_SUMMARY)
+ summary["folder"] = "archive"
+ summary["read"] = True
+ self.set_mock_json(summary)
+ params: resend.Inboxes.Threads.UpdateParams = {
+ "inbox_id": INBOX_ID,
+ "id": THREAD_ID,
+ "read": True,
+ "folder": "archive",
+ }
+ thread: resend.InboxThreadSummary = resend.Inboxes.Threads.update(params)
+ assert thread["folder"] == "archive"
+ assert thread["read"] is True
+
+ def test_should_update_inbox_threads_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.update(
+ {"inbox_id": INBOX_ID, "id": THREAD_ID, "read": True}
+ )
+
+ def test_inbox_threads_remove(self) -> None:
+ self.set_mock_json({"object": "inbox_thread", "id": THREAD_ID, "deleted": True})
+ removed: resend.Inboxes.Threads.RemoveResponse = (
+ resend.Inboxes.Threads.remove(INBOX_ID, THREAD_ID)
+ )
+ assert removed["id"] == THREAD_ID
+ assert removed["deleted"] is True
+
+ def test_should_remove_inbox_threads_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.remove(INBOX_ID, THREAD_ID)
+
+
+class TestResendInboxThreadLabels(ResendBaseTest):
+ def test_inbox_thread_labels_add(self) -> None:
+ self.set_mock_json({"object": "list", "data": [THREAD_LABEL]})
+ params: resend.Inboxes.Threads.Labels.AddParams = {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "labels": [LABEL_ID],
+ }
+ labels: resend.Inboxes.Threads.Labels.ListResponse = (
+ resend.Inboxes.Threads.Labels.add(params)
+ )
+ assert labels["object"] == "list"
+ assert labels["data"] == [THREAD_LABEL]
+
+ def test_should_add_inbox_thread_labels_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.Labels.add(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "labels": [LABEL_ID]}
+ )
+
+ def test_inbox_thread_labels_set(self) -> None:
+ self.set_mock_json({"object": "list", "data": []})
+ params: resend.Inboxes.Threads.Labels.SetParams = {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "labels": [],
+ }
+ labels: resend.Inboxes.Threads.Labels.ListResponse = (
+ resend.Inboxes.Threads.Labels.set(params)
+ )
+ assert labels["data"] == []
+
+ def test_should_set_inbox_thread_labels_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.Labels.set(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "labels": []}
+ )
+
+ def test_inbox_thread_labels_remove(self) -> None:
+ self.set_mock_json({"object": "list", "data": []})
+ labels: resend.Inboxes.Threads.Labels.ListResponse = (
+ resend.Inboxes.Threads.Labels.remove(INBOX_ID, THREAD_ID, LABEL_ID)
+ )
+ assert labels["object"] == "list"
+ assert labels["data"] == []
+
+ def test_should_remove_inbox_thread_labels_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.Labels.remove(INBOX_ID, THREAD_ID, LABEL_ID)
+
+
+class TestResendInboxThreadEmails(ResendBaseTest):
+ def test_inbox_thread_emails_list(self) -> None:
+ self.set_mock_json({"object": "list", "has_more": False, "data": [THREAD_EMAIL]})
+ emails: resend.Inboxes.Threads.Emails.ListResponse = (
+ resend.Inboxes.Threads.Emails.list(INBOX_ID, THREAD_ID, {"limit": 5})
+ )
+ assert emails["object"] == "list"
+ email = emails["data"][0]
+ assert email["id"] == EMAIL_ID
+ assert email["from"] == "Ada Lovelace "
+ assert email["attachments"][0]["filename"] == "receipt.pdf"
+
+ def test_should_list_inbox_thread_emails_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.Emails.list(INBOX_ID, THREAD_ID)
+
+ def test_inbox_thread_emails_get(self) -> None:
+ self.set_mock_json(THREAD_EMAIL)
+ email: resend.InboxThreadEmail = resend.Inboxes.Threads.Emails.get(
+ INBOX_ID, THREAD_ID, EMAIL_ID
+ )
+ assert email["id"] == EMAIL_ID
+ assert email["direction"] == "inbound"
+ assert email["reply_to"] == ["replies@example.org"]
+
+ def test_should_get_inbox_thread_emails_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.Emails.get(INBOX_ID, THREAD_ID, EMAIL_ID)
+
+ def test_inbox_thread_emails_reply(self) -> None:
+ self.set_mock_json({"id": SENT_EMAIL_ID})
+ params: resend.Inboxes.Threads.Emails.ReplyParams = {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "text": "Refund issued for order 1041.",
+ "reply_all": True,
+ "attachments": [{"filename": "note.txt", "content": "aGVsbG8="}],
+ }
+ reply: resend.Inboxes.Threads.Emails.ReplyResponse = (
+ resend.Inboxes.Threads.Emails.reply(params)
+ )
+ assert reply["id"] == SENT_EMAIL_ID
+
+ def test_should_reply_inbox_thread_emails_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.Emails.reply(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "text": "Hi",
+ }
+ )
+
+ def test_inbox_thread_emails_forward(self) -> None:
+ self.set_mock_json({"id": SENT_EMAIL_ID})
+ params: resend.Inboxes.Threads.Emails.ForwardParams = {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "to": ["colleague@example.org"],
+ "text": "Flagging this refund request for you.",
+ }
+ forward: resend.Inboxes.Threads.Emails.ForwardResponse = (
+ resend.Inboxes.Threads.Emails.forward(params)
+ )
+ assert forward["id"] == SENT_EMAIL_ID
+
+ def test_should_forward_inbox_thread_emails_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Threads.Emails.forward(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "to": "colleague@example.org",
+ }
+ )
+
+
+class TestResendInboxDrafts(ResendBaseTest):
+ def test_inbox_drafts_list(self) -> None:
+ self.set_mock_json(
+ {
+ "object": "list",
+ "has_more": False,
+ "data": [
+ {
+ "id": DRAFT_ID,
+ "type": "standalone",
+ "to": ["ada@example.org"],
+ "cc": [],
+ "bcc": [],
+ "subject": "Refund for order 1041",
+ "snippet": "Refund issued for order 1041.",
+ "thread_id": None,
+ "reply_to_email_id": None,
+ "updated_at": "2026-08-05T14:12:04.110Z",
+ "attachments": [],
+ }
+ ],
+ }
+ )
+ drafts: resend.Inboxes.Drafts.ListResponse = resend.Inboxes.Drafts.list(
+ INBOX_ID
+ )
+ assert drafts["object"] == "list"
+ assert drafts["data"][0]["id"] == DRAFT_ID
+ assert drafts["data"][0]["snippet"] == "Refund issued for order 1041."
+ assert drafts["data"][0]["attachments"] == []
+
+ def test_should_list_inbox_drafts_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Drafts.list(INBOX_ID)
+
+ def test_inbox_drafts_create(self) -> None:
+ self.set_mock_json({"object": "inbox_draft", "id": DRAFT_ID})
+ params: resend.Inboxes.Drafts.CreateParams = {
+ "inbox_id": INBOX_ID,
+ "to": ["ada@example.org"],
+ "subject": "Refund for order 1041",
+ "text": "Refund issued for order 1041.",
+ "attachments": [
+ {
+ "content": "aGVsbG8=",
+ "filename": "note.txt",
+ "content_type": "text/plain",
+ }
+ ],
+ }
+ draft: resend.Inboxes.Drafts.CreateResponse = resend.Inboxes.Drafts.create(
+ params
+ )
+ assert draft["object"] == "inbox_draft"
+ assert draft["id"] == DRAFT_ID
+
+ def test_should_create_inbox_drafts_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Drafts.create({"inbox_id": INBOX_ID, "text": "Hi"})
+
+ def test_inbox_drafts_get(self) -> None:
+ self.set_mock_json(DRAFT)
+ draft: resend.InboxDraft = resend.Inboxes.Drafts.get(INBOX_ID, DRAFT_ID)
+ assert draft["object"] == "inbox_draft"
+ assert draft["id"] == DRAFT_ID
+ assert draft["type"] == "standalone"
+ assert draft["email_id"] is None
+ assert draft["attachments"][0]["content_type"] == "application/pdf"
+ assert draft["attachments"][0]["size"] == 20841
+
+ def test_should_get_inbox_drafts_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Drafts.get(INBOX_ID, DRAFT_ID)
+
+ def test_inbox_drafts_update(self) -> None:
+ self.set_mock_json({"object": "inbox_draft", "id": DRAFT_ID})
+ params: resend.Inboxes.Drafts.UpdateParams = {
+ "inbox_id": INBOX_ID,
+ "id": DRAFT_ID,
+ "text": "Refund issued for order 1041.",
+ "cc": None,
+ }
+ draft: resend.Inboxes.Drafts.UpdateResponse = resend.Inboxes.Drafts.update(
+ params
+ )
+ assert draft == {"object": "inbox_draft", "id": DRAFT_ID}
+
+ def test_should_update_inbox_drafts_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Drafts.update(
+ {"inbox_id": INBOX_ID, "id": DRAFT_ID, "subject": None}
+ )
+
+ def test_inbox_drafts_remove(self) -> None:
+ self.set_mock_json({"object": "inbox_draft", "id": DRAFT_ID, "deleted": True})
+ removed: resend.Inboxes.Drafts.RemoveResponse = resend.Inboxes.Drafts.remove(
+ INBOX_ID, DRAFT_ID
+ )
+ assert removed["id"] == DRAFT_ID
+ assert removed["deleted"] is True
+
+ def test_should_remove_inbox_drafts_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Drafts.remove(INBOX_ID, DRAFT_ID)
+
+ def test_inbox_drafts_send(self) -> None:
+ self.set_mock_json({"id": SENT_EMAIL_ID})
+ sent: resend.Inboxes.Drafts.SendResponse = resend.Inboxes.Drafts.send(
+ INBOX_ID, DRAFT_ID
+ )
+ assert sent["id"] == SENT_EMAIL_ID
+
+ def test_should_send_inbox_drafts_raise_exception_when_no_content(
+ self,
+ ) -> None:
+ self.set_mock_json(None)
+ with self.assertRaises(NoContentError):
+ _ = resend.Inboxes.Drafts.send(INBOX_ID, DRAFT_ID)
+
+
+class TestInboxesRequest(TestCase):
+ def setUp(self) -> None:
+ resend.api_key = "re_123"
+ self.mock_client = create_autospec(HTTPClient, instance=True)
+ self.mock_client.name = "mock"
+ self.mock_client.request.return_value = (
+ b'{"object": "inbox", "id": "b3e2b2b6-3f0e-4c8e-9ad3-2f43a1e2c7f1"}',
+ 200,
+ {"Content-Type": "application/json"},
+ )
+ self.previous_http_client = resend.default_http_client
+ resend.default_http_client = self.mock_client
+
+ def tearDown(self) -> None:
+ resend.default_http_client = self.previous_http_client
+
+ def last_request(self) -> Dict[str, Any]:
+ _, kwargs = self.mock_client.request.call_args
+ return dict(kwargs)
+
+ def test_create_posts_the_params(self) -> None:
+ resend.Inboxes.create({"email_address": "support@acme.com", "forwarding": True})
+ req = self.last_request()
+ assert req["method"] == "post"
+ assert req["url"] == "https://api.resend.com/inboxes"
+ assert req["json"] == {"email_address": "support@acme.com", "forwarding": True}
+
+ def test_list_adds_the_pagination_query(self) -> None:
+ resend.Inboxes.list({"limit": 2, "after": INBOX_ID})
+ req = self.last_request()
+ assert req["method"] == "get"
+ assert req["url"] == f"https://api.resend.com/inboxes?limit=2&after={INBOX_ID}"
+
+ def test_get_encodes_an_email_address(self) -> None:
+ resend.Inboxes.get("support@acme.com")
+ assert (
+ self.last_request()["url"]
+ == "https://api.resend.com/inboxes/support%40acme.com"
+ )
+
+ def test_update_keeps_the_id_out_of_the_body(self) -> None:
+ resend.Inboxes.update({"id": INBOX_ID, "from_name": ""})
+ req = self.last_request()
+ assert req["method"] == "patch"
+ assert req["url"] == f"https://api.resend.com/inboxes/{INBOX_ID}"
+ assert req["json"] == {"from_name": ""}
+
+ def test_remove_deletes_the_inbox(self) -> None:
+ resend.Inboxes.remove(INBOX_ID)
+ req = self.last_request()
+ assert req["method"] == "delete"
+ assert req["url"] == f"https://api.resend.com/inboxes/{INBOX_ID}"
+
+ def test_agent_get_and_update(self) -> None:
+ resend.Inboxes.Agent.get(INBOX_ID)
+ assert self.last_request()["url"] == f"https://api.resend.com/inboxes/{INBOX_ID}/agent"
+
+ resend.Inboxes.Agent.update(
+ {"inbox_id": INBOX_ID, "instructions": None, "actions": []}
+ )
+ req = self.last_request()
+ assert req["method"] == "patch"
+ assert req["url"] == f"https://api.resend.com/inboxes/{INBOX_ID}/agent"
+ assert req["json"] == {"instructions": None, "actions": []}
+
+ def test_every_inbox_route_encodes_an_email_address(self) -> None:
+ base = "https://api.resend.com/inboxes/support%40acme.com"
+ calls: List[Tuple[Callable[[], Any], str]] = [
+ (lambda: resend.Inboxes.update({"id": "support@acme.com", "name": "S"}), ""),
+ (lambda: resend.Inboxes.remove("support@acme.com"), ""),
+ (lambda: resend.Inboxes.Agent.get("support@acme.com"), "/agent"),
+ (
+ lambda: resend.Inboxes.Agent.update(
+ {"inbox_id": "support@acme.com", "tone": None}
+ ),
+ "/agent",
+ ),
+ (lambda: resend.Inboxes.Labels.list("support@acme.com"), "/labels"),
+ (
+ lambda: resend.Inboxes.Labels.create(
+ {"inbox_id": "support@acme.com", "name": "Urgent"}
+ ),
+ "/labels",
+ ),
+ (
+ lambda: resend.Inboxes.Labels.update(
+ {"inbox_id": "support@acme.com", "id": LABEL_ID, "name": "U"}
+ ),
+ f"/labels/{LABEL_ID}",
+ ),
+ (
+ lambda: resend.Inboxes.Labels.remove("support@acme.com", LABEL_ID),
+ f"/labels/{LABEL_ID}",
+ ),
+ (lambda: resend.Inboxes.Threads.list("support@acme.com"), "/threads"),
+ (
+ lambda: resend.Inboxes.Threads.search("support@acme.com"),
+ "/threads/search",
+ ),
+ (
+ lambda: resend.Inboxes.Threads.get("support@acme.com", THREAD_ID),
+ f"/threads/{THREAD_ID}",
+ ),
+ (
+ lambda: resend.Inboxes.Threads.update(
+ {"inbox_id": "support@acme.com", "id": THREAD_ID, "read": True}
+ ),
+ f"/threads/{THREAD_ID}",
+ ),
+ (
+ lambda: resend.Inboxes.Threads.remove("support@acme.com", THREAD_ID),
+ f"/threads/{THREAD_ID}",
+ ),
+ (
+ lambda: resend.Inboxes.Threads.Labels.add(
+ {
+ "inbox_id": "support@acme.com",
+ "thread_id": THREAD_ID,
+ "labels": [LABEL_ID],
+ }
+ ),
+ f"/threads/{THREAD_ID}/labels",
+ ),
+ (
+ lambda: resend.Inboxes.Threads.Emails.list(
+ "support@acme.com", THREAD_ID
+ ),
+ f"/threads/{THREAD_ID}/emails",
+ ),
+ (
+ lambda: resend.Inboxes.Threads.Emails.reply(
+ {
+ "inbox_id": "support@acme.com",
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "text": "Hi",
+ }
+ ),
+ f"/threads/{THREAD_ID}/emails/{EMAIL_ID}/reply",
+ ),
+ (lambda: resend.Inboxes.Drafts.list("support@acme.com"), "/drafts"),
+ (
+ lambda: resend.Inboxes.Drafts.create(
+ {"inbox_id": "support@acme.com", "text": "Hi"}
+ ),
+ "/drafts",
+ ),
+ (
+ lambda: resend.Inboxes.Drafts.send("support@acme.com", DRAFT_ID),
+ f"/drafts/{DRAFT_ID}/send",
+ ),
+ ]
+ for call, suffix in calls:
+ call()
+ assert self.last_request()["url"] == f"{base}{suffix}"
+
+ def test_labels_paths_and_bodies(self) -> None:
+ base = f"https://api.resend.com/inboxes/{INBOX_ID}/labels"
+ resend.Inboxes.Labels.list(INBOX_ID)
+ assert self.last_request()["url"] == base
+
+ resend.Inboxes.Labels.create({"inbox_id": INBOX_ID, "name": "Urgent"})
+ req = self.last_request()
+ assert (req["method"], req["url"], req["json"]) == (
+ "post",
+ base,
+ {"name": "Urgent"},
+ )
+
+ resend.Inboxes.Labels.update(
+ {"inbox_id": INBOX_ID, "id": LABEL_ID, "color": "#F76B15"}
+ )
+ req = self.last_request()
+ assert (req["method"], req["url"], req["json"]) == (
+ "patch",
+ f"{base}/{LABEL_ID}",
+ {"color": "#F76B15"},
+ )
+
+ resend.Inboxes.Labels.remove(INBOX_ID, LABEL_ID)
+ req = self.last_request()
+ assert (req["method"], req["url"]) == ("delete", f"{base}/{LABEL_ID}")
+
+ def test_threads_list_joins_lists_and_lowercases_booleans(self) -> None:
+ resend.Inboxes.Threads.list(
+ INBOX_ID,
+ {"folders": ["inbox", "sent"], "labels": [LABEL_ID], "read": False},
+ )
+ assert self.last_request()["url"] == (
+ f"https://api.resend.com/inboxes/{INBOX_ID}/threads"
+ f"?folders=inbox%2Csent&labels={LABEL_ID}&read=false"
+ )
+
+ def test_threads_list_leaves_out_empty_lists(self) -> None:
+ resend.Inboxes.Threads.list(INBOX_ID, {"folders": [], "labels": [], "limit": 5})
+ assert (
+ self.last_request()["url"]
+ == f"https://api.resend.com/inboxes/{INBOX_ID}/threads?limit=5"
+ )
+
+ def test_threads_search_builds_the_query(self) -> None:
+ params: resend.Inboxes.Threads.SearchParams = {
+ "query": "invoice -draft",
+ "from": ["isabella@example.com", "carolina@example.com"],
+ "has_attachment": True,
+ "start_date": "2026-09-01",
+ }
+ resend.Inboxes.Threads.search(INBOX_ID, params)
+ assert self.last_request()["url"] == (
+ f"https://api.resend.com/inboxes/{INBOX_ID}/threads/search"
+ "?query=invoice+-draft"
+ "&from=isabella%40example.com%2Ccarolina%40example.com"
+ "&has_attachment=true&start_date=2026-09-01"
+ )
+
+ def test_threads_get_update_and_remove(self) -> None:
+ url = f"https://api.resend.com/inboxes/{INBOX_ID}/threads/{THREAD_ID}"
+ resend.Inboxes.Threads.get(INBOX_ID, THREAD_ID)
+ assert self.last_request()["url"] == url
+
+ resend.Inboxes.Threads.update(
+ {"inbox_id": INBOX_ID, "id": THREAD_ID, "folder": "archive"}
+ )
+ req = self.last_request()
+ assert (req["method"], req["url"]) == ("patch", url)
+ assert req["json"] == {"folder": "archive"}
+
+ resend.Inboxes.Threads.remove(INBOX_ID, THREAD_ID)
+ req = self.last_request()
+ assert (req["method"], req["url"]) == ("delete", url)
+
+ def test_thread_labels_paths_and_bodies(self) -> None:
+ base = f"https://api.resend.com/inboxes/{INBOX_ID}/threads/{THREAD_ID}/labels"
+ resend.Inboxes.Threads.Labels.add(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "labels": [LABEL_ID]}
+ )
+ req = self.last_request()
+ assert (req["method"], req["url"], req["json"]) == (
+ "post",
+ base,
+ {"labels": [LABEL_ID]},
+ )
+
+ resend.Inboxes.Threads.Labels.set(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "labels": []}
+ )
+ req = self.last_request()
+ assert (req["method"], req["url"], req["json"]) == ("put", base, {"labels": []})
+
+ resend.Inboxes.Threads.Labels.remove(INBOX_ID, THREAD_ID, LABEL_ID)
+ req = self.last_request()
+ assert (req["method"], req["url"]) == ("delete", f"{base}/{LABEL_ID}")
+
+ def test_thread_emails_paths_and_bodies(self) -> None:
+ base = f"https://api.resend.com/inboxes/{INBOX_ID}/threads/{THREAD_ID}/emails"
+ resend.Inboxes.Threads.Emails.list(INBOX_ID, THREAD_ID, {"before": EMAIL_ID})
+ assert self.last_request()["url"] == f"{base}?before={EMAIL_ID}"
+
+ resend.Inboxes.Threads.Emails.get(INBOX_ID, THREAD_ID, EMAIL_ID)
+ assert self.last_request()["url"] == f"{base}/{EMAIL_ID}"
+
+ attachment: resend.Attachment = {"filename": "note.txt", "content": "aGVsbG8="}
+ resend.Inboxes.Threads.Emails.reply(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "html": "Done.
",
+ "reply_all": True,
+ "attachments": [attachment],
+ }
+ )
+ req = self.last_request()
+ assert (req["method"], req["url"]) == ("post", f"{base}/{EMAIL_ID}/reply")
+ assert req["json"] == {
+ "html": "Done.
",
+ "reply_all": True,
+ "attachments": [attachment],
+ }
+
+ resend.Inboxes.Threads.Emails.forward(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "to": "colleague@example.org",
+ }
+ )
+ req = self.last_request()
+ assert (req["method"], req["url"]) == ("post", f"{base}/{EMAIL_ID}/forward")
+ assert req["json"] == {"to": "colleague@example.org"}
+
+ def test_drafts_paths_and_bodies(self) -> None:
+ base = f"https://api.resend.com/inboxes/{INBOX_ID}/drafts"
+ resend.Inboxes.Drafts.list(INBOX_ID, {"limit": 1})
+ assert self.last_request()["url"] == f"{base}?limit=1"
+
+ resend.Inboxes.Drafts.create(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "reply_to_email_id": EMAIL_ID,
+ "to": [],
+ }
+ )
+ req = self.last_request()
+ assert (req["method"], req["url"]) == ("post", base)
+ assert req["json"] == {
+ "thread_id": THREAD_ID,
+ "reply_to_email_id": EMAIL_ID,
+ "to": [],
+ }
+
+ resend.Inboxes.Drafts.get(INBOX_ID, DRAFT_ID)
+ assert self.last_request()["url"] == f"{base}/{DRAFT_ID}"
+
+ resend.Inboxes.Drafts.update({"inbox_id": INBOX_ID, "id": DRAFT_ID, "to": None})
+ req = self.last_request()
+ assert (req["method"], req["url"], req["json"]) == (
+ "patch",
+ f"{base}/{DRAFT_ID}",
+ {"to": None},
+ )
+
+ attachment: resend.Inboxes.Drafts.AttachmentParams = {
+ "content": "aGVsbG8=",
+ "filename": "note.txt",
+ "content_type": "text/plain",
+ }
+ resend.Inboxes.Drafts.create(
+ {"inbox_id": INBOX_ID, "to": "ada@example.org", "attachments": [attachment]}
+ )
+ assert self.last_request()["json"] == {
+ "to": "ada@example.org",
+ "attachments": [attachment],
+ }
+
+ resend.Inboxes.Drafts.update(
+ {"inbox_id": INBOX_ID, "id": DRAFT_ID, "attachments": []}
+ )
+ assert self.last_request()["json"] == {"attachments": []}
+
+ resend.Inboxes.Drafts.update(
+ {"inbox_id": INBOX_ID, "id": DRAFT_ID, "attachments": None}
+ )
+ assert self.last_request()["json"] == {"attachments": None}
+
+ resend.Inboxes.Drafts.remove(INBOX_ID, DRAFT_ID)
+ req = self.last_request()
+ assert (req["method"], req["url"]) == ("delete", f"{base}/{DRAFT_ID}")
+
+ resend.Inboxes.Drafts.send(INBOX_ID, DRAFT_ID)
+ req = self.last_request()
+ assert (req["method"], req["url"]) == ("post", f"{base}/{DRAFT_ID}/send")
+
+ def test_sending_methods_send_the_idempotency_key(self) -> None:
+ resend.Inboxes.Threads.Emails.reply(
+ {"inbox_id": INBOX_ID, "thread_id": THREAD_ID, "email_id": EMAIL_ID, "text": "Hi"},
+ {"idempotency_key": "reply-1"},
+ )
+ assert self.last_request()["headers"]["Idempotency-Key"] == "reply-1"
+
+ resend.Inboxes.Threads.Emails.forward(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "to": "colleague@example.org",
+ },
+ {"idempotency_key": "forward-1"},
+ )
+ assert self.last_request()["headers"]["Idempotency-Key"] == "forward-1"
+
+ resend.Inboxes.Drafts.create(
+ {"inbox_id": INBOX_ID, "text": "Hi"}, {"idempotency_key": "create-1"}
+ )
+ req = self.last_request()
+ assert req["headers"]["Idempotency-Key"] == "create-1"
+ assert req["json"] == {"text": "Hi"}
+
+ resend.Inboxes.Drafts.send(
+ INBOX_ID, DRAFT_ID, options={"idempotency_key": "send-1"}
+ )
+ assert self.last_request()["headers"]["Idempotency-Key"] == "send-1"
+
+ resend.Inboxes.Drafts.send(INBOX_ID, DRAFT_ID)
+ assert "Idempotency-Key" not in self.last_request()["headers"]
+
+ def test_sending_methods_send_custom_headers(self) -> None:
+ headers = {"X-Ticket-Id": "4821"}
+ resend.Inboxes.Threads.Emails.reply(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "text": "Hi",
+ "headers": headers,
+ }
+ )
+ assert self.last_request()["json"] == {"text": "Hi", "headers": headers}
+
+ resend.Inboxes.Threads.Emails.forward(
+ {
+ "inbox_id": INBOX_ID,
+ "thread_id": THREAD_ID,
+ "email_id": EMAIL_ID,
+ "to": "colleague@example.org",
+ "headers": headers,
+ }
+ )
+ assert self.last_request()["json"] == {
+ "to": "colleague@example.org",
+ "headers": headers,
+ }
+
+ resend.Inboxes.Drafts.send(INBOX_ID, DRAFT_ID, {"headers": headers})
+ req = self.last_request()
+ assert req["url"] == f"https://api.resend.com/inboxes/{INBOX_ID}/drafts/{DRAFT_ID}/send"
+ assert req["json"] == {"headers": headers}
+
+ resend.Inboxes.Drafts.send(INBOX_ID, DRAFT_ID)
+ assert self.last_request()["json"] == {}