From 589e0c5ecde795edac25b6bb8330607b47157292 Mon Sep 17 00:00:00 2001 From: Roshan Jonnalagadda Date: Tue, 7 Jul 2026 17:43:17 +1000 Subject: [PATCH 1/6] Document agent-readable mailbox attachments --- changelog.mdx | 13 +++++++++++++ guides/attachments.mdx | 9 +++++---- mailbox-api/introduction.mdx | 10 ++++++++-- openapi-app.json | 2 +- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/changelog.mdx b/changelog.mdx index 7e06be6..9e0227c 100644 --- a/changelog.mdx +++ b/changelog.mdx @@ -4,6 +4,19 @@ description: "New features, improvements, and fixes across the Sendmux platform. rss: true --- + + ## Mailbox API + +Mailbox attachment download links are now shorter and use stable attachment refs, making them easier to pass through agent tools and command-line workflows. + +Existing short-lived download links keep the same expiry and safety model, while fresh message and attachment reads return the new compact link shape. + + ## Developer tools + +MCP clients can now read text attachments directly through the mailbox attachment read tool, so agents no longer need a separate web fetch step for common files like Markdown recipes. + + + ## Domain verification diff --git a/guides/attachments.mdx b/guides/attachments.mdx index 5849b1b..5df69cc 100644 --- a/guides/attachments.mdx +++ b/guides/attachments.mdx @@ -74,7 +74,7 @@ Mailbox messages include attachment metadata when attachments are present: The `download_url` is short-lived and grants access only to that attachment. Fetch it promptly with any HTTP client; no `Authorization` header is needed for the URL itself. -If the URL expires, re-read the message with `GET /mailbox/messages/{message_id}`, list or search messages again, or call the MCP `mailbox_get_attachment` tool to get a fresh `download_url`. The authenticated attachment download endpoint continues to work for clients that send bearer credentials. +If the URL expires, re-read the message with `GET /mailbox/messages/{message_id}`, list or search messages again, or call the MCP `mailbox_get_attachment` tool to get a fresh `download_url`. MCP clients that need attachment text should call `mailbox_read_attachment` first; it returns inline text for text-like attachments and a link for binary or oversized files. The authenticated attachment download endpoint continues to work for clients that send bearer credentials. Realtime received-message events can include the same attachment metadata when it is available. Handle older events where `attachments` is omitted, then re-read message metadata before downloading. @@ -84,10 +84,11 @@ Range requests are still supported on attachment downloads, including short-live 1. Wait for or list messages. 2. Read the message metadata and find the attachment. -3. Fetch `download_url` promptly. -4. If the URL expired, re-read the message or attachment metadata and fetch the new URL. +3. For MCP, call `mailbox_read_attachment` with the message ID and attachment ID. +4. Use returned inline text directly, or fetch the returned link promptly for binary or oversized files. +5. If the URL expired, re-read the message or attachment metadata and fetch the new URL. -MCP clients should use the attachment metadata tool rather than trying to return raw binary content through the tool call. +Do not construct attachment URLs manually. ## Related guides diff --git a/mailbox-api/introduction.mdx b/mailbox-api/introduction.mdx index 92ffc5e..88674f6 100644 --- a/mailbox-api/introduction.mdx +++ b/mailbox-api/introduction.mdx @@ -562,7 +562,7 @@ curl https://app.sendmux.ai/api/v1/mailbox/messages:batch-get \ "size_bytes": 12345, "disposition": "attachment", "content_id": null, - "download_url": "/api/v1/mailbox/messages/msg_123/attachments/att_123" + "download_url": "/api/v1/mailbox/messages/msg_123/attachment-downloads/attref_abc123?download_token=..." } ] } @@ -731,7 +731,7 @@ curl "https://app.sendmux.ai/api/v1/mailbox/messages/msg_123/content?strip_signa "size_bytes": 12345, "disposition": "attachment", "content_id": null, - "download_url": "/api/v1/mailbox/messages/msg_123/attachments/att_123" + "download_url": "/api/v1/mailbox/messages/msg_123/attachment-downloads/attref_abc123?download_token=..." } ] }, @@ -814,6 +814,12 @@ curl -X POST https://app.sendmux.ai/api/v1/mailbox/messages/send \ Small inline base64 attachments are still supported on `messages/send`, but the upload endpoint avoids large JSON request bodies. +Message metadata can include a short-lived `download_url` for clients that need +a no-header download link. Fetch it promptly without an `Authorization` header. +If it expires, re-read message or attachment metadata to receive a fresh URL. +MCP clients should prefer `mailbox_read_attachment` for text-like inbound +attachments and use the returned link only for binary or oversized files. + Attachment downloads support byte ranges: ```bash diff --git a/openapi-app.json b/openapi-app.json index a3ff212..19459fc 100644 --- a/openapi-app.json +++ b/openapi-app.json @@ -1173,7 +1173,7 @@ }, "download_url": { "description": "Short-lived URL for this exact attachment. Fetch it promptly; if it expires, call the message or attachment metadata endpoint again to receive a fresh URL.", - "example": "https://app.sendmux.ai/api/v1/mailbox/messages/msg_123/attachments/blob_123?download_token=...", + "example": "https://app.sendmux.ai/api/v1/mailbox/messages/msg_123/attachment-downloads/attref_abc123?download_token=...", "type": "string" }, "filename": { From bcfc757baf3f7afd58c399d2a5af2d128f3a6da0 Mon Sep 17 00:00:00 2001 From: Roshan Jonnalagadda Date: Tue, 7 Jul 2026 17:45:50 +1000 Subject: [PATCH 2/6] Clarify MCP attachment read fallback --- changelog.mdx | 2 +- guides/attachments.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.mdx b/changelog.mdx index 9e0227c..0e6a583 100644 --- a/changelog.mdx +++ b/changelog.mdx @@ -13,7 +13,7 @@ Existing short-lived download links keep the same expiry and safety model, while ## Developer tools -MCP clients can now read text attachments directly through the mailbox attachment read tool, so agents no longer need a separate web fetch step for common files like Markdown recipes. +MCP clients can now read text attachments directly through `mailbox_read_attachment`, so agents no longer need a separate web fetch step for common files like Markdown recipes. diff --git a/guides/attachments.mdx b/guides/attachments.mdx index 5df69cc..c5be21b 100644 --- a/guides/attachments.mdx +++ b/guides/attachments.mdx @@ -74,7 +74,7 @@ Mailbox messages include attachment metadata when attachments are present: The `download_url` is short-lived and grants access only to that attachment. Fetch it promptly with any HTTP client; no `Authorization` header is needed for the URL itself. -If the URL expires, re-read the message with `GET /mailbox/messages/{message_id}`, list or search messages again, or call the MCP `mailbox_get_attachment` tool to get a fresh `download_url`. MCP clients that need attachment text should call `mailbox_read_attachment` first; it returns inline text for text-like attachments and a link for binary or oversized files. The authenticated attachment download endpoint continues to work for clients that send bearer credentials. +If the URL expires, re-read the message with `GET /mailbox/messages/{message_id}`, list or search messages again, or call the MCP `mailbox_get_attachment` tool to get a fresh `download_url`. MCP clients that need attachment text should call `mailbox_read_attachment` first; it returns inline text for text-like attachments and returns a resource link for binary or oversized files. The authenticated attachment download endpoint continues to work for clients that send bearer credentials. Realtime received-message events can include the same attachment metadata when it is available. Handle older events where `attachments` is omitted, then re-read message metadata before downloading. From e293f384636994df6c788a62019f1008099b751e Mon Sep 17 00:00:00 2001 From: Roshan Jonnalagadda Date: Tue, 7 Jul 2026 22:00:07 +1000 Subject: [PATCH 3/6] docs: document sending attachment uploads --- changelog.mdx | 8 +- guides/attachments.mdx | 77 +- guides/sending-via-http.mdx | 4 +- openapi-sending.json | 742 +++++++++++++++++- .../sendmux-sending.postman_collection.json | 690 ++++++++++++++++ sending-api/errors.mdx | 3 +- sending-api/introduction.mdx | 7 +- 7 files changed, 1505 insertions(+), 26 deletions(-) diff --git a/changelog.mdx b/changelog.mdx index 0e6a583..d534376 100644 --- a/changelog.mdx +++ b/changelog.mdx @@ -4,7 +4,13 @@ description: "New features, improvements, and fixes across the Sendmux platform. rss: true --- - + + ## Sending API + +Agents can now upload Sending API attachment bytes first and send by `attachment_id`, keeping normal files out of prompts and JSON base64 payloads. + +Hosted agents can mint a short-lived upload URL, upload with the returned headers and no Sendmux API key in the upload command, then attach the returned `attachment_id` to single or batch sends. + ## Mailbox API Mailbox attachment download links are now shorter and use stable attachment refs, making them easier to pass through agent tools and command-line workflows. diff --git a/guides/attachments.mdx b/guides/attachments.mdx index c5be21b..f65345c 100644 --- a/guides/attachments.mdx +++ b/guides/attachments.mdx @@ -4,25 +4,83 @@ description: "Send, upload, and download attachments with Sendmux." keywords: ["attachments", "file uploads", "file downloads", "base64", "MIME types", "size limits"] --- -HTTP send requests can include up to 10 attachments. Each attachment is sent as base64 content inside the JSON body. +For the Sending API, upload file bytes first and send by `attachment_id`. Inline base64 attachments remain supported for small generated content and older integrations, but uploaded refs are the preferred path for normal files. ## Limits | Limit | Value | | --- | --- | | Attachments per email | 10 | -| Request body | 25 MB | +| Sending API binary upload | 18 MiB | +| Final sent message | 25 MB | | Mailbox upload attachment | 7,500,000 bytes | -Base64 content counts towards the request body size, so keep files below the full request limit. +Base64 content counts towards the request body size and increases payload size. Upload refs avoid that overhead, but the final encoded email must still fit within the 25 MB message limit. Mailbox attachment uploads are available for mailbox send workflows that need to upload a file first and then reference the returned blob ID when sending. Direct mailbox uploads and presigned mailbox uploads use the same per-attachment cap. -## Upload before sending +## Upload before sending with the Sending API -For local tools and SDK helpers, send the file path and let the tool read the bytes locally. For shell-capable or hosted agents, mint a short-lived presigned upload URL, then upload the file bytes with the exact returned method, headers, content type, and content length. +Upload bytes directly when your client has the API key: + +```bash +SIZE_BYTES="$(wc -c < invoice.pdf | tr -d ' ')" +ATTACHMENT_ID="$( + curl -sS -X POST "https://smtp.sendmux.ai/api/v1/emails/attachments?filename=invoice.pdf" \ + -H "Authorization: Bearer smx_mbx_your_key_here" \ + -H "Content-Type: application/pdf" \ + -H "Content-Length: $SIZE_BYTES" \ + --data-binary @invoice.pdf \ + | jq -r '.data.attachment_id' +)" +``` + +Then reference it from `POST /emails/send` or `POST /emails/send/batch`: + +```json +{ + "from": { "email": "hello@example.com" }, + "to": [{ "email": "user@example.com" }], + "subject": "Invoice attached", + "html_body": "

