diff --git a/examples/mail/FileAttachmentExample.java b/examples/mail/FileAttachmentExample.java
new file mode 100644
index 00000000..0f2ba820
--- /dev/null
+++ b/examples/mail/FileAttachmentExample.java
@@ -0,0 +1,64 @@
+import com.sendgrid.Method;
+import com.sendgrid.Request;
+import com.sendgrid.Response;
+import com.sendgrid.SendGrid;
+import com.sendgrid.helpers.mail.Mail;
+import com.sendgrid.helpers.mail.objects.Attachments;
+import com.sendgrid.helpers.mail.objects.Content;
+import com.sendgrid.helpers.mail.objects.Email;
+import java.io.IOException;
+
+public class FileAttachmentExample {
+
+ // Small 1x1 PNG image embedded directly; no local image file is required.
+ private static final String IMAGE_BASE64 =
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mOQanoEAAI4AX++PQw4AAAAAElFTkSuQmCC";
+
+ public static void main(String[] args) throws IOException {
+ attachment();
+ }
+
+ public static void attachment() throws IOException {
+ String apiKey = System.getenv("SENDGRID_API_KEY");
+ if (apiKey == null || apiKey.trim().isEmpty()) {
+ throw new IllegalStateException(
+ "Set the SENDGRID_API_KEY environment variable.");
+ }
+
+ String fromAddress = "sender@example.com";
+ String toAddress = "recipient@example.com";
+
+ Mail mail = new Mail(
+ new Email(fromAddress),
+ "SendGrid Java: File attachment example",
+ new Email(toAddress),
+ new Content("text/plain",
+ "Please find the attached image."));
+
+ Attachments attachment = new Attachments();
+ attachment.setContent(IMAGE_BASE64);
+ attachment.setType("image/png");
+ attachment.setFilename("mail.png");
+ attachment.setDisposition("attachment");
+ mail.addAttachments(attachment);
+
+ sendEmail(apiKey, mail);
+ }
+
+ private static void sendEmail(String apiKey, Mail mail)
+ throws IOException {
+ Request request = new Request();
+ request.setMethod(Method.POST);
+ request.setEndpoint("mail/send");
+ request.setBody(mail.build());
+
+ Response response = new SendGrid(apiKey).api(request);
+ System.out.println("HTTP status: " + response.getStatusCode());
+
+ if (response.getStatusCode() < 200
+ || response.getStatusCode() >= 300) {
+ System.err.println(response.getBody());
+ throw new IllegalStateException("Email sending failed.");
+ }
+ }
+}
diff --git a/examples/mail/InlineFileAttachmentExample.java b/examples/mail/InlineFileAttachmentExample.java
new file mode 100644
index 00000000..8aecfcb4
--- /dev/null
+++ b/examples/mail/InlineFileAttachmentExample.java
@@ -0,0 +1,68 @@
+import com.sendgrid.Method;
+import com.sendgrid.Request;
+import com.sendgrid.Response;
+import com.sendgrid.SendGrid;
+import com.sendgrid.helpers.mail.Mail;
+import com.sendgrid.helpers.mail.objects.Attachments;
+import com.sendgrid.helpers.mail.objects.Content;
+import com.sendgrid.helpers.mail.objects.Email;
+import java.io.IOException;
+
+public class InlineFileAttachmentExample {
+
+ private static final String CONTENT_ID = "company-logo";
+
+ // Small 1x1 PNG image embedded directly; no local image file is required.
+ private static final String IMAGE_BASE64 =
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mOQanoEAAI4AX++PQw4AAAAAElFTkSuQmCC";
+
+ public static void main(String[] args) throws IOException {
+ inline();
+ }
+
+ private static void inline() throws IOException {
+ String apiKey = System.getenv("SENDGRID_API_KEY");
+ if (apiKey == null || apiKey.trim().isEmpty()) {
+ throw new IllegalStateException(
+ "Set the SENDGRID_API_KEY environment variable.");
+ }
+
+ String fromAddress = "sender@example.com";
+ String toAddress = "recipient@example.com";
+
+ String html = ""
+ + ""
+ + "
"
+ + "Welcome!
"
+ + "This image is embedded in the email:
"
+ + "
"
+ + ""
+ + "";
+
+ Mail mail = new Mail(
+ new Email(fromAddress),
+ "SendGrid Java: Inline image example",
+ new Email(toAddress),
+ new Content("text/html", html)
+ );
+
+ Attachments inlineImage = new Attachments();
+ inlineImage.setContent(IMAGE_BASE64);
+ inlineImage.setType("image/png");
+ inlineImage.setFilename("mail.png");
+ inlineImage.setDisposition("inline");
+ inlineImage.setContentId(CONTENT_ID);
+ mail.addAttachments(inlineImage);
+
+ Request request = new Request();
+ request.setMethod(Method.POST);
+ request.setEndpoint("mail/send");
+ request.setBody(mail.build());
+
+ Response response = new SendGrid(apiKey).api(request);
+ System.out.println("HTTP status: " + response.getStatusCode());
+ if (response.getStatusCode() < 200 || response.getStatusCode() > 299) {
+ System.err.println(response.getBody());
+ }
+ }
+}
diff --git a/examples/mail/PreventThreadingExample.java b/examples/mail/PreventThreadingExample.java
new file mode 100644
index 00000000..ab04dfdf
--- /dev/null
+++ b/examples/mail/PreventThreadingExample.java
@@ -0,0 +1,64 @@
+import com.sendgrid.Method;
+import com.sendgrid.Request;
+import com.sendgrid.Response;
+import com.sendgrid.SendGrid;
+import com.sendgrid.helpers.mail.Mail;
+import com.sendgrid.helpers.mail.objects.Content;
+import com.sendgrid.helpers.mail.objects.Email;
+import java.io.IOException;
+import java.util.UUID;
+
+public class PreventThreadingExample {
+ public static void main(String[] args) throws IOException {
+ String apiKey = System.getenv("SENDGRID_API_KEY");
+ if (apiKey == null || apiKey.trim().isEmpty()) {
+ throw new IllegalStateException(
+ "Set the SENDGRID_API_KEY environment variable.");
+ }
+
+ String fromAddress = "sender@example.com";
+ String toAddress = "recipient@example.com";
+
+ // Gmail groups emails that share a subject into one conversation.
+ // A unique X-Entity-Ref-ID header on each email keeps them separate.
+ // See https://docs.customer.io/messaging/channels/email/headers/custom-mail-headers/
+ for (int i = 1; i <= 3; i++) {
+ Mail mail = new Mail(
+ new Email(fromAddress),
+ "Invoice Reminder",
+ new Email(toAddress),
+ new Content("text/plain",
+ "This is email #" + i + ". It should appear as its own conversation."));
+ mail.addHeader("X-Entity-Ref-ID", UUID.randomUUID().toString());
+
+ String sgMessageId = sendEmail(apiKey, mail);
+ System.out.println("Email #" + i + " sent: " + sgMessageId);
+ }
+
+ System.out.println("All emails sent with unique X-Entity-Ref-ID headers.");
+ System.out.println("Each should appear as a separate conversation in Gmail.");
+ }
+
+ // Returns the X-Message-Id SendGrid assigned to the email.
+ private static String sendEmail(String apiKey, Mail mail) throws IOException {
+ Request request = new Request();
+ request.setMethod(Method.POST);
+ request.setEndpoint("mail/send");
+ request.setBody(mail.build());
+
+ Response response = new SendGrid(apiKey).api(request);
+ System.out.println("HTTP status: " + response.getStatusCode());
+
+ if (response.getStatusCode() < 200
+ || response.getStatusCode() >= 300) {
+ System.err.println(response.getBody());
+ throw new IllegalStateException("Email sending failed.");
+ }
+
+ String messageId = response.getHeaders().get("X-Message-Id");
+ if (messageId == null) {
+ throw new IllegalStateException("SendGrid did not return X-Message-Id.");
+ }
+ return messageId;
+ }
+}
diff --git a/examples/mail/ThreadingExample.java b/examples/mail/ThreadingExample.java
new file mode 100644
index 00000000..aed3099d
--- /dev/null
+++ b/examples/mail/ThreadingExample.java
@@ -0,0 +1,71 @@
+import com.sendgrid.Method;
+import com.sendgrid.Request;
+import com.sendgrid.Response;
+import com.sendgrid.SendGrid;
+import com.sendgrid.helpers.mail.Mail;
+import com.sendgrid.helpers.mail.objects.Content;
+import com.sendgrid.helpers.mail.objects.Email;
+import java.io.IOException;
+
+public class ThreadingExample {
+ public static void main(String[] args) throws IOException {
+ String apiKey = System.getenv("SENDGRID_API_KEY");
+ if (apiKey == null || apiKey.trim().isEmpty()) {
+ throw new IllegalStateException(
+ "Set the SENDGRID_API_KEY environment variable.");
+ }
+
+ String fromAddress = "sender@example.com";
+ String toAddress = "recipient@example.com";
+
+ if (args.length == 0) {
+ // Step 1: send the first email. Its smtp-id arrives in the "delivered"
+ // webhook event, not in this response.
+ Mail first = new Mail(
+ new Email(fromAddress),
+ "SendGrid Java: Threading example",
+ new Email(toAddress),
+ new Content("text/plain", "This starts the thread."));
+ String sgMessageId = sendEmail(apiKey, first);
+ System.out.println("X-Message-Id: " + sgMessageId);
+ System.out.println("Find the smtp-id for this message in its delivered "
+ + "webhook event, then run again with it as the argument.");
+ return;
+ }
+
+ // Step 2: reply using the smtp-id from the delivered webhook event,
+ // for example "".
+ String smtpId = args[0];
+ Mail reply = new Mail(
+ new Email(fromAddress),
+ "Re: SendGrid Java: Threading example",
+ new Email(toAddress),
+ new Content("text/plain", "This reply should group with the first email."));
+ reply.addHeader("In-Reply-To", smtpId);
+ reply.addHeader("References", smtpId);
+ sendEmail(apiKey, reply);
+ }
+
+ // Returns the X-Message-Id SendGrid assigned to the email.
+ private static String sendEmail(String apiKey, Mail mail) throws IOException {
+ Request request = new Request();
+ request.setMethod(Method.POST);
+ request.setEndpoint("mail/send");
+ request.setBody(mail.build());
+
+ Response response = new SendGrid(apiKey).api(request);
+ System.out.println("HTTP status: " + response.getStatusCode());
+
+ if (response.getStatusCode() < 200
+ || response.getStatusCode() >= 300) {
+ System.err.println(response.getBody());
+ throw new IllegalStateException("Email sending failed.");
+ }
+
+ String messageId = response.getHeaders().get("X-Message-Id");
+ if (messageId == null) {
+ throw new IllegalStateException("SendGrid did not return X-Message-Id.");
+ }
+ return messageId;
+ }
+}
diff --git a/use-cases/README.md b/use-cases/README.md
index 204d8609..4d8c31f5 100644
--- a/use-cases/README.md
+++ b/use-cases/README.md
@@ -6,6 +6,7 @@ Please [open an issue](https://github.com/sendgrid/sendgrid-ruby/issues) or [mak
* [Transactional Templates](transactional-templates.md)
* [Legacy Templates](legacy-templates.md)
* [Personalizations](personalizations.md)
+* [Threaded Replies](threaded-replies.md)
# Twilio Use Cases
* [Twilio Setup](twilio-setup.md)
diff --git a/use-cases/threaded-replies.md b/use-cases/threaded-replies.md
new file mode 100644
index 00000000..891aaedf
--- /dev/null
+++ b/use-cases/threaded-replies.md
@@ -0,0 +1,105 @@
+# Threaded Replies
+
+This example goes over how to send a reply that stays in the same email thread as an earlier message. You can find documentation for the Event Webhook [here](https://sendgrid.com/docs/API_Reference/Webhooks/event.html), for the Webhooks API [here](https://www.twilio.com/docs/sendgrid/api-reference/webhooks), and for Mail Send [here](https://docs.sendgrid.com/api-reference/mail-send/mail-send).
+
+A reply sent without threading headers shows up in Gmail as a new conversation. For example, a follow-up to an order confirmation lands as a separate message instead of under the original. Setting `In-Reply-To` and `References` to the original email's `smtp-id` keeps the reply in the same thread in Gmail, Outlook, and Apple Mail.
+
+A complete two-step version is in [ThreadingExample.java](../examples/mail/ThreadingExample.java).
+
+## Which ID to use
+
+**Short answer:** reply with the `smtp-id` from the `delivered` webhook event.
+The `X-Message-Id` from the send response is not the value that threads a
+reply.
+
+Mail clients group messages by the RFC 5322 `Message-ID` header. The webhook's
+`smtp-id` is that `Message-ID`, so it's the value to use.
+
+The reply's subject should be the original subject with `Re: ` in front. Gmail uses the subject as a second signal when grouping, so a mismatched subject is more likely to start a new conversation.
+
+## Sequence
+
+```
+your app SendGrid webhook endpoint
+ | POST /mail/send | |
+ |---------------------------->| |
+ | response: X-Message-Id | |
+ |<----------------------------| |
+ | store X-Message-Id | |
+ | | delivered (smtp-id) |
+ | |----------------------------->|
+ | | | match on sg_message_id,
+ | | | store smtp-id
+ | POST /mail/send | |
+ | In-Reply-To: smtp-id | |
+ |---------------------------->| |
+```
+
+## Matching a webhook event to a send
+
+`sg_message_id` in an event is the response's `X-Message-Id` followed by a
+suffix. To match, take the part before the first `.` and compare it with the
+stored `X-Message-Id`:
+
+```
+sg_message_id: abc123XYZ.recvd-example-1-0
+ └──────┘
+ match against stored X-Message-Id: abc123XYZ
+```
+
+When it matches, save the event's `smtp-id` on the stored record. This rule is
+based on one sample, so confirm it on several real sends before you depend on
+it.
+
+Example `delivered` event (trimmed):
+
+```json
+{
+ "email": "recipient@example.com",
+ "event": "delivered",
+ "sg_event_id": "ZXhhbXBsZS1ldmVudC1pZA",
+ "sg_message_id": "abc123XYZ.recvd-example-1-0",
+ "smtp-id": "",
+ "timestamp": 1700000000
+}
+```
+
+The `processed` event carries the same `smtp-id`, so it also works if you need
+the ID before delivery.
+
+## Troubleshooting
+
+- **The reply starts a new thread.** Check which value you sent. The
+ `X-Message-Id` will not thread. Send the `smtp-id`, and keep its angle
+ brackets.
+- **The reply is in a separate conversation.** Check that the subject is the original subject with `Re: ` in front, as well as the `smtp-id`.
+- **There is no `smtp-id` yet.** The reply has to wait for the `delivered` or
+ `processed` event. Don't send it from the API response alone.
+- **No event matches the stored ID.** Compare the part of `sg_message_id` before
+ the first `.` with the stored `X-Message-Id`, character for character.
+- **The same event arrives twice.** Webhooks can repeat, so make the save
+ idempotent: writing the same `smtp-id` twice should change nothing.
+
+## Verify the thread in Gmail
+
+Check the headers after the reply arrives. This shows whether the reply carries the right values.
+
+1. Open the original email in Gmail and confirm the reply appears under it, in the same conversation.
+2. On the reply, click the three-dot **More** menu next to the reply button, then choose **Show original**.
+3. In the raw headers, find these lines:
+ - `Message-ID:` on the original email. It should match the `smtp-id` from the `delivered` event, including the angle brackets.
+ - `In-Reply-To:` on the reply. It should contain the same `smtp-id`.
+ - `References:` on the reply. It should contain the same `smtp-id`.
+4. If the reply shows in a separate conversation, compare its `In-Reply-To` and `References` values with the original's `Message-ID`. A mismatch means the reply was sent with the wrong ID.
+
+## Viewing SendGrid deliverability webhook events
+
+The Event Webhook needs a public URL to post to. For a first test, use a temporary endpoint:
+
+1. Open [webhook.site](https://webhook.site) and copy the unique URL it gives you.
+2. In the SendGrid dashboard, open the Event Webhook settings, paste the URL, and enable the `delivered` and `processed` events.
+3. Run `ThreadingExample` to send the first email. Refresh webhook.site and look for the `delivered` event, then check its `sg_message_id` and `smtp-id`.
+
+Other tools that work the same way include [Beeceptor](https://beeceptor.com), which can also mock responses, and the [Svix webhook testing guide](https://www.svix.com/resources/faq/best-webhook-testing-inspecting-and-debugging-tools-2026/), which compares several options.
+
+Temporary endpoints are public, so anyone with the URL can read what is posted to it. Don't send real customer data through one.