Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
892b3ed
feat(inboxes): add the inbox request and response models
felipefreitag Oct 12, 2026
10dfd92
feat(inboxes): add the inboxes service with agent, labels, threads, t…
felipefreitag Oct 12, 2026
1b3c11e
test(inboxes): cover every inbox operation
felipefreitag Oct 12, 2026
7a02ec7
feat(webhooks): add the inbox event types
felipefreitag Oct 12, 2026
38183d1
chore: bump to v4.29.0
felipefreitag Oct 12, 2026
de3c3b6
docs(inboxes): reword boolean getter descriptions
felipefreitag Oct 12, 2026
52f5bd0
fix(inboxes): encode the inbox email address in the get path
felipefreitag Oct 12, 2026
87aef1a
feat(inboxes): validate required fields in the request builders
felipefreitag Oct 12, 2026
be7d44d
feat(inboxes): reject empty label ID lists and a blank inbox name
felipefreitag Oct 12, 2026
5c91dcf
Merge remote-tracking branch 'origin/main' into feature/dev-2329-java…
felipefreitag Oct 13, 2026
152363e
feat(inboxes): remove label_id from the thread update
felipefreitag Oct 13, 2026
f1d0684
feat(inboxes): add attachments to the inbox drafts
felipefreitag Oct 13, 2026
6539d44
fix(inboxes): reject blank draft attachment fields
felipefreitag Oct 13, 2026
b472cb1
fix(inboxes): drop the draft file-count check and align behavior note…
felipefreitag Oct 13, 2026
91406c0
feat(inboxes): accept an idempotency key on draft create, draft send,…
felipefreitag Oct 13, 2026
a16b6bc
feat(inboxes): accept custom headers on reply, forward and draft send
felipefreitag Oct 13, 2026
c983091
docs(inboxes): align idempotency, header and attachment notes with th…
felipefreitag Oct 13, 2026
3abb768
fix(inboxes): rename message_count to email_count
felipefreitag Oct 14, 2026
7bc5d22
fix(inboxes): correct the reply recipient, trash and from_name notes
felipefreitag Oct 14, 2026
c7e990c
docs(inboxes): describe the agent tone presets
felipefreitag Oct 14, 2026
0cba761
Merge branch 'main' into feature/dev-2329-java-sdk-external-kewyn-via…
kewynakshlley Oct 16, 2026
b9abbcb
fix(inboxes): send and read the agent actions as `actions`
kewynakshlley Oct 16, 2026
fdb76f3
feat(inboxes): add the add, set and remove thread label endpoints
kewynakshlley Oct 16, 2026
0c3c21e
fix(inboxes): drop add_label_ids and remove_label_ids from the thread…
kewynakshlley Oct 16, 2026
6764c45
fix(inboxes): read updated_at on inboxes and created_at/updated_at on…
kewynakshlley Oct 16, 2026
1fe6014
fix(inboxes): model the update draft response as {object, id}
kewynakshlley Oct 16, 2026
30f35d6
docs(inboxes): correct the inbox ID and send draft notes
kewynakshlley Oct 16, 2026
0f4144b
fix(inboxes): encode every ID in the inbox paths as a single segment
kewynakshlley Oct 16, 2026
d0a207c
fix(inboxes): reject null or blank IDs in the thread label builders
kewynakshlley Oct 16, 2026
51e2e10
fix(inboxes): drop search highlights and match the tone and attachmen…
dielduarte Oct 16, 2026
13c0e3c
fix(inboxes): read the agent actions as InboxAgentAction
dielduarte Oct 20, 2026
4f6c35a
fix(inboxes): enforce label ID limits, dedupe agent actions and type …
kewynakshlley Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
GROUP=com.resend
VERSION_NAME=4.28.0
VERSION_NAME=4.29.0

POM_URL=https://github.com/resendlabs/resend-java
POM_SCM_URL=https://github.com/resendlabs/resend-java.git
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/resend/Resend.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.resend.services.contactproperties.ContactProperties;
import com.resend.services.domains.Domains;
import com.resend.services.emails.Emails;
import com.resend.services.inboxes.Inboxes;
import com.resend.services.segments.Segments;
import com.resend.services.suppressions.Suppressions;
import com.resend.services.webhooks.Webhooks;
Expand Down Expand Up @@ -248,6 +249,15 @@ public Usage usage() {
return new Usage(apiKey, httpClient);
}

