Repository navigation
feat(inboxes): add support for the Inboxes API #298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
dielduarte
merged 20 commits into
main
from
feature/dev-2322-python-sdk-internal-squad-devtools
Oct 16, 2026
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
2647298
feat(inboxes): add the Inboxes resource
felipefreitag fe2ede9
test(inboxes): cover every Inboxes method
felipefreitag 7637497
docs(examples): add Inboxes examples
felipefreitag fc5d96b
feat(webhooks): add the inbox webhook event types
felipefreitag f7f70f6
chore: bump version to 2.50.0
felipefreitag 5a38b38
test(inboxes): assert the path of every async method
felipefreitag 765ee17
fix(inboxes): leave empty list filters out of the query
felipefreitag 90d0486
docs(examples): reply to the first email of the thread
felipefreitag a162fb7
fix(inboxes): remove label_id from the thread update params
felipefreitag 8207c6d
feat(inboxes): add attachments to drafts
felipefreitag fe14e17
docs(examples): attach a file to the example draft
felipefreitag 7e2d410
docs(inboxes): align the doc comments with the API audit
felipefreitag cc96a60
feat(inboxes): accept an idempotency key on the inbox sending methods
felipefreitag 81201d9
docs(examples): send the example draft with an idempotency key
felipefreitag 4af433a
feat(inboxes): accept custom headers on reply, forward and send draft
felipefreitag fc2f1f7
docs(examples): send the example draft with a custom header
felipefreitag 4000679
docs(inboxes): align the doc comments with the spec review
felipefreitag e20cf43
fix(inboxes): rename message_count to email_count
felipefreitag 877bdbb
docs(inboxes): describe the agent tone presets
felipefreitag 4803c17
feat(inboxes): match the stabilized Inboxes API
dielduarte File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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()) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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": "<p>Refund issued for order 1041.</p>", | ||
| } | ||
| 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']}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: This lookup raises
KeyErrorbefore the intended error wheneverRESEND_API_KEYis unset. Useos.environ.get()so the guard emits the missing-key message.Prompt for AI agents