feat(inboxes): add support for the Inboxes API
Summary
Add the Inboxes API to the Python SDK. The names follow the Node SDK preview, in the Python style of the other resources.
resend.Inboxes:create,list,get,update,remove.gettakes an inbox ID or an inbox email address.resend.Inboxes.Agent:get,update.resend.Inboxes.Labels:list,create,update,remove.resend.Inboxes.Threads:list,search,get,update,remove.resend.Inboxes.Threads.Emails:list,get,reply,forward.resend.Inboxes.Drafts:list,create,get,update,remove,send.
Every method also has an _async variant. That makes 26 operations, one for each route under /inboxes in the spec.
Methods with a request body take the path IDs in the params dict, as in Webhooks.update. The resource's own ID uses the key id, and the parent IDs use inbox_id, thread_id and email_id. The SDK keeps the path IDs out of the request body. Methods without a request body take the path IDs as positional arguments, as in Webhooks.get_event(webhook_id, event_id) and Domains.remove(domain_id). This includes remove and Drafts.send.
List and search queries send list values comma-separated and leave out empty lists. They send booleans as true or false. The API does not accept repeated parameters or Python's True.
The shapes follow the spec at GA:
- Create inbox, create label and create draft return
{ object, id }. - Reply, forward and send draft return
{ id }, the ID of the sent email. - Label colors are
#RRGGBBstrings. - Reply and forward accept
attachments, with the same type asEmails.send. - Thread update accepts
read,folder,add_label_idsandremove_label_ids(monorepo #12396). There is nolabel_id. - Draft create and update accept
attachments(monorepo #12481). Each one needscontent,filenameandcontent_type, inInboxes.Drafts.AttachmentParams. On update, the list replaces every attachment, and[]removes them all.InboxDraft,InboxDraftListItemand the webhook draft object returnattachmentsasInboxDraftAttachment.
Threads.Emails.reply, Threads.Emails.forward, Drafts.create and Drafts.send take an optional options dict with idempotency_key (monorepo #11768). This is the same shape as Emails.send. The SDK sends it as the Idempotency-Key header.
Threads.Emails.reply and forward take headers in their params, as Emails.send does (monorepo #12509). Drafts.send takes an optional params dict with headers, between the path IDs and the options. Resend sets In-Reply-To, References and Reply-To, so the API rejects them with a 422.
The PR also adds the 14 inbox.* events to WebhookEvent, with a typed payload for each in WebhookEventPayload.
The doc comments use the behavior notes from the spec descriptions.
Depends on
- The spec: resend/resend-openapi#125.
- Monorepo #12396, #12481, #11768 and #12509 are merged and live.
- No other SDK dependency.
- The tone presets text merges after resend-monorepo #12580, #12634 and #12635 are deployed.
Verification
CI commands, run locally on Python 3.9 with the CI flags:
flake8 --max-line-length=130 ./resend/
mypy --install-types --non-interactive --ignore-missing-imports resend/ examples/ tests/
pytest --cov=resend --cov-report=xml --doctest-modules tests
flake8 is clean. pytest passes 822 tests. mypy shows only 2 unused-ignore errors in tests/exceptions_test.py, which this PR does not touch. The newer local mypy causes them. CI runs Python 3.8 to 3.14.
End to end against https://api.resend.com, through the SDK methods, on the shared test inbox. Each write was self-contained and was cleaned up. The last full run had 57 passed calls and checks, and 0 failures.
| Call | Result |
|---|---|
Inboxes.list, get by ID, get by email address | OK |
Inboxes.create | 403 validation_error, plan inbox limit. This is the expected result, because the body passed validation. |
Inboxes.update name, then restore | OK |
Agent.get, update, then restore | OK |
Labels.create, update, list, remove | OK. Color #e93d82 came back as #E93D82. |
Seed email to the inbox, then Threads.search | Found, with matched_email_id and highlights |
Threads.list with folders, read, labels | OK |
Threads.search with from, has_attachment, start_date | OK |
Threads.get, update read and folder | OK |
Threads.update add_label_ids | OK. The thread labels holds the label, and the labels filter returns the thread. |
Threads.update with one ID in both arrays | 422 validation_error, as expected |
Threads.update remove_label_ids | OK. The thread labels is empty, and the label still exists. |
Threads.Emails.list, get | OK |
Threads.Emails.reply, with and without an attachment | OK, returns { id } |
Threads.Emails.forward with an attachment | OK, returns { id } |
Drafts.create with one attachment, then get and list | OK. Both return the attachment with filename and content_type. |
Drafts.update with a second attachment set, then with [] | OK. The set is replaced, then empty. |
Drafts.send for a draft with an attachment | OK, returns { id } |
Drafts.create with an attachment, then remove | OK |
Threads.get_async, Labels.list_async | OK |
Threads.remove for the seeded thread and the sent draft thread | OK |
Drafts.create twice with the same key | OK. Same draft ID, idempotent-replayed: true, one draft in the list. |
Drafts.create with the same key and a different body | 409 invalid_idempotent_request, as expected |
Drafts.send twice with the same key | OK. Same email ID, idempotent-replayed: true. |
Threads.Emails.reply with X-Ticket-Id | OK, sent to delivered@resend.dev through the Reply-To |
Threads.Emails.reply with a reply-to header | 422 validation_error, as expected |
Drafts.send with X-Ticket-Id | OK |
Webhooks cannot deliver in this setup. A script checked that the 14 inbox.* events of the spec are in WebhookEvent and in WebhookEventPayload.
Not in this PR
- Sender authentication fields. They are not on the API main branch.
- A docs page. The docs PR adds the Python tabs.