/**
* Returns an Inboxes object that can be used to interact with the Inboxes service.
*
* @return An Inboxes object.
*/
public Inboxes inboxes() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: API Key Permission Check SDK Methods

Please confirm that production API keys used with inboxes() have full_access; Resend restricts sending_access keys to sending emails, while this service exposes inbox resource creation, updates, and deletion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/main/java/com/resend/Resend.java, line 257:

<comment>Please confirm that production API keys used with `inboxes()` have `full_access`; Resend restricts `sending_access` keys to sending emails, while this service exposes inbox resource creation, updates, and deletion.</comment>

<file context>
@@ -248,6 +249,15 @@ public Usage usage() {
+     *
+     * @return An Inboxes object.
+     */
+    public Inboxes inboxes() {
+        return new Inboxes(apiKey, httpClient);
+    }
</file context>

return new Inboxes(apiKey, httpClient);
}

/**
* Builds a {@link Resend} instance with a custom configuration.
*
Expand Down
18 changes: 17 additions & 1 deletion src/main/java/com/resend/core/helper/URLHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import java.util.stream.Collectors;

/**
* URL Helper for handling query params.
* URL Helper for handling path segments and query params.
*/
public class URLHelper {

Expand Down Expand Up @@ -75,6 +75,22 @@ public static String parse(ListParams params, Map<String, String> extras) {
return "?" + query;
}

/**
* Encodes a value for use as a single path segment, such as an ID or an email address.
* Characters that would change the path, like {@code /}, {@code ?}, {@code #} and {@code %}, are percent-encoded.
*
* @param value The path segment value.
* @return The encoded path segment.
* @throws IllegalArgumentException If the value is null, empty, {@code .} or {@code ..}, which cannot be sent as a
* single segment.
*/
public static String encodePathSegment(String value) {
if (value == null || value.isEmpty() || value.equals(".") || value.equals("..")) {
throw new IllegalArgumentException("A path segment must not be null, empty, \".\" or \"..\"");
}
return encode(value).replace("+", "%20");
}

private static String encode(String value) {
try {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/resend/core/net/HttpMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ public enum HttpMethod {
* HTTP PATCH method.
*/
PATCH,

/**
* HTTP PUT method.
*/
PUT,
}
62 changes: 62 additions & 0 deletions src/main/java/com/resend/services/inboxes/InboxAgent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.resend.services.inboxes;

import com.resend.core.exception.ResendException;
import com.resend.core.helper.URLHelper;
Comment thread
kewynakshlley marked this conversation as resolved.
import com.resend.core.net.HttpMethod;
import com.resend.core.net.IHttpClient;
import com.resend.core.service.BaseService;
import com.resend.services.inboxes.model.GetInboxAgentResponseSuccess;
import com.resend.services.inboxes.model.UpdateInboxAgentOptions;
import com.resend.services.inboxes.model.UpdateInboxAgentResponseSuccess;
import okhttp3.MediaType;

/**
* Represents the Resend Inbox Agent module.
*/
public class InboxAgent extends BaseService {

/**
* Constructs an instance of the {@code InboxAgent} class.
*
* @param apiKey The apiKey used for authentication.
*/
public InboxAgent(final String apiKey) {
super(apiKey);
}

/**
* Constructs an instance of the {@code InboxAgent} class that sends requests through the given HTTP client.
*
* @param apiKey The apiKey used for authentication.
* @param httpClient The HTTP client to use.
*/
public InboxAgent(final String apiKey, final IHttpClient<String> httpClient) {
super(apiKey, httpClient);
}

/**
* Retrieves the agent settings of an inbox. An inbox without a configured agent returns empty settings:
* {@code instructions} and {@code tone} are null and {@code actions} is an empty list.
*
* @param inboxId The inbox ID or the inbox email address.
* @return The agent settings of the inbox.
* @throws ResendException If an error occurs while retrieving the agent settings.
*/
public GetInboxAgentResponseSuccess get(String inboxId) throws ResendException {
return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/agent", HttpMethod.GET, null, MediaType.get("application/json"), GetInboxAgentResponseSuccess.class);
}

/**
* Updates the agent settings of an inbox. The agent is created when the inbox has none.
* Omitted fields keep their current value.
*
* @param inboxId The inbox ID or the inbox email address.
* @param updateInboxAgentOptions The settings to update.
* @return The ID of the inbox agent.
* @throws ResendException If an error occurs while updating the agent settings.
*/
public UpdateInboxAgentResponseSuccess update(String inboxId, UpdateInboxAgentOptions updateInboxAgentOptions) throws ResendException {
String payload = super.resendMapper.writeValue(updateInboxAgentOptions);
return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/agent", HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxAgentResponseSuccess.class);
}
}
232 changes: 232 additions & 0 deletions src/main/java/com/resend/services/inboxes/InboxDrafts.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package com.resend.services.inboxes;

import com.resend.core.exception.ResendException;
import com.resend.core.helper.URLHelper;
import com.resend.core.net.HttpMethod;
import com.resend.core.net.IHttpClient;
import com.resend.core.net.ListParams;
import com.resend.core.net.RequestOptions;
import com.resend.core.service.BaseService;
import com.resend.services.inboxes.model.CreateInboxDraftOptions;
import com.resend.services.inboxes.model.CreateInboxDraftResponseSuccess;
import com.resend.services.inboxes.model.GetInboxDraftResponseSuccess;
import com.resend.services.inboxes.model.ListInboxDraftsResponseSuccess;
import com.resend.services.inboxes.model.RemoveInboxDraftResponseSuccess;
import com.resend.services.inboxes.model.SendInboxDraftOptions;
import com.resend.services.inboxes.model.SendInboxDraftResponseSuccess;
import com.resend.services.inboxes.model.UpdateInboxDraftOptions;
import com.resend.services.inboxes.model.UpdateInboxDraftResponseSuccess;
import okhttp3.MediaType;

/**
* Represents the Resend Inbox Drafts module.
*/
public class InboxDrafts extends BaseService {

/**
* Constructs an instance of the {@code InboxDrafts} class.
*
* @param apiKey The apiKey used for authentication.
*/
public InboxDrafts(final String apiKey) {
super(apiKey);
}

/**
* Constructs an instance of the {@code InboxDrafts} class that sends requests through the given HTTP client.
*
* @param apiKey The apiKey used for authentication.
* @param httpClient The HTTP client to use.
*/
public InboxDrafts(final String apiKey, final IHttpClient<String> httpClient) {
super(apiKey, httpClient);
}

/**
* Retrieves the unsent drafts on an inbox, most recently updated first.
*
* @param inboxId The inbox ID or the inbox email address.
* @return A ListInboxDraftsResponseSuccess containing the drafts.
* @throws ResendException If an error occurs while retrieving the drafts.
*/
public ListInboxDraftsResponseSuccess list(String inboxId) throws ResendException {
return list(inboxId, null);
}

/**
* Retrieves a page of the unsent drafts on an inbox, most recently updated first.
* The {@code after} and {@code before} cursors take a draft ID from the current page.
* A cursor that is not an unsent draft on this inbox returns a 404.
*
* @param inboxId The inbox ID or the inbox email address.
* @param params The params used to paginate the list.
* @return A ListInboxDraftsResponseSuccess containing the drafts.
* @throws ResendException If an error occurs while retrieving the drafts.
*/
public ListInboxDraftsResponseSuccess list(String inboxId, ListParams params) throws ResendException {
return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts" + URLHelper.parse(params), HttpMethod.GET, null, MediaType.get("application/json"), ListInboxDraftsResponseSuccess.class);
}

/**
* Creates an unsent draft on an inbox. Omit {@code thread_id} and {@code reply_to_email_id} for a draft that
* starts a new thread. Pass both together for a reply draft in an existing thread. 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 {@code attachments}, which replaces the
* attachments of the existing draft when sent. A reply draft created without {@code subject} stores an empty
* subject and gets no {@code Re:} prefix, so set one before you send it.
*
* @param inboxId The inbox ID or the inbox email address.
* @param createInboxDraftOptions The draft content.
* @return The ID of the draft.
* @throws ResendException If an error occurs while creating the draft.
*/
public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOptions createInboxDraftOptions) throws ResendException {
String payload = super.resendMapper.writeValue(createInboxDraftOptions);
return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts", HttpMethod.POST, payload, MediaType.get("application/json"), CreateInboxDraftResponseSuccess.class);
}

/**
* Creates an unsent draft on an inbox. Omit {@code thread_id} and {@code reply_to_email_id} for a draft that
* starts a new thread. Pass both together for a reply draft in an existing thread. 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 {@code attachments}, which replaces the
* attachments of the existing draft when sent. A reply draft created without {@code subject} stores an empty
* subject and gets no {@code Re:} prefix, so set one before you send it.
*
* <p>Set an idempotency key in {@code requestOptions} so the draft is created only once. A retry with the same key
* within 24 hours returns the first successful response, with the {@code idempotent-replayed: true} header. Reusing the
* key with a different body, or while the first request is still running, returns a 409. A failed request is
* not stored, so you can retry it with the same key.</p>
*
* @param inboxId The inbox ID or the inbox email address.
* @param createInboxDraftOptions The draft content.
* @param requestOptions The options with additional headers.
* @return The ID of the draft.
* @throws ResendException If an error occurs while creating the draft.
*/
public CreateInboxDraftResponseSuccess create(String inboxId, CreateInboxDraftOptions createInboxDraftOptions, RequestOptions requestOptions) throws ResendException {
String payload = super.resendMapper.writeValue(createInboxDraftOptions);
return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts", HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, CreateInboxDraftResponseSuccess.class);
}

/**
* Retrieves a single draft.
*
* @param inboxId The inbox ID or the inbox email address.
* @param draftId The draft ID.
* @return The draft.
* @throws ResendException If an error occurs while retrieving the draft.
*/
public GetInboxDraftResponseSuccess get(String inboxId, String draftId) throws ResendException {
return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId), HttpMethod.GET, null, MediaType.get("application/json"), GetInboxDraftResponseSuccess.class);
}

/**
* Updates the content of an unsent draft. Omitted fields are left unchanged. A request that sets
* {@code text} or {@code html} replaces the whole body.
* Sending {@code attachments} replaces every attachment on the draft, and an empty list removes them all. The body part you omit from that request becomes
* {@code null}. A draft that was already sent, or that changed during the request, returns a 409.
*
* @param inboxId The inbox ID or the inbox email address.
* @param draftId The draft ID.
* @param updateInboxDraftOptions The fields to update.
* @return The ID of the updated draft. Use {@link #get(String, String)} to read its content.
* @throws ResendException If an error occurs while updating the draft.
*/
public UpdateInboxDraftResponseSuccess update(String inboxId, String draftId, UpdateInboxDraftOptions updateInboxDraftOptions) throws ResendException {
String payload = super.resendMapper.writeValue(updateInboxDraftOptions);
return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId), HttpMethod.PATCH, payload, MediaType.get("application/json"), UpdateInboxDraftResponseSuccess.class);
}

