feat(inboxes): add support for the Inboxes API

Merged
FE
1d ago
+6168-1

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. get takes 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 #RRGGBB strings.
  • Reply and forward accept attachments, with the same type as Emails.send.
  • Thread update accepts read, folder, add_label_ids and remove_label_ids (monorepo #12396). There is no label_id.
  • Draft create and update accept attachments (monorepo #12481). Each one needs content, filename and content_type, in Inboxes.Drafts.AttachmentParams. On update, the list replaces every attachment, and [] removes them all. InboxDraft, InboxDraftListItem and the webhook draft object return attachments as InboxDraftAttachment.

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

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.

CallResult
Inboxes.list, get by ID, get by email addressOK
Inboxes.create403 validation_error, plan inbox limit. This is the expected result, because the body passed validation.
Inboxes.update name, then restoreOK
Agent.get, update, then restoreOK
Labels.create, update, list, removeOK. Color #e93d82 came back as #E93D82.
Seed email to the inbox, then Threads.searchFound, with matched_email_id and highlights
Threads.list with folders, read, labelsOK
Threads.search with from, has_attachment, start_dateOK
Threads.get, update read and folderOK
Threads.update add_label_idsOK. The thread labels holds the label, and the labels filter returns the thread.
Threads.update with one ID in both arrays422 validation_error, as expected
Threads.update remove_label_idsOK. The thread labels is empty, and the label still exists.
Threads.Emails.list, getOK
Threads.Emails.reply, with and without an attachmentOK, returns { id }
Threads.Emails.forward with an attachmentOK, returns { id }
Drafts.create with one attachment, then get and listOK. 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 attachmentOK, returns { id }
Drafts.create with an attachment, then removeOK
Threads.get_async, Labels.list_asyncOK
Threads.remove for the seeded thread and the sent draft threadOK
Drafts.create twice with the same keyOK. Same draft ID, idempotent-replayed: true, one draft in the list.
Drafts.create with the same key and a different body409 invalid_idempotent_request, as expected
Drafts.send twice with the same keyOK. Same email ID, idempotent-replayed: true.
Threads.Emails.reply with X-Ticket-IdOK, sent to delivered@resend.dev through the Reply-To
Threads.Emails.reply with a reply-to header422 validation_error, as expected
Drafts.send with X-Ticket-IdOK

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.