From 9eba093e8336bf63413e5fbf7a560b458388dc0c Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 30 Oct 2026 13:06:23 -0400 Subject: [PATCH 1/8] feat(inboxes): accept path attachments on drafts --- resend/inboxes/drafts/_inbox_drafts.py | 54 +++++++++++++++++++------- tests/inboxes_test.py | 37 ++++++++++++++++++ 2 files changed, 78 insertions(+), 13 deletions(-) diff --git a/resend/inboxes/drafts/_inbox_drafts.py b/resend/inboxes/drafts/_inbox_drafts.py index 0ecb346..82ac19d 100644 --- a/resend/inboxes/drafts/_inbox_drafts.py +++ b/resend/inboxes/drafts/_inbox_drafts.py @@ -21,11 +21,9 @@ class InboxDrafts: InboxDrafts manages the drafts of an inbox. """ - class AttachmentParams(TypedDict): + class ContentAttachmentParams(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. + ContentAttachmentParams is a file to store on a draft, sent inline. Attributes: content (str): The content of the file, encoded in base64. @@ -46,6 +44,34 @@ class AttachmentParams(TypedDict): The MIME type of the file, for example ``application/pdf``. """ + class PathAttachmentParams(TypedDict): + """ + PathAttachmentParams is a file to store on a draft, downloaded from a URL + when the request is made. + + Attributes: + path (str): The URL to download the file from. + filename (str): The name of the file. + content_type (NotRequired[str]): The MIME type of the file. + """ + + path: str + """ + The URL to download the file from, starting with ``http`` or ``https``. + A file that cannot be downloaded returns a 422. + """ + filename: str + """ + The name of the file. + """ + content_type: NotRequired[str] + """ + The MIME type of the file. Defaults to the content type of the + downloaded file. + """ + + AttachmentParams = Union[ContentAttachmentParams, PathAttachmentParams] + class ListParams(TypedDict): """ ListParams are the pagination parameters for listing drafts. @@ -148,10 +174,11 @@ class CreateParams(TypedDict): """ 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. + Files to store on the draft. Each one needs a ``filename`` and either + ``content`` with a ``content_type``, or a ``path`` URL that is downloaded + when the request is made. The files on a draft, downloaded ones included, + can total at most 25 MB. An empty file, a blocked file type, or a ``path`` + that cannot be downloaded returns a 422. """ thread_id: NotRequired[str] """ @@ -260,11 +287,12 @@ class UpdateParams(TypedDict): """ 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. + Files to store on the draft. Each one needs a ``filename`` and either + ``content`` with a ``content_type``, or a ``path`` URL that is downloaded + when the request is made. The files on a draft, downloaded ones included, + can total at most 25 MB. An empty file, a blocked file type, or a ``path`` + that cannot be downloaded returns a 422. Sending the field replaces every + attachment on the draft. Send ``[]`` or None to remove them all. """ class UpdateResponse(BaseResponse): diff --git a/tests/inboxes_test.py b/tests/inboxes_test.py index 93cc449..2e5ec85 100644 --- a/tests/inboxes_test.py +++ b/tests/inboxes_test.py @@ -720,6 +720,20 @@ def test_should_send_inbox_drafts_raise_exception_when_no_content( _ = resend.Inboxes.Drafts.send(INBOX_ID, DRAFT_ID) +def _invalid_draft_attachments() -> List[resend.Inboxes.Drafts.AttachmentParams]: + """Checked by mypy: each entry must stay a type error.""" + return [ + {"filename": "note.txt"}, # type: ignore[list-item] + {"content": "aGVsbG8=", "filename": "note.txt"}, # type: ignore[list-item] + { # type: ignore[list-item] + "content": "aGVsbG8=", + "path": "https://resend.com/static/sample/invoice.pdf", + "filename": "note.txt", + "content_type": "text/plain", + }, + ] + + class TestInboxesRequest(TestCase): def setUp(self) -> None: resend.api_key = "re_123" @@ -1057,6 +1071,29 @@ def test_drafts_paths_and_bodies(self) -> None: "attachments": [attachment], } + path_attachment: resend.Inboxes.Drafts.AttachmentParams = { + "path": "https://resend.com/static/sample/invoice.pdf", + "filename": "invoice.pdf", + } + resend.Inboxes.Drafts.create( + {"inbox_id": INBOX_ID, "attachments": [path_attachment]} + ) + assert self.last_request()["json"] == {"attachments": [path_attachment]} + + typed_path_attachment: resend.Inboxes.Drafts.AttachmentParams = { + "path": "https://resend.com/static/sample/invoice.pdf", + "filename": "invoice.pdf", + "content_type": "application/pdf", + } + resend.Inboxes.Drafts.update( + { + "inbox_id": INBOX_ID, + "id": DRAFT_ID, + "attachments": [typed_path_attachment], + } + ) + assert self.last_request()["json"] == {"attachments": [typed_path_attachment]} + resend.Inboxes.Drafts.update( {"inbox_id": INBOX_ID, "id": DRAFT_ID, "attachments": []} ) From 72eae4ff517dcb7acee28a8ef60adcb0d8a44d59 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 30 Oct 2026 13:06:23 -0400 Subject: [PATCH 2/8] docs(examples): attach a file by path to the inbox draft --- examples/inboxes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/inboxes.py b/examples/inboxes.py index ef7abc1..bbf9f80 100644 --- a/examples/inboxes.py +++ b/examples/inboxes.py @@ -132,7 +132,11 @@ "content": "UmVmdW5kIHJlY2VpcHQ=", "filename": "receipt.txt", "content_type": "text/plain", - } + }, + { + "path": "https://resend.com/static/sample/invoice.pdf", + "filename": "invoice.pdf", + }, ], } draft: resend.Inboxes.Drafts.CreateResponse = resend.Inboxes.Drafts.create( From 361d7461765e7f9578cd8b8db809b47879b7f6f1 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 30 Oct 2026 13:06:23 -0400 Subject: [PATCH 3/8] chore: bump version to 2.51.0 --- resend/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resend/version.py b/resend/version.py index b77813e..09e91aa 100644 --- a/resend/version.py +++ b/resend/version.py @@ -1,4 +1,4 @@ -__version__ = "2.50.1" +__version__ = "2.51.0" def get_version() -> str: From 39183bff9307278052cbf53f9c251ca8bca5a3f2 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 30 Oct 2026 13:27:14 -0400 Subject: [PATCH 4/8] test: use a placeholder URL for path attachments --- examples/inboxes.py | 4 ++-- tests/inboxes_test.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/inboxes.py b/examples/inboxes.py index bbf9f80..838cca0 100644 --- a/examples/inboxes.py +++ b/examples/inboxes.py @@ -134,8 +134,8 @@ "content_type": "text/plain", }, { - "path": "https://resend.com/static/sample/invoice.pdf", - "filename": "invoice.pdf", + "path": "https://resend.com/static/brand/resend-wordmark-black.png", + "filename": "resend-wordmark-black.png", }, ], } diff --git a/tests/inboxes_test.py b/tests/inboxes_test.py index 2e5ec85..913c7b2 100644 --- a/tests/inboxes_test.py +++ b/tests/inboxes_test.py @@ -727,7 +727,7 @@ def _invalid_draft_attachments() -> List[resend.Inboxes.Drafts.AttachmentParams] {"content": "aGVsbG8=", "filename": "note.txt"}, # type: ignore[list-item] { # type: ignore[list-item] "content": "aGVsbG8=", - "path": "https://resend.com/static/sample/invoice.pdf", + "path": "https://example.com/invoice.pdf", "filename": "note.txt", "content_type": "text/plain", }, @@ -1072,7 +1072,7 @@ def test_drafts_paths_and_bodies(self) -> None: } path_attachment: resend.Inboxes.Drafts.AttachmentParams = { - "path": "https://resend.com/static/sample/invoice.pdf", + "path": "https://example.com/invoice.pdf", "filename": "invoice.pdf", } resend.Inboxes.Drafts.create( @@ -1081,7 +1081,7 @@ def test_drafts_paths_and_bodies(self) -> None: assert self.last_request()["json"] == {"attachments": [path_attachment]} typed_path_attachment: resend.Inboxes.Drafts.AttachmentParams = { - "path": "https://resend.com/static/sample/invoice.pdf", + "path": "https://example.com/invoice.pdf", "filename": "invoice.pdf", "content_type": "application/pdf", } From 1c0079ec5dec62235a8ef1d3ecd856c7cafd3d20 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 30 Oct 2026 13:57:22 -0400 Subject: [PATCH 5/8] docs(inboxes): describe path attachments in the create draft docstrings --- resend/inboxes/drafts/_inbox_drafts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resend/inboxes/drafts/_inbox_drafts.py b/resend/inboxes/drafts/_inbox_drafts.py index 82ac19d..7e5cd75 100644 --- a/resend/inboxes/drafts/_inbox_drafts.py +++ b/resend/inboxes/drafts/_inbox_drafts.py @@ -422,8 +422,8 @@ def create( ``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 + stored on the draft, so a ``path`` file is downloaded when the request + is made. 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 @@ -578,8 +578,8 @@ async def create_async( ``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 + stored on the draft, so a ``path`` file is downloaded when the request + is made. 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 From 1a2b14637d2dbdc4d38cea14a04c2f9db744dc94 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 30 Oct 2026 14:11:54 -0400 Subject: [PATCH 6/8] fix(inboxes): keep AttachmentParams constructible and add DraftAttachmentParams --- resend/inboxes/drafts/_inbox_drafts.py | 14 +++++++------- tests/inboxes_test.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/resend/inboxes/drafts/_inbox_drafts.py b/resend/inboxes/drafts/_inbox_drafts.py index 7e5cd75..833db29 100644 --- a/resend/inboxes/drafts/_inbox_drafts.py +++ b/resend/inboxes/drafts/_inbox_drafts.py @@ -21,9 +21,9 @@ class InboxDrafts: InboxDrafts manages the drafts of an inbox. """ - class ContentAttachmentParams(TypedDict): + class AttachmentParams(TypedDict): """ - ContentAttachmentParams is a file to store on a draft, sent inline. + AttachmentParams is a file to store on a draft, sent inline. Attributes: content (str): The content of the file, encoded in base64. @@ -70,7 +70,7 @@ class PathAttachmentParams(TypedDict): downloaded file. """ - AttachmentParams = Union[ContentAttachmentParams, PathAttachmentParams] + DraftAttachmentParams = Union[AttachmentParams, PathAttachmentParams] class ListParams(TypedDict): """ @@ -137,7 +137,7 @@ class CreateParams(TypedDict): 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 to store on the draft. + attachments (NotRequired[Optional[List[DraftAttachmentParams]]]): 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. """ @@ -172,7 +172,7 @@ class CreateParams(TypedDict): """ The plain text body. """ - attachments: NotRequired[Optional[List["InboxDrafts.AttachmentParams"]]] + attachments: NotRequired[Optional[List["InboxDrafts.DraftAttachmentParams"]]] """ Files to store on the draft. Each one needs a ``filename`` and either ``content`` with a ``content_type``, or a ``path`` URL that is downloaded @@ -247,7 +247,7 @@ class UpdateParams(TypedDict): 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 + attachments (NotRequired[Optional[List[DraftAttachmentParams]]]): Files that replace every attachment on the draft. """ @@ -285,7 +285,7 @@ class UpdateParams(TypedDict): """ The plain text body. Pass None to clear. """ - attachments: NotRequired[Optional[List["InboxDrafts.AttachmentParams"]]] + attachments: NotRequired[Optional[List["InboxDrafts.DraftAttachmentParams"]]] """ Files to store on the draft. Each one needs a ``filename`` and either ``content`` with a ``content_type``, or a ``path`` URL that is downloaded diff --git a/tests/inboxes_test.py b/tests/inboxes_test.py index 913c7b2..151756c 100644 --- a/tests/inboxes_test.py +++ b/tests/inboxes_test.py @@ -720,7 +720,7 @@ def test_should_send_inbox_drafts_raise_exception_when_no_content( _ = resend.Inboxes.Drafts.send(INBOX_ID, DRAFT_ID) -def _invalid_draft_attachments() -> List[resend.Inboxes.Drafts.AttachmentParams]: +def _invalid_draft_attachments() -> List[resend.Inboxes.Drafts.DraftAttachmentParams]: """Checked by mypy: each entry must stay a type error.""" return [ {"filename": "note.txt"}, # type: ignore[list-item] @@ -1058,11 +1058,11 @@ def test_drafts_paths_and_bodies(self) -> None: {"to": None}, ) - attachment: resend.Inboxes.Drafts.AttachmentParams = { - "content": "aGVsbG8=", - "filename": "note.txt", - "content_type": "text/plain", - } + 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]} ) @@ -1071,7 +1071,7 @@ def test_drafts_paths_and_bodies(self) -> None: "attachments": [attachment], } - path_attachment: resend.Inboxes.Drafts.AttachmentParams = { + path_attachment: resend.Inboxes.Drafts.DraftAttachmentParams = { "path": "https://example.com/invoice.pdf", "filename": "invoice.pdf", } @@ -1080,7 +1080,7 @@ def test_drafts_paths_and_bodies(self) -> None: ) assert self.last_request()["json"] == {"attachments": [path_attachment]} - typed_path_attachment: resend.Inboxes.Drafts.AttachmentParams = { + typed_path_attachment: resend.Inboxes.Drafts.DraftAttachmentParams = { "path": "https://example.com/invoice.pdf", "filename": "invoice.pdf", "content_type": "application/pdf", From dba9a57f829c9fc4a68894964d965f8f8a06e038 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 30 Oct 2026 14:17:22 -0400 Subject: [PATCH 7/8] test(inboxes): build the draft attachment through InboxDrafts for older mypy --- tests/inboxes_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/inboxes_test.py b/tests/inboxes_test.py index 151756c..c0d1b2a 100644 --- a/tests/inboxes_test.py +++ b/tests/inboxes_test.py @@ -1058,7 +1058,7 @@ def test_drafts_paths_and_bodies(self) -> None: {"to": None}, ) - attachment = resend.Inboxes.Drafts.AttachmentParams( + attachment = resend.InboxDrafts.AttachmentParams( content="aGVsbG8=", filename="note.txt", content_type="text/plain", From 678c40b435bbb5853e72397d524b1b6819ed46e0 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 30 Oct 2026 17:17:28 -0400 Subject: [PATCH 8/8] docs: use a placeholder URL in the draft attachment example --- examples/inboxes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/inboxes.py b/examples/inboxes.py index 838cca0..7cafc04 100644 --- a/examples/inboxes.py +++ b/examples/inboxes.py @@ -134,8 +134,8 @@ "content_type": "text/plain", }, { - "path": "https://resend.com/static/brand/resend-wordmark-black.png", - "filename": "resend-wordmark-black.png", + "path": "https://example.com/invoice.pdf", + "filename": "invoice.pdf", }, ], }