# Send Email with Java: A Complete API Tutorial

> Send email with Java using HttpClient and an email API instead of Jakarta Mail: templates, attachments, batches, safe retries and a Spring Boot example.

Canonical: https://www.cresca.xyz/blog/send-email-java
Author: Lalit Kumar Jangid
Published: 2026-10-27
Last updated: 2026-10-27

The classic way to **send email with Java** is Jakarta Mail (formerly JavaMail) over SMTP: sessions, transports, properties and a mail server to run or rent. An email API is simpler. With Java's built-in `HttpClient` you send one JSON request, Cresca returns an email id, and the message goes out from your own verified domain within seconds. This tutorial covers the basic request, a reusable client with retries, templates, attachments, batches and a Spring Boot bean.

**TL;DR**

- POST JSON to `https://api.cresca.xyz/api/v1/emails` with `java.net.http.HttpClient` (Java 11+).
- Send `Authorization: Bearer <key>`; the body has from, to, subject and html.
- Retry 429, 5xx and `IOException` with an `Idempotency-Key` so a retry never sends twice.
- Batch up to 100 emails per request.

## Before you start

1. **Verify a sending domain.** In Cresca, open [Sending Domains](https://app.cresca.xyz/sending-domains), add your domain and publish the DNS records it shows (DKIM, a custom MAIL FROM domain and an ownership record). If your DNS is on Cloudflare, Cresca can publish them for you.
2. **Add a sender** on that domain, for example `hello@acme.com`. Every `from` address you send from must be an active sender on a verified domain.
3. **Create an API key** on the [API keys](https://app.cresca.xyz/api-keys) page. Choose *Sending access* for production: it can send email but cannot read logs or change templates. The key starts with `cr_live_` and is shown once, so store it as the environment variable `CRESCA_API_KEY`.

The basic example needs Java 17 or later and nothing else. The client below uses Jackson (`com.fasterxml.jackson.core:jackson-databind`) to build and read JSON; Spring Boot already includes it. The full setup, with screenshots of each step, is in the [email API quickstart](https://www.cresca.xyz/docs/quickstart).

## Send email with Java: the basic request

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class SendEmail {
    public static void main(String[] args) throws Exception {
        String json = """
            {
              "from": "Acme <hello@acme.com>",
              "to": "ana@example.com",
              "subject": "Welcome to Acme",
              "html": "<p>Hi Ana, your account is ready.</p>"
            }
            """;

        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.cresca.xyz/api/v1/emails"))
            .timeout(Duration.ofSeconds(10))
            .header("Authorization", "Bearer " + System.getenv("CRESCA_API_KEY"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(json))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

Run it with `java SendEmail.java`. A successful send prints `200` and `{"id":"4ef9a417-..."}`. The text block keeps the JSON readable; for anything with user data, build the JSON with a library, as below, so quotes and newlines are escaped correctly.

## A reusable Java client with retries

The basic request is fine for a script. In production you want three more things, and `CrescaClient` below adds all of them:

- **An `Idempotency-Key` header.** If the connection drops after Cresca accepted the email, a retry with the same key returns the first result instead of sending a second email. Keys last 24 hours. Build them from the event, such as `welcome/user-123` or `invoice/inv-2026-0042`, never from the time or a random value. See [idempotency](https://www.cresca.xyz/docs/idempotency).
- **Retries only where they help.** Retry network errors, timeouts, `429 rate_limit_exceeded` and `5xx` with exponential backoff, and wait for the `retry-after` header when Cresca sends it. Do not retry other `4xx` errors: the same request fails the same way.
- **Errors you can branch on.** Every error body has a stable `name` and a readable `message`, for example `{"statusCode": 422, "name": "validation_error", "message": "..."}`. Branch on `name`, because messages can be reworded.

```java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;

/** A small client for the Cresca email API. Safe to share between threads. */
public class CrescaClient {
    private static final String API_URL = "https://api.cresca.xyz/api/v1";
    private static final int ATTEMPTS = 4;

    private final HttpClient http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
    private final ObjectMapper json = new ObjectMapper();
    private final String apiKey;

    public CrescaClient(String apiKey) {
        this.apiKey = apiKey;
    }

    public JsonNode post(String path, Object payload, String idempotencyKey)
            throws IOException, InterruptedException {
        HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(API_URL + path))
            .timeout(Duration.ofSeconds(10))
            .header("Authorization", "Bearer " + apiKey)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(json.writeValueAsString(payload)));
        if (idempotencyKey != null) {
            builder.header("Idempotency-Key", idempotencyKey);
        }
        HttpRequest request = builder.build();

        for (int attempt = 1; ; attempt++) {
            HttpResponse<String> response;
            try {
                response = http.send(request, HttpResponse.BodyHandlers.ofString());
            } catch (IOException e) {
                // Network error or timeout: the idempotency key makes a retry safe.
                if (attempt == ATTEMPTS) throw e;
                Thread.sleep(backoff(attempt));
                continue;
            }

            JsonNode body = parse(response.body());
            int status = response.statusCode();
            if (status >= 200 && status < 300) return body;

            String name = body.path("name").asText("unknown_error");
            boolean retryable = status >= 500
                || (status == 429 && !name.equals("monthly_quota_exceeded"))
                || name.equals("concurrent_idempotent_requests");
            if (!retryable || attempt == ATTEMPTS) {
                throw new CrescaException(status, name, body.path("message").asText(""));
            }
            long waitMillis = response.headers().firstValue("retry-after")
                .map(seconds -> Long.parseLong(seconds) * 1000)
                .orElse(backoff(attempt));
            Thread.sleep(waitMillis);
        }
    }

    private JsonNode parse(String body) {
        try {
            return json.readTree(body);
        } catch (IOException e) {
            return json.createObjectNode(); // a proxy error may have no JSON body
        }
    }

    // 1s, 2s, 4s... plus up to 250 ms of jitter.
    private static long backoff(int attempt) {
        return (1L << (attempt - 1)) * 1000 + ThreadLocalRandom.current().nextLong(250);
    }

    public static class CrescaException extends RuntimeException {
        public final int status;
        public final String name;

        CrescaException(int status, String name, String message) {
            super(status + " " + name + ": " + message);
            this.status = status;
            this.name = name;
        }
    }
}
```

Using it:

```java
CrescaClient cresca = new CrescaClient(System.getenv("CRESCA_API_KEY"));

JsonNode sent = cresca.post("/emails", Map.of(
    "from", "Acme <hello@acme.com>",
    "to", List.of(user.email()),
    "subject", "Welcome to Acme",
    "html", "<p>Hi " + HtmlUtils.htmlEscape(user.firstName()) + ", your account is ready.</p>"
), "welcome/" + user.id());

System.out.println(sent.get("id").asText());
```

Escape user input before it goes into HTML (`HtmlUtils` is Spring's; OWASP's encoder or Apache Commons Text work too). Templates escape variables for you.

## Send a template with variables

Instead of building HTML in your code, you can design the email in Cresca's template editor, mark the parts your app fills in with triple braces such as `{{{RESET_LINK}}}`, and publish it. Your app then sends only the template alias and the values. Sends always use the *published* version, so you can edit a draft without touching live email. Values are HTML-escaped, so a user's name can never change your markup. A missing variable with no fallback returns `422 validation_error` naming it. More in [templates and variables](https://www.cresca.xyz/docs/templates).

```java
cresca.post("/emails", Map.of(
    "to", List.of(user.email()),
    "template", Map.of(
        "id", "password-reset", // the template's alias or id
        "variables", Map.of("NAME", user.firstName(), "RESET_LINK", resetUrl)
    )
), "password-reset/" + resetRequestId);
```

## Add attachments

Attachments are sent inline as base64: up to 20 files and 7 MB in total per email. Each item has `filename`, `content` and optionally `content_type`. Set `content_id` to show an inline image with `<img src="cid:logo">`. Executable types such as .exe and .js are refused, and attaching by URL is not supported yet.

```java
byte[] pdf = Files.readAllBytes(Path.of("invoices/inv-2026-0042.pdf"));

cresca.post("/emails", Map.of(
    "from", "Acme Billing <billing@acme.com>",
    "to", List.of(customer.email()),
    "subject", "Your invoice INV-2026-0042",
    "html", "<p>Thanks for your payment. Your invoice is attached.</p>",
    "attachments", List.of(Map.of(
        "filename", "inv-2026-0042.pdf",
        "content", Base64.getEncoder().encodeToString(pdf),
        "content_type", "application/pdf"
    ))
), "invoice/inv-2026-0042");
```

## Send a batch of up to 100 emails

`POST /emails/batch` takes a JSON array of up to 100 emails, each with the same fields as a single send except `attachments` and `scheduled_at`. It is all or nothing: if one email is invalid, nothing is sent and the error message names the failing email by its zero-based index. The response is `{"data": [{"id": "..."}, ...]}` in the same order. One request counts as one request against your rate limit, so batching is how you send thousands of emails per minute.

```java
List<Map<String, Object>> emails = users.stream()
    .map(u -> Map.<String, Object>of(
        "from", "Acme <hello@acme.com>",
        "to", List.of(u.email()),
        "subject", "Your weekly summary",
        "html", renderSummary(u)))
    .toList();

for (int i = 0; i < emails.size(); i += 100) {
    List<Map<String, Object>> chunk = emails.subList(i, Math.min(i + 100, emails.size()));
    JsonNode result = cresca.post("/emails/batch", chunk, "weekly-summary/2026-10-27/" + (i / 100));
    System.out.printf("Batch %d: %d emails accepted%n", i / 100, result.get("data").size());
}
```

## Use it in Spring Boot

Register one client as a bean, keep the key in configuration (`cresca.api-key`, set from the `CRESCA_API_KEY` environment variable), and send from an `@Async` method or a queue listener so web requests never wait on email:

```java
@Configuration
class CrescaConfig {
    @Bean
    CrescaClient crescaClient(@Value("${cresca.api-key}") String apiKey) {
        return new CrescaClient(apiKey);
    }
}

@Service
class WelcomeEmailService {
    private final CrescaClient cresca;

    WelcomeEmailService(CrescaClient cresca) {
        this.cresca = cresca;
    }

    @Async
    public void send(User user) throws Exception {
        cresca.post("/emails", Map.of(
            "to", List.of(user.email()),
            "template", Map.of("id", "welcome", "variables", Map.of("NAME", user.firstName()))
        ), "welcome/" + user.id());
    }
}
```

`@Async` needs `@EnableAsync` on a configuration class.

## Jakarta Mail or an email API?

Jakarta Mail and Spring's `JavaMailSender` are mature SMTP clients and the right tool if you run a mail server. An API replaces sessions, transports and SMTP credentials with one HTTP call, gives you JSON errors with a stable `name`, and makes retries safe with idempotency keys. Cresca does not offer an SMTP relay yet, so with Cresca you use the API as shown here.

## Errors to handle when you send email with Java

These are the errors a sending app actually meets. The full list is on the [errors page](https://www.cresca.xyz/docs/errors).

| Status | name | What it means | Retry? |
| --- | --- | --- | --- |
| 403 | `invalid_from_address` | The from address is not an active sender on a verified domain. | No, fix the sender |
| 422 | `validation_error` | A field is invalid, or a template variable is missing. | No, fix the request |
| 422 | `missing_required_field` | No to, from or subject (and no template that supplies them). | No |
| 429 | `rate_limit_exceeded` | More requests per second than your plan allows. | Yes, after retry-after |
| 429 | `monthly_quota_exceeded` | The plan's email allowance is used up. | No |
| 409 | `concurrent_idempotent_requests` | The same Idempotency-Key is still being processed. | Yes, shortly |
| 5xx | `application_error` | A problem on Cresca's side. | Yes, with backoff |

Recipients on your suppression list (earlier hard bounces, complaints and unsubscribes) are skipped, but the request still succeeds. See [suppression](https://www.cresca.xyz/docs/suppression).

## Check that the email was delivered

A `200` response means Cresca accepted the email, not that it reached the inbox. Open [Transactional](https://app.cresca.xyz/transactional) in the app to see each email's timeline, or look it up with a full-access key:

```bash
curl https://api.cresca.xyz/api/v1/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c \
  -H "Authorization: Bearer $CRESCA_API_KEY"
```

The `last_event` field moves from `queued` to `sent` and then `delivered` or `bounced`. See [email lifecycle](https://www.cresca.xyz/docs/email-lifecycle) for every status.

## Frequently asked questions

### How do I send email in Java without SMTP?

Use an email API with the HttpClient built into Java 11 and later. With Cresca you POST a JSON body with from, to, subject and html to https://api.cresca.xyz/api/v1/emails with your API key as a bearer token. There is no mail session, transport or SMTP password.

### Is there an official Cresca SDK for Java?

No, and you do not need one. The API is plain HTTPS and JSON, so the HTTP client you already use in Java works. The helper in this tutorial is about 40 lines you can copy into your project and own.

### Does this work with Kotlin?

Yes. CrescaClient is plain Java and can be called from Kotlin as it is, or you can port it to Ktor's client or OkHttp. The request, headers and retry rules are the same in any JVM language.

### How do I send HTML email in Java?

Put the HTML in the html field of the JSON body. Cresca creates the plain-text part automatically unless you send your own text field. Build the JSON with Jackson or another library rather than by string concatenation, so quotes and special characters are escaped correctly.

### How many emails can I send per request?

One email can have up to 50 recipients across to, cc and bcc, and a batch request carries up to 100 emails. Requests per second depend on your plan: 2 on Free, 10 on Professional, 20 on Premium, 50 on Ultra and 100 on Enterprise. Each recipient counts as one email against the monthly allowance.

### Is Cresca free to try?

Yes. The Free plan gives you 30 days, 50 contacts and 50 emails with no credit card, which is enough to build and test your integration. API email shares your plan's email allowance with campaigns; paid plans are on the pricing page.

## The same tutorial in other languages

[Node.js](https://www.cresca.xyz/blog/send-email-nodejs) · [Python](https://www.cresca.xyz/blog/send-email-python) · [PHP](https://www.cresca.xyz/blog/send-email-php) · [Go](https://www.cresca.xyz/blog/send-email-golang) · [Ruby](https://www.cresca.xyz/blog/send-email-ruby) · [C# and .NET](https://www.cresca.xyz/blog/send-email-csharp) · [Next.js](https://www.cresca.xyz/blog/send-email-nextjs) · [Laravel](https://www.cresca.xyz/blog/send-email-laravel) · [Django](https://www.cresca.xyz/blog/send-email-django) · [Ruby on Rails](https://www.cresca.xyz/blog/send-email-rails)

For every field of the Emails API with cURL examples, read the [send email API guide](https://www.cresca.xyz/blog/send-email-api). For the big picture, start with the [transactional email API overview](https://www.cresca.xyz/blog/transactional-email-api). Still deciding how to send? Read [SMTP vs API](https://www.cresca.xyz/blog/smtp-vs-api-sending).

## Start sending

Verify a domain, create a sending-access key and send your first email with Java in a few minutes. **[Try Cresca free for 30 days](https://app.cresca.xyz/signup)**, no credit card required, then follow the [quickstart](https://www.cresca.xyz/docs/quickstart) or browse the [API docs](https://www.cresca.xyz/docs).