Please find your invoice attached.

", + "attachments": [{ "attachment_id": "att_tz4a98xxat96iws9zmbrgj3a" }] +} +``` + +## Delegated uploads -The presigned upload URL does not include an API key. It can write only the single attachment it was minted for, expires quickly, and returns the same blob ID shape as a direct mailbox attachment upload. +Use a delegated upload when a browser, hosted agent, or another remote client needs to upload bytes without seeing your API key. + +```bash +SIZE_BYTES="$(wc -c < invoice.pdf | tr -d ' ')" +SHA256="$(shasum -a 256 invoice.pdf | awk '{print $1}')" + +curl -X POST https://smtp.sendmux.ai/api/v1/emails/attachment-uploads \ + -H "Authorization: Bearer smx_mbx_your_key_here" \ + -H "Content-Type: application/json" \ + -d "{ + \"filename\": \"invoice.pdf\", + \"content_type\": \"application/pdf\", + \"size_bytes\": $SIZE_BYTES, + \"sha256\": \"$SHA256\" + }" +``` + +The response includes an `upload_url` and required `headers`. Send a `PUT` request to that URL with the exact file bytes and returned headers. The upload token can write only that attachment, expires quickly, and does not grant send access. + +## Agent and MCP workflows + +For local MCP clients, use `sending_upload_attachment` with `file_path` before `sending_send_email` or `sending_send_email_batch`. The tool reads the local file from a client-approved root and returns an `attachment_id`. + +For hosted MCP clients or shell-capable agents, use `sending_create_attachment_upload`, upload the file bytes to the returned `upload_url` with the returned headers, then pass the resulting `attachment_id` in the send request. Do not place file bytes or large base64 strings in the prompt. + +For mailbox sends, use `mailbox_upload_attachment` first and send with the returned `blob_id`. For inbound mailbox attachments, use `mailbox_read_attachment` first when you need text-like content. + +## Mailbox upload before sending + +Mailbox send workflows can also upload a file first and reference the returned blob ID when sending. Direct mailbox uploads and presigned mailbox uploads use the same per-attachment cap. + +For local tools and SDK helpers, send the file path and let the tool read the bytes locally. For shell-capable or hosted agents, mint a short-lived presigned upload URL, then upload the file bytes with the exact returned method, headers, content type, and content length. ## Allowed files @@ -30,7 +88,7 @@ Sendmux accepts common image, document, spreadsheet, presentation, text, calenda Binary files are checked against their claimed file type. Text-based files are checked by extension. -## Add an attachment +## Inline base64 compatibility ```json { @@ -48,10 +106,13 @@ Binary files are checked against their claimed file type. Text-based files are c } ``` +Use this form only for small content that is already in memory. For files on disk, upload bytes first and use `attachment_id`. + ## Fields | Field | Required | Notes | | --- | --- | --- | +| `attachment_id` | Yes, for uploaded refs | Temporary ID returned by a Sending API upload endpoint. | | `filename` | Yes | Must include an allowed extension. | | `content` | Yes | Base64-encoded file content. | | `type` | No | Optional MIME type override. | @@ -94,7 +155,7 @@ Do not construct attachment URLs manually. - Add attachments to single or batch send requests. + Send uploaded attachment refs in single or batch requests. Receive live mailbox events and then download attachments from message metadata. diff --git a/guides/sending-via-http.mdx b/guides/sending-via-http.mdx index 5ba0b5e..819eace 100644 --- a/guides/sending-via-http.mdx +++ b/guides/sending-via-http.mdx @@ -38,7 +38,7 @@ curl -X POST https://smtp.sendmux.ai/api/v1/emails/send \ }' ``` -A successful request returns `201 Created` with a queued `message_id`. +A successful request returns `200 OK` with a queued `message_id`. ## Send a batch @@ -74,7 +74,7 @@ curl -X POST https://smtp.sendmux.ai/api/v1/emails/send/batch \ | `reply_to` | Direct replies to a different address. | | `return_path` | Set the envelope sender for bounce handling. | | `custom_headers` | Add custom `X-*` headers to the outgoing message. | -| `attachments` | Attach files as base64 content. | +| `attachments` | Attach uploaded `attachment_id` refs, or tiny inline base64 content. | | `Idempotency-Key` | Safely retry a request without sending twice. | See the [Sending API reference](/sending-api/introduction) for the full schema. diff --git a/openapi-sending.json b/openapi-sending.json index c0bf7bd..758c042 100644 --- a/openapi-sending.json +++ b/openapi-sending.json @@ -27,39 +27,184 @@ "type": "object" }, "Attachment": { + "anyOf": [ + { + "$ref": "#/components/schemas/InlineAttachment" + }, + { + "$ref": "#/components/schemas/UploadedAttachmentRef" + } + ] + }, + "AttachmentUploadData": { "additionalProperties": false, "properties": { - "content": { - "description": "Base64-encoded file content", - "minLength": 1, + "attachment_id": { + "description": "Temporary attachment ID to use in email attachments[].", + "example": "att_tz4a98xxat96iws9zmbrgj3a", + "pattern": "^att_[a-z0-9]{24}$", "type": "string" }, - "encoding": { - "default": "base64", + "content_type": { + "description": "Stored attachment MIME type", + "example": "application/pdf", + "type": "string" + }, + "expires_at": { + "description": "ISO timestamp when this temporary attachment reference expires.", + "example": "2026-07-07T08:15:00.000Z", + "format": "date-time", + "type": "string" + }, + "filename": { + "description": "Stored attachment filename", + "example": "analysis.pdf", + "type": "string" + }, + "size_bytes": { + "description": "Stored attachment byte size", + "example": 20480, + "type": "integer" + } + }, + "required": [ + "attachment_id", + "filename", + "content_type", + "size_bytes", + "expires_at" + ], + "type": "object" + }, + "AttachmentUploadIntentData": { + "additionalProperties": false, + "properties": { + "expires_at": { + "description": "ISO timestamp when the upload URL expires.", + "example": "2026-07-07T08:15:00.000Z", + "format": "date-time", + "type": "string" + }, + "headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Headers that must be sent with the PUT upload request.", + "example": { + "Content-Length": "20480", + "Content-Type": "application/pdf", + "X-Sendmux-Upload-Token": "smx_upload_..." + }, + "type": "object" + }, + "max_size_bytes": { + "description": "Maximum upload size in bytes", + "example": 18874368, + "type": "integer" + }, + "method": { + "description": "HTTP method for upload_url", "enum": [ - "base64" + "PUT" ], "type": "string" }, + "upload_id": { + "description": "Temporary upload intent ID.", + "example": "upl_tz4a98xxat96iws9zmbrgj3a", + "pattern": "^upl_[a-z0-9]{24}$", + "type": "string" + }, + "upload_url": { + "description": "Short-lived URL that accepts a binary PUT with the returned headers.", + "example": "https://smtp.sendmux.ai/api/v1/emails/attachment-uploads/upl_tz4a98xxat96iws9zmbrgj3a", + "format": "uri", + "type": "string" + } + }, + "required": [ + "upload_id", + "upload_url", + "method", + "expires_at", + "max_size_bytes", + "headers" + ], + "type": "object" + }, + "AttachmentUploadIntentRequest": { + "additionalProperties": false, + "properties": { + "content_type": { + "description": "MIME type expected for the upload.", + "example": "application/pdf", + "maxLength": 255, + "type": "string" + }, "filename": { - "description": "Filename with allowed extension", - "example": "invoice.pdf", + "description": "Filename to associate with the uploaded attachment.", + "example": "analysis.pdf", "maxLength": 255, "minLength": 1, "type": "string" }, - "type": { - "description": "MIME type override", - "example": "application/pdf", + "sha256": { + "description": "Optional SHA-256 hex digest for the upload bytes.", + "example": "0000000000000000000000000000000000000000000000000000000000000000", + "pattern": "^[a-f0-9]{64}$", "type": "string" + }, + "size_bytes": { + "description": "Exact byte size that will be uploaded.", + "example": 20480, + "exclusiveMinimum": 0, + "type": "integer" } }, "required": [ "filename", - "content" + "size_bytes" ], "type": "object" }, + "AttachmentUploadIntentResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessEnvelope" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/AttachmentUploadIntentData" + } + }, + "required": [ + "data" + ], + "type": "object" + } + ], + "unevaluatedProperties": false + }, + "AttachmentUploadResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/SuccessEnvelope" + }, + { + "properties": { + "data": { + "$ref": "#/components/schemas/AttachmentUploadData" + } + }, + "required": [ + "data" + ], + "type": "object" + } + ], + "unevaluatedProperties": false + }, "BatchResultItem": { "additionalProperties": false, "properties": { @@ -184,7 +329,7 @@ "additionalProperties": false, "properties": { "attachments": { - "description": "File attachments (max 10)", + "description": "File attachments (max 10). Use attachment_id refs for uploaded files.", "items": { "$ref": "#/components/schemas/Attachment" }, @@ -361,6 +506,40 @@ ], "type": "object" }, + "InlineAttachment": { + "additionalProperties": false, + "properties": { + "content": { + "description": "Base64-encoded file content", + "minLength": 1, + "type": "string" + }, + "encoding": { + "default": "base64", + "enum": [ + "base64" + ], + "type": "string" + }, + "filename": { + "description": "Filename with allowed extension", + "example": "invoice.pdf", + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "type": { + "description": "MIME type override", + "example": "application/pdf", + "type": "string" + } + }, + "required": [ + "filename", + "content" + ], + "type": "object" + }, "Meta": { "additionalProperties": false, "properties": { @@ -455,6 +634,21 @@ "meta" ], "type": "object" + }, + "UploadedAttachmentRef": { + "additionalProperties": false, + "properties": { + "attachment_id": { + "description": "Temporary uploaded attachment ID returned by POST /emails/attachments.", + "example": "att_tz4a98xxat96iws9zmbrgj3a", + "pattern": "^att_[a-z0-9]{24}$", + "type": "string" + } + }, + "required": [ + "attachment_id" + ], + "type": "object" } }, "securitySchemes": { @@ -472,6 +666,528 @@ }, "openapi": "3.1.0", "paths": { + "/emails/attachment-uploads": { + "post": { + "description": "Create a short-lived upload URL and token that lets a remote client PUT one binary attachment without exposing the API key on the upload request. Requires `email.send` permission.", + "operationId": "sendingCreateAttachmentUpload", + "parameters": [ + { + "description": "Optional client-generated key to make the request idempotent for 24 hours. Replays under the same key return the cached response; a reused key with a different body returns 409 idempotency_conflict.", + "example": "order-12345-2026-04-24", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "maxLength": 255, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttachmentUploadIntentRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttachmentUploadIntentResponse" + } + } + }, + "description": "Attachment upload URL created", + "headers": { + "Location": { + "description": "Canonical URL for the temporary attachment upload intent.", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Malformed request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication required" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Insufficient permissions" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Idempotency conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Attachment exceeds the upload size cap" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Attachment upload validation failed" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Rate limit exceeded" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Attachment upload service temporarily unavailable" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Create an attachment upload URL", + "tags": [ + "Attachments" + ] + } + }, + "/emails/attachment-uploads/{upload_id}": { + "put": { + "description": "Upload the exact binary bytes for a previously-created attachment upload URL. This operation uses the short-lived upload token header returned by POST /emails/attachment-uploads, not a Sendmux API key.", + "operationId": "sendingCompleteAttachmentUpload", + "parameters": [ + { + "description": "Short-lived upload token returned by POST /emails/attachment-uploads.", + "in": "header", + "name": "X-Sendmux-Upload-Token", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Upload intent ID returned by POST /emails/attachment-uploads.", + "in": "path", + "name": "upload_id", + "required": true, + "schema": { + "pattern": "^upl_[a-z0-9]{24}$", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttachmentUploadResponse" + } + } + }, + "description": "Attachment uploaded successfully", + "headers": { + "Location": { + "description": "Canonical URL for the temporary uploaded attachment metadata.", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Missing or invalid upload headers" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Upload token missing, invalid, or expired" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Upload intent not found or expired" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Upload already completed with different content" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Uploaded bytes do not match the expected size" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Attachment validation failed" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Attachment storage temporarily unavailable" + } + }, + "security": [], + "summary": "Upload bytes to an attachment upload URL", + "tags": [ + "Attachments" + ] + } + }, + "/emails/attachments": { + "post": { + "description": "Upload binary attachment bytes and receive a temporary attachment_id for use in attachments[]. Requires `email.send` permission.", + "operationId": "sendingUploadAttachment", + "parameters": [ + { + "description": "Optional client-generated key to make the request idempotent for 24 hours. Replays under the same key return the cached response; a reused key with a different body returns 409 idempotency_conflict.", + "example": "order-12345-2026-04-24", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "maxLength": 255, + "type": "string" + } + }, + { + "description": "Filename to associate with the uploaded attachment.", + "example": "analysis.pdf", + "in": "query", + "name": "filename", + "required": true, + "schema": { + "maxLength": 255, + "minLength": 1, + "type": "string" + } + }, + { + "description": "MIME type override for the uploaded attachment.", + "example": "application/pdf", + "in": "query", + "name": "content_type", + "required": false, + "schema": { + "maxLength": 255, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttachmentUploadResponse" + } + } + }, + "description": "Attachment uploaded successfully", + "headers": { + "Location": { + "description": "Canonical URL for the temporary uploaded attachment metadata.", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Missing or invalid upload metadata" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication required" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Insufficient permissions" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Idempotency conflict" + }, + "413": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Attachment exceeds the upload size cap" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Attachment validation failed" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Rate limit exceeded" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Attachment storage temporarily unavailable" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Upload an attachment", + "tags": [ + "Attachments" + ] + } + }, + "/emails/attachments/{attachment_id}": { + "get": { + "description": "Return metadata for a temporary uploaded attachment owned by the authenticated team. File bytes are not returned.", + "operationId": "sendingGetAttachment", + "parameters": [ + { + "description": "Temporary attachment ID returned by an upload endpoint.", + "in": "path", + "name": "attachment_id", + "required": true, + "schema": { + "pattern": "^att_[a-z0-9]{24}$", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttachmentUploadResponse" + } + } + }, + "description": "Attachment metadata" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Authentication required" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Insufficient permissions" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Attachment not found or expired" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Rate limit exceeded" + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Attachment storage temporarily unavailable" + } + }, + "security": [ + { + "BearerAuth": [] + } + ], + "summary": "Get attachment metadata", + "tags": [ + "Attachments" + ] + } + }, "/emails/send": { "post": { "description": "Queue a single email for delivery. Requires `email.send` permission.", diff --git a/postman/sendmux-sending.postman_collection.json b/postman/sendmux-sending.postman_collection.json index 7562266..3b68210 100644 --- a/postman/sendmux-sending.postman_collection.json +++ b/postman/sendmux-sending.postman_collection.json @@ -19,6 +19,696 @@ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ + { + "description": { + "content": "", + "type": "text/plain" + }, + "event": [], + "item": [ + { + "event": [], + "name": "Create an attachment upload URL", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\n \"content_type\": \"\",\n \"filename\": \"\",\n \"sha256\": \"\",\n \"size_bytes\": \"\"\n}" + }, + "description": { + "content": "Create a short-lived upload URL and token that lets a remote client PUT one binary attachment without exposing the API key on the upload request. Requires `email.send` permission.", + "type": "text/plain" + }, + "header": [ + { + "description": "Optional client-generated key to make the request idempotent for 24 hours. Replays under the same key return the cached response; a reused key with a different body returns 409 idempotency_conflict.", + "disabled": true, + "key": "Idempotency-Key", + "value": "" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "POST", + "name": "Create an attachment upload URL", + "url": { + "host": [ + "{{sending_base_url}}" + ], + "path": [ + "emails", + "attachment-uploads" + ], + "query": [], + "variable": [] + } + }, + "response": [ + { + "_postman_previewlanguage": "json", + "body": "", + "code": 201, + "cookie": [], + "header": [ + { + "description": "Canonical URL for the temporary attachment upload intent.", + "disabled": true, + "key": "Location", + "value": "string" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment upload URL created", + "status": "Created" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 400, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Malformed request", + "status": "Bad Request" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 401, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Authentication required", + "status": "Unauthorized" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 403, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Insufficient permissions", + "status": "Forbidden" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 409, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Idempotency conflict", + "status": "Conflict" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 413, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment exceeds the upload size cap", + "status": "Request Entity Too Large" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 422, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment upload validation failed", + "status": "Unprocessable Entity (WebDAV) (RFC 4918)" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 429, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Rate limit exceeded", + "status": "Too Many Requests" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 503, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment upload service temporarily unavailable", + "status": "Service Unavailable" + } + ] + }, + { + "event": [], + "name": "Upload bytes to an attachment upload URL", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "body": { + "mode": "raw", + "raw": "" + }, + "description": { + "content": "Upload the exact binary bytes for a previously-created attachment upload URL. This operation uses the short-lived upload token header returned by POST /emails/attachment-uploads, not a Sendmux API key.", + "type": "text/plain" + }, + "header": [ + { + "description": "(Required) Short-lived upload token returned by POST /emails/attachment-uploads.", + "disabled": false, + "key": "X-Sendmux-Upload-Token", + "value": "" + }, + { + "key": "Content-Type", + "value": "application/octet-stream" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "PUT", + "name": "Upload bytes to an attachment upload URL", + "url": { + "host": [ + "{{sending_base_url}}" + ], + "path": [ + "emails", + "attachment-uploads", + ":upload_id" + ], + "query": [], + "variable": [ + { + "description": "(Required) Upload intent ID returned by POST /emails/attachment-uploads.", + "disabled": false, + "key": "upload_id", + "type": "any", + "value": "" + } + ] + } + }, + "response": [ + { + "_postman_previewlanguage": "json", + "body": "", + "code": 201, + "cookie": [], + "header": [ + { + "description": "Canonical URL for the temporary uploaded attachment metadata.", + "disabled": true, + "key": "Location", + "value": "string" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment uploaded successfully", + "status": "Created" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 400, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Missing or invalid upload headers", + "status": "Bad Request" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 401, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Upload token missing, invalid, or expired", + "status": "Unauthorized" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 404, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Upload intent not found or expired", + "status": "Not Found" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 409, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Upload already completed with different content", + "status": "Conflict" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 413, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Uploaded bytes do not match the expected size", + "status": "Request Entity Too Large" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 422, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment validation failed", + "status": "Unprocessable Entity (WebDAV) (RFC 4918)" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 503, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment storage temporarily unavailable", + "status": "Service Unavailable" + } + ] + }, + { + "event": [], + "name": "Upload an attachment", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "body": { + "mode": "raw", + "raw": "" + }, + "description": { + "content": "Upload binary attachment bytes and receive a temporary attachment_id for use in attachments[]. Requires `email.send` permission.", + "type": "text/plain" + }, + "header": [ + { + "description": "Optional client-generated key to make the request idempotent for 24 hours. Replays under the same key return the cached response; a reused key with a different body returns 409 idempotency_conflict.", + "disabled": true, + "key": "Idempotency-Key", + "value": "" + }, + { + "key": "Content-Type", + "value": "application/octet-stream" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "POST", + "name": "Upload an attachment", + "url": { + "host": [ + "{{sending_base_url}}" + ], + "path": [ + "emails", + "attachments" + ], + "query": [ + { + "description": "(Required) Filename to associate with the uploaded attachment.", + "disabled": false, + "key": "filename", + "value": "" + }, + { + "description": "MIME type override for the uploaded attachment.", + "disabled": true, + "key": "content_type", + "value": "" + } + ], + "variable": [] + } + }, + "response": [ + { + "_postman_previewlanguage": "json", + "body": "", + "code": 201, + "cookie": [], + "header": [ + { + "description": "Canonical URL for the temporary uploaded attachment metadata.", + "disabled": true, + "key": "Location", + "value": "string" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment uploaded successfully", + "status": "Created" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 400, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Missing or invalid upload metadata", + "status": "Bad Request" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 401, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Authentication required", + "status": "Unauthorized" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 403, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Insufficient permissions", + "status": "Forbidden" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 409, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Idempotency conflict", + "status": "Conflict" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 413, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment exceeds the upload size cap", + "status": "Request Entity Too Large" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 422, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment validation failed", + "status": "Unprocessable Entity (WebDAV) (RFC 4918)" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 429, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Rate limit exceeded", + "status": "Too Many Requests" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 503, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment storage temporarily unavailable", + "status": "Service Unavailable" + } + ] + }, + { + "event": [], + "name": "Get attachment metadata", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "description": { + "content": "Return metadata for a temporary uploaded attachment owned by the authenticated team. File bytes are not returned.", + "type": "text/plain" + }, + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "GET", + "name": "Get attachment metadata", + "url": { + "host": [ + "{{sending_base_url}}" + ], + "path": [ + "emails", + "attachments", + ":attachment_id" + ], + "query": [], + "variable": [ + { + "description": "(Required) Temporary attachment ID returned by an upload endpoint.", + "disabled": false, + "key": "attachment_id", + "type": "any", + "value": "" + } + ] + } + }, + "response": [ + { + "_postman_previewlanguage": "json", + "body": "", + "code": 200, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment metadata", + "status": "OK" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 401, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Authentication required", + "status": "Unauthorized" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 403, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Insufficient permissions", + "status": "Forbidden" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 404, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment not found or expired", + "status": "Not Found" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 429, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Rate limit exceeded", + "status": "Too Many Requests" + }, + { + "_postman_previewlanguage": "json", + "body": "", + "code": 503, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "name": "Attachment storage temporarily unavailable", + "status": "Service Unavailable" + } + ] + } + ], + "name": "Attachments" + }, { "description": { "content": "", diff --git a/sending-api/errors.mdx b/sending-api/errors.mdx index bb5c620..86e0ffd 100644 --- a/sending-api/errors.mdx +++ b/sending-api/errors.mdx @@ -81,6 +81,7 @@ Common causes: - Missing required fields (`from`, `to`, `subject`, `html_body`) - Invalid email addresses - Attachment file type not in the allowed list +- Uploaded attachment refs that expired or belong to a different team - Batch exceeds 100 messages ### idempotency_conflict @@ -89,7 +90,7 @@ Common causes: ### payload_too_large -**HTTP 413**: The request body exceeds the maximum allowed size. `html_body` and `text_body` are capped at 25MB each. The full request body, including base64-encoded attachments, must be 25MB or less. Attachments are also bounded by the 10-attachment maximum. +**HTTP 413**: The request body, binary attachment upload, or final encoded message exceeds the maximum allowed size. `html_body` and `text_body` are capped at 25MB each. Inline base64 attachments count towards the JSON request body. Uploaded attachment bytes are capped separately, and the final sent message must still fit within 25MB. ### no_active_providers diff --git a/sending-api/introduction.mdx b/sending-api/introduction.mdx index 5f7ea2c..f8c8a7d 100644 --- a/sending-api/introduction.mdx +++ b/sending-api/introduction.mdx @@ -64,6 +64,10 @@ API keys are scoped to a team. Create and manage keys under **Settings > API Key | -------------------------------- | ------------------- | | `POST /api/v1/emails/send` | `email.send` | | `POST /api/v1/emails/send/batch` | `email.send` | +| `POST /api/v1/emails/attachments` | `email.send` | +| `GET /api/v1/emails/attachments/{attachment_id}` | `email.send` | +| `POST /api/v1/emails/attachment-uploads` | `email.send` | +| `PUT /api/v1/emails/attachment-uploads/{upload_id}` | Upload token | | `GET /api/v1/openapi.json` | None (public) | Account-management endpoints live in the Management API and use root API keys with `provider.*` permissions. @@ -135,7 +139,8 @@ Single-resource `GET` responses return a weak `ETag`. Send it back in an `If-Non - **UTC ISO 8601 timestamps** in RFC 3339 format: `2026-03-19T10:30:00Z` - **Public IDs only**. Private numeric IDs are never exposed. - **`Cache-Control: no-store`** on all authenticated responses (the OpenAPI spec endpoint uses `public, max-age=3600`) -- **JSON only**. Use `Content-Type: application/json`. +- JSON endpoints use `Content-Type: application/json`. +- Binary attachment upload endpoints use `Content-Type` for the file MIME type and send raw bytes with `application/octet-stream` or the file's MIME type. ## OpenAPI specification From c45da6716f95234f4027f0eec2b28fea402ea59d Mon Sep 17 00:00:00 2001 From: Roshan Jonnalagadda Date: Tue, 7 Jul 2026 22:08:00 +1000 Subject: [PATCH 4/6] docs: clarify hosted MCP attachment uploads --- changelog.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.mdx b/changelog.mdx index d534376..98c04a4 100644 --- a/changelog.mdx +++ b/changelog.mdx @@ -21,6 +21,8 @@ Existing short-lived download links keep the same expiry and safety model, while MCP clients can now read text attachments directly through `mailbox_read_attachment`, so agents no longer need a separate web fetch step for common files like Markdown recipes. +Hosted MCP Sending tools now expose the same attachment upload workflow, including delegated uploads for agents that should not handle API keys or base64 payloads. +
From 86108c7d27b122803a1bd59d3f54e05835e04638 Mon Sep 17 00:00:00 2001 From: Roshan Jonnalagadda Date: Tue, 7 Jul 2026 22:09:07 +1000 Subject: [PATCH 5/6] docs: refine hosted MCP attachment upload changelog --- changelog.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.mdx b/changelog.mdx index 98c04a4..b43a845 100644 --- a/changelog.mdx +++ b/changelog.mdx @@ -21,7 +21,7 @@ Existing short-lived download links keep the same expiry and safety model, while MCP clients can now read text attachments directly through `mailbox_read_attachment`, so agents no longer need a separate web fetch step for common files like Markdown recipes. -Hosted MCP Sending tools now expose the same attachment upload workflow, including delegated uploads for agents that should not handle API keys or base64 payloads. +Hosted MCP Sending tools now support direct attachment uploads and delegated upload URLs, so agents do not need to handle API keys or base64 payloads. From 043c78d26664b553442ae3364f232a3c1df42783 Mon Sep 17 00:00:00 2001 From: Roshan Jonnalagadda Date: Tue, 7 Jul 2026 22:09:51 +1000 Subject: [PATCH 6/6] docs: clarify hosted MCP sending attachment refs --- guides/attachments.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/attachments.mdx b/guides/attachments.mdx index f65345c..da1b6ba 100644 --- a/guides/attachments.mdx +++ b/guides/attachments.mdx @@ -72,7 +72,7 @@ The response includes an `upload_url` and required `headers`. Send a `PUT` reque For local MCP clients, use `sending_upload_attachment` with `file_path` before `sending_send_email` or `sending_send_email_batch`. The tool reads the local file from a client-approved root and returns an `attachment_id`. -For hosted MCP clients or shell-capable agents, use `sending_create_attachment_upload`, upload the file bytes to the returned `upload_url` with the returned headers, then pass the resulting `attachment_id` in the send request. Do not place file bytes or large base64 strings in the prompt. +For hosted MCP clients or shell-capable agents, use `sending_create_attachment_upload`, upload the file bytes to the returned `upload_url` with the returned headers, then pass the resulting `attachment_id` in `sending_send_email` or `sending_send_email_batch`. Do not place file bytes or large base64 strings in the prompt. For mailbox sends, use `mailbox_upload_attachment` first and send with the returned `blob_id`. For inbound mailbox attachments, use `mailbox_read_attachment` first when you need text-like content.