/**
* Discards an unsent draft. A draft that was already sent, or that changed during the request, returns a 409.
*
* @param inboxId The inbox ID or the inbox email address.
* @param draftId The draft ID.
* @return The RemoveInboxDraftResponseSuccess with the details of the removed draft.
* @throws ResendException If an error occurs while removing the draft.
*/
public RemoveInboxDraftResponseSuccess remove(String inboxId, String draftId) throws ResendException {
return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId), HttpMethod.DELETE, null, MediaType.get("application/json"), RemoveInboxDraftResponseSuccess.class);
}

/**
* Sends 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 {@code to} recipient and {@code html} or {@code text}, or the
* request returns a 422. A draft that starts a new thread also needs a non-empty {@code subject}. A reply draft
* can be sent with an empty subject.
* A draft that was already sent returns a 409.
*
* @param inboxId The inbox ID or the inbox email address.
* @param draftId The draft ID.
* @return The ID of the sent email.
* @throws ResendException If an error occurs while sending the draft.
*/
public SendInboxDraftResponseSuccess send(String inboxId, String draftId) throws ResendException {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom agent: API Key Permission Check SDK Methods

Please confirm that production API keys have the permissions required by these new inbox-draft operations before deployment. Resend keys restricted to sending_access cannot perform draft list/get/update/remove operations, while the PR only verifies a shared test key.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/main/java/com/resend/services/inboxes/InboxDrafts.java, line 134:

<comment>Please confirm that production API keys have the permissions required by these new inbox-draft operations before deployment. Resend keys restricted to `sending_access` cannot perform draft list/get/update/remove operations, while the PR only verifies a shared test key.</comment>

<file context>
@@ -0,0 +1,137 @@
+     * @return The ID of the sent email.
+     * @throws ResendException If an error occurs while sending the draft.
+     */
+    public SendInboxDraftResponseSuccess send(String inboxId, String draftId) throws ResendException {
+        return execute("/inboxes/" + inboxId + "/drafts/" + draftId + "/send", HttpMethod.POST, "", MediaType.get("application/json"), SendInboxDraftResponseSuccess.class);
+    }
</file context>

return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId) + "/send", HttpMethod.POST, "", MediaType.get("application/json"), SendInboxDraftResponseSuccess.class);
}

