Skip to content

feat(inboxes): add the inboxes API - #172

Merged
dielduarte merged 16 commits into
mainfrom
feature/dev-2323-go-sdk-internal-squad-devtools
Oct 16, 2026
Merged

dielduarte merged 16 commits into
mainfrom
feature/dev-2323-go-sdk-internal-squad-devtools

Conversation

@felipefreitag

@felipefreitag felipefreitag commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Summary

This PR adds the Inboxes API to the Go SDK. It covers the 26 routes under /inboxes and the 14 inbox.* webhook events.

  • client.Inboxes: Create, List, Get, Update, Remove. Get accepts an inbox ID or the inbox email address.
  • client.Inboxes.Agent: Get, Update.
  • client.Inboxes.Labels: List, Create, Update, Remove.
  • client.Inboxes.Threads: List, Search, Get, Update, Remove.
  • client.Inboxes.Threads.Emails: List, Get, Reply, Forward.
  • client.Inboxes.Drafts: List, Create, Get, Update, Remove, Send.
  • 14 EventInbox* webhook event constants.
  • examples/inboxes.go.
  • Version bump to 4.9.0, in its own commit.

Each method has a WithContext variant, the same as Suppressions. Drafts.Create, Drafts.Send, Threads.Emails.Reply and Threads.Emails.Forward also have a WithOptions variant that takes an IdempotencyKey, the same as Emails.SendWithOptions. The names follow the Node SDK preview. The shapes follow the spec PR resend/resend-openapi#125:

  • Create inbox, create label and create draft return object and id only.
  • Reply, forward and send draft return the sent email id only.
  • Label colors are #RRGGBB strings.
  • Reply and Forward accept Attachments. They use the Attachment type of Emails.Send.
  • Reply, Forward and Drafts.Send accept Headers, a map[string]string like SendEmailRequest.Headers. Drafts.Send takes an optional *SendInboxDraftRequest for it, and nil params send no body. Resend sets In-Reply-To, References and Reply-To, so the API returns a 422 for them.
  • Threads.Update sends read, folder, add_label_ids and remove_label_ids. Folder uses InboxMoveThreadFolder, which has no sent.
  • Drafts.Create and Drafts.Update accept Attachments of type InboxDraftAttachmentInput. Each one needs Content, Filename and ContentType, and it has no Path or ContentId. On update, the field replaces every attachment, and an empty slice removes them all. InboxDraft and InboxDraftListItem return attachments.

Some update fields can be cleared with null. For these, the request types have Clear* methods (UpdateInboxAgentRequest.ClearTone, UpdateInboxDraftRequest.ClearHtml). This follows SetUnsubscribed and SetOpenTracking. A nil slice is omitted, and an empty slice is sent as []. So EnabledActions: []resend.InboxAgentAction{} disables all agent actions.

Depends on

Verification

go build ./...
go vet ./...
go test ./...

All pass on Go 1.26.1. CI passes on Go 1.21 to 1.26.

I also ran an end-to-end script through the built SDK against https://api.resend.com, on the shared test inbox. 47 of 47 calls passed:

  • Inboxes.Create: 403 "You have reached the inbox limit of your plan" (expected, the team is at its limit).
  • Inboxes.List, Get (by ID and by email), Update and restore: OK.
  • Agent.Get, Update, restore with ClearTone and ClearInstructions: OK.
  • Labels.Create (lowercase hex, returned uppercase), Update, Update with an empty body, List, Remove: OK.
  • Threads.Search (returns matched_email_id and highlights), List with folders and read, Get: OK.
  • Threads.Update with read and folder archive, then back to inbox: OK.
  • Threads.Update with add_label_ids (the thread gets the label), then remove_label_ids (the label stays on the inbox): OK. The same ID in both arrays returns the expected validation error.
  • Threads.Emails.List, Get, Reply, Reply with a text attachment, Forward with a text attachment: OK. Each send returned the sent email id.
  • Drafts.Create with an attachment, then Get and List return it. Update with a new attachment replaces it, and Update with [] removes it: OK.
  • Drafts.Update with ClearText, Send of a draft with an attachment, Remove, and a reply draft create and remove: OK.
  • Threads.Remove on the seeded thread and on the thread of the sent draft: OK.

Follow-up runs on the same inbox:

  • A draft with 11 small files is accepted and stores all 11.
  • Drafts.Send of a reply draft with no subject returns the expected "subject is required" error.
  • Drafts.CreateWithOptions twice with the same key returns the same draft ID, and the inbox has one draft. The same key with a different body returns the idempotency error.
  • Drafts.SendWithOptions twice with the same key returns the same sent email ID.
  • Reply with X-Ticket-Id in Headers is sent to the Reply-To address. Drafts.Send with a custom header and with nil params both send. Reply with reply-to in Headers returns the expected 422.

Not in this PR

  • Sender authentication fields. They are not on the API main branch.
  • Typed payloads for the inbox webhook events. The SDK types webhook events as name constants only.
  • README changes. The README does not list methods.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

3 issues found across 17 files

Confidence score: 4/5

  • inboxes.go adds routes that need more than the send-only permissions on restricted Resend keys, and inboxes are limited to beta-enabled accounts. Confirm the required permissions and account availability.
  • UpdateInboxRequest.FromName does not clear the value as documented: a non-nil pointer to an empty string serializes as "from_name": "", not null. Align the serialization with the clear behavior promised in the PR description.
  • inbox_agent.go lets ClearInstructions() or ClearTone() take precedence over a later assigned value on the same request, silently dropping that value. Make the final setter call determine what gets sent.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. 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. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="inbox_agent.go">

<violation number="1" location="inbox_agent.go:155">
P3: In setNullableString the `cleared` flag always wins over a non-nil value, so a caller who calls ClearInstructions()/ClearTone() and then assigns a new value on the same request struct gets `instructions`/`tone` silently serialized as null; the assigned value is dropped. Prefer the value when it is non-nil so the most recent action wins. This also governs the identical Clear* pattern in UpdateInboxDraftRequest, so the fix applies there too.</violation>
</file>

<file name="inboxes.go">

<violation number="1" location="inboxes.go:88">
P2: `UpdateInboxRequest.FromName` claims a pointer to an empty string clears the value, but with `omitempty` a non-nil `&""` still serializes as `"from_name": ""`, not `null`. The PR description says clearable fields use `Clear*` methods to send `null`, and the sibling types added in this same PR (inbox_agent.go `ClearInstructions`/`ClearTone`, inbox_drafts.go `ClearSubject`/`ClearHtml`/`ClearText`) implement exactly that via `MarshalJSON` + `setNullableString`. If the API only clears on `null`, this will send `""` instead and silently fail to clear; at minimum it diverges from the Clear* convention established in this PR.</violation>

<violation number="2" location="inboxes.go:145">
P2: Custom agent: **API Key Permission Check SDK Methods**

These new inbox routes require more than the send-only permissions available to restricted Resend keys, and inboxes are limited to beta-enabled accounts. Confirm that the existing production keys and accounts have the required `full_access` and inbox beta access before release.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread inboxes.go
return CreateInboxResponse{}, errors.New("[ERROR]: EmailAddress is required")
}

req, err := s.client.NewRequest(ctx, http.MethodPost, "inboxes", params)

Copy link
Copy Markdown

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

These new inbox routes require more than the send-only permissions available to restricted Resend keys, and inboxes are limited to beta-enabled accounts. Confirm that the existing production keys and accounts have the required full_access and inbox beta access before release.

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 inboxes.go, line 145:

<comment>These new inbox routes require more than the send-only permissions available to restricted Resend keys, and inboxes are limited to beta-enabled accounts. Confirm that the existing production keys and accounts have the required `full_access` and inbox beta access before release.</comment>