/**
* Sends 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 {@code to} recipient and {@code html} or {@code text}, or the
* request returns a 422. A draft that starts a new thread also needs a non-empty {@code subject}. A reply draft
* can be sent with an empty subject.
* A draft that was already sent returns a 409.
*
* <p>Set an idempotency key in {@code requestOptions} so the draft is sent only once. A retry with the same key
* within 24 hours returns the first successful response, with the {@code idempotent-replayed: true} header. Reusing the
* key with a different body, or while the first request is still running, returns a 409. A failed request is
* not stored, so you can retry it with the same key.</p>
*
* @param inboxId The inbox ID or the inbox email address.
* @param draftId The draft ID.
* @param requestOptions The options with additional headers.
* @return The ID of the sent email.
* @throws ResendException If an error occurs while sending the draft.
*/
public SendInboxDraftResponseSuccess send(String inboxId, String draftId, RequestOptions requestOptions) throws ResendException {
return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId) + "/send", HttpMethod.POST, "", MediaType.get("application/json"), requestOptions, SendInboxDraftResponseSuccess.class);
}

/**
* Sends a draft from the inbox address with custom headers. A reply draft is sent in its thread. A draft
* without a thread starts a new thread. The draft must have at least one {@code to} recipient and {@code html}
* or {@code text}, or the request returns a 422. A draft that starts a new thread also needs a non-empty
* {@code subject}. A reply draft can be sent with an empty subject.
* A draft that was already sent returns a 409.
*
* @param inboxId The inbox ID or the inbox email address.
* @param draftId The draft ID.
* @param sendInboxDraftOptions The custom headers to add to the email.
* @return The ID of the sent email.
* @throws ResendException If an error occurs while sending the draft.
*/
public SendInboxDraftResponseSuccess send(String inboxId, String draftId, SendInboxDraftOptions sendInboxDraftOptions) throws ResendException {
String payload = sendInboxDraftOptions == null ? "" : super.resendMapper.writeValue(sendInboxDraftOptions);
return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId) + "/send", HttpMethod.POST, payload, MediaType.get("application/json"), SendInboxDraftResponseSuccess.class);
}