<file context>
@@ -0,0 +1,263 @@
+		return CreateInboxResponse{}, errors.New("[ERROR]: EmailAddress is required")
+	}
+
+	req, err := s.client.NewRequest(ctx, http.MethodPost, "inboxes", params)
+	if err != nil {
+		return CreateInboxResponse{}, ErrFailedToCreateInboxesCreateRequest
</file context>

Comment thread inbox_thread_emails.go Resolved
Comment thread inboxes.go
Name string `json:"name,omitempty"`
// FromName is the name recipients see when mail is sent from this inbox. Max 78 characters. A
// pointer to an empty string removes it.
FromName *string `json:"from_name,omitempty"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: UpdateInboxRequest.FromName claims a pointer to an empty string clears the value, but with omitempty a non-nil &"" still serializes as "from_name": "", not null. The PR description says clearable fields use Clear* methods to send null, and the sibling types added in this same PR (inbox_agent.go ClearInstructions/ClearTone, inbox_drafts.go ClearSubject/ClearHtml/ClearText) implement exactly that via MarshalJSON + setNullableString. If the API only clears on null, this will send "" instead and silently fail to clear; at minimum it diverges from the Clear* convention established in this PR.

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 inboxes.go, line 88:

<comment>`UpdateInboxRequest.FromName` claims a pointer to an empty string clears the value, but with `omitempty` a non-nil `&""` still serializes as `"from_name": ""`, not `null`. The PR description says clearable fields use `Clear*` methods to send `null`, and the sibling types added in this same PR (inbox_agent.go `ClearInstructions`/`ClearTone`, inbox_drafts.go `ClearSubject`/`ClearHtml`/`ClearText`) implement exactly that via `MarshalJSON` + `setNullableString`. If the API only clears on `null`, this will send `""` instead and silently fail to clear; at minimum it diverges from the Clear* convention established in this PR.</comment>

<file context>
@@ -0,0 +1,263 @@
+	Name string `json:"name,omitempty"`
+	// FromName is the name recipients see when mail is sent from this inbox. Max 78 characters. A
+	// pointer to an empty string removes it.
+	FromName *string `json:"from_name,omitempty"`
+}
+
</file context>

Comment thread inbox_agent.go Outdated
Comment on lines +155 to +159
if cleared {
aux[key] = nil
} else if value != nil {
aux[key] = *value
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: In setNullableString the cleared flag always wins over a non-nil value, so a caller who calls ClearInstructions()/ClearTone() and then assigns a new value on the same request struct gets instructions/tone silently serialized as null; the assigned value is dropped. Prefer the value when it is non-nil so the most recent action wins. This also governs the identical Clear* pattern in UpdateInboxDraftRequest, so the fix applies there too.

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 inbox_agent.go, line 155:

<comment>In setNullableString the `cleared` flag always wins over a non-nil value, so a caller who calls ClearInstructions()/ClearTone() and then assigns a new value on the same request struct gets `instructions`/`tone` silently serialized as null; the assigned value is dropped. Prefer the value when it is non-nil so the most recent action wins. This also governs the identical Clear* pattern in UpdateInboxDraftRequest, so the fix applies there too.</comment>

<file context>
@@ -0,0 +1,160 @@
+}
+
+func setNullableString(aux map[string]any, key string, value *string, cleared bool) {
+	if cleared {
+		aux[key] = nil
+	} else if value != nil {
</file context>
Suggested change
if cleared {
aux[key] = nil
} else if value != nil {
aux[key] = *value
}
func setNullableString(aux map[string]any, key string, value *string, cleared bool) {
if value != nil {
aux[key] = *value
} else if cleared {
aux[key] = nil
}
}

Comment thread inbox_drafts.go Resolved
Comment thread inbox_labels.go Resolved

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 8 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 3 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 6 files (changes from recent commits).

Confidence score: 4/5

  • inboxes.go leaves the documented folder restriction unenforced, so callers can still set Folder to InboxFolderSent. Use a defined string type to enforce the restriction at compile time.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. 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. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="inboxes.go">

<violation number="1" location="inboxes.go:24">
P2: This alias does not prevent callers from setting `Folder: InboxFolderSent`, so the documented restriction is not enforced even at compile time. Make this a defined string type so the sent-folder constant cannot be assigned accidentally.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread inboxes.go

// InboxMoveThreadFolder is a folder a thread update can move a thread to. It has no `sent`,
// because `sent` is assigned automatically.
type InboxMoveThreadFolder = string

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This alias does not prevent callers from setting Folder: InboxFolderSent, so the documented restriction is not enforced even at compile time. Make this a defined string type so the sent-folder constant cannot be assigned accidentally.

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 inboxes.go, line 24:

<comment>This alias does not prevent callers from setting `Folder: InboxFolderSent`, so the documented restriction is not enforced even at compile time. Make this a defined string type so the sent-folder constant cannot be assigned accidentally.</comment>

<file context>
@@ -14,11 +14,22 @@ const (
 
+// InboxMoveThreadFolder is a folder a thread update can move a thread to. It has no `sent`,
+// because `sent` is assigned automatically.
+type InboxMoveThreadFolder = string
+
+const (
</file context>
Suggested change
type InboxMoveThreadFolder = string
type InboxMoveThreadFolder string

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 5 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 4 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 5 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 4 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 5 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 4 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 5 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 4 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 4 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 4 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 2 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 4 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 4 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Auto-approval blocked by 4 unresolved issues from previous reviews.

View guided diff | Re-trigger cubic

@dielduarte dielduarte left a comment

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.

some findings so far in code, but everything is working I tested locally:

  • Clearing to on a draft: UpdateInboxDraftRequest.To can only be left out or sent as []. The spec documents only null as the way to clear to; it allows [] just for cc and bcc. I tested it, and the API accepts to: [] and clears it, so it works, but the spec doesn't promise it. Fix with a ClearTo() like the existing ClearSubject(), or get [] documented in the spec.
  • Reply attachments: reply and forward reuse the emails Attachment type, which sends the file content as an array of numbers rather than a string. I sent a reply with an attachment to check, and it arrived correctly (text/plain, 20 bytes).
  • Nits:
    • draft create trims whitespace before its non-empty check, which is stricter than the spec
    • the helper setNullableString is in inbox_agent.go; the codebase keeps shared helpers in utils.go
    • withQuery is a very generic name
    • one test only re-checks string constants

Follow resend-openapi #137 and resend-node 7943103:

- Inboxes.Update and Inboxes.Remove take the Inbox ID or the inbox
  email address, like every other inbox route.
- Agent: EnabledActions is now Actions, the archive_thread and
  delete_thread actions are now archive and move_to_trash, and Tone is
  one of the four InboxAgentTone presets.
- Add Inboxes.Threads.Labels with Add, Set and Remove. Thread Update no
  longer takes AddLabelIds or RemoveLabelIds.
- Drafts.Update returns UpdateInboxDraftResponse with only Object and Id.
- Search results lose Highlights.
- Inbox adds UpdatedAt, and InboxListItem adds CreatedAt and UpdatedAt.
- Reply and forward no longer cap attachments at 10 files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 existing issue remains and 1 new issue found across 14 files (changes from recent commits).

Confidence score: 4/5

  • Inboxes.Threads.Labels requires a full_access API key; a sending_access key cannot use these operations. Confirm production keys have the required permission before enabling them.
  • The email-address route test does not cover Inboxes.Update or Inboxes.Remove, so regressions in either changed route could go unnoticed. Add both calls to the test.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. 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. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="inbox_thread_labels_test.go">

<violation number="1" location="inbox_thread_labels_test.go:110">
P2: This email-address route test omits `Inboxes.Update` and `Inboxes.Remove`, despite those routes being changed to accept email addresses. A regression in either top-level route would pass the test; add both calls and their expected paths.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

View guided diff | Re-trigger cubic

assert.EqualError(t, err, "[ERROR]: Label ID is required")
}

func TestInboxRoutesAcceptAnEmailAddress(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This email-address route test omits Inboxes.Update and Inboxes.Remove, despite those routes being changed to accept email addresses. A regression in either top-level route would pass the test; add both calls and their expected paths.

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 inbox_thread_labels_test.go, line 110:

<comment>This email-address route test omits `Inboxes.Update` and `Inboxes.Remove`, despite those routes being changed to accept email addresses. A regression in either top-level route would pass the test; add both calls and their expected paths.</comment>

<file context>
@@ -0,0 +1,144 @@
+	assert.EqualError(t, err, "[ERROR]: Label ID is required")
+}
+
+func TestInboxRoutesAcceptAnEmailAddress(t *testing.T) {
+	setup()
+	defer teardown()
</file context>

@dielduarte
dielduarte marked this pull request as ready for review October 16, 2026 19:57
@dielduarte
dielduarte merged commit 4706405 into main Oct 16, 2026
10 checks passed
@dielduarte
dielduarte deleted the feature/dev-2323-go-sdk-internal-squad-devtools branch October 16, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

⌂ 换网址