/**
* Sends a draft from the inbox address with custom headers. A reply draft is sent in its thread. A draft
* without a thread starts a new thread. The draft must have at least one {@code to} recipient and {@code html}
* or {@code text}, or the request returns a 422. A draft that starts a new thread also needs a non-empty
* {@code subject}. A reply draft can be sent with an empty subject.
* A draft that was already sent returns a 409.
*
* <p>Set an idempotency key in {@code requestOptions} so the draft is sent only once. A retry with the same key
* within 24 hours returns the first successful response, with the {@code idempotent-replayed: true} header. Reusing the
* key with a different body, or while the first request is still running, returns a 409. A failed request is
* not stored, so you can retry it with the same key.</p>
*
* @param inboxId The inbox ID or the inbox email address.
* @param draftId The draft ID.
* @param sendInboxDraftOptions The custom headers to add to the email.
* @param requestOptions The options with additional headers.
* @return The ID of the sent email.
* @throws ResendException If an error occurs while sending the draft.
*/
public SendInboxDraftResponseSuccess send(String inboxId, String draftId, SendInboxDraftOptions sendInboxDraftOptions, RequestOptions requestOptions) throws ResendException {
String payload = sendInboxDraftOptions == null ? "" : super.resendMapper.writeValue(sendInboxDraftOptions);
return execute("/inboxes/" + URLHelper.encodePathSegment(inboxId) + "/drafts/" + URLHelper.encodePathSegment(draftId) + "/send", HttpMethod.POST, payload, MediaType.get("application/json"), requestOptions, SendInboxDraftResponseSuccess.class);
}
}
Loading
Loading