← Back to blog
·7 min read·
Vanessa LozzardoVanessa Lozzardo

How to send email with Spring Boot using Sendkit

Integrate Sendkit into your Spring Boot application with the Java SDK. Covers configuration, services, async sending, and error handling.

spring-bootjavaemail-apitutorial
How to send email with Spring Boot using Sendkit

Spring Boot makes most integrations painless, but email is one of those things that always feels like more work than it should be. Spring Mail with JavaMailSender works, but you're dealing with SMTP sessions, MimeMessage builders, and zero visibility into what happens after the message leaves your server.

The Sendkit email API replaces all of that with a single method call. This guide covers the full integration: adding the SDK, configuring it properly, building a service layer, sending asynchronously, handling errors, and setting up a webhook endpoint to track delivery events.

If you just need plain Java without Spring, there's a separate guide on sending email with Java.

Add the dependency

The Sendkit Java SDK requires Java 11+. Add it to your build file.

Maven:

<dependency>
    <groupId>dev.sendkit</groupId>
    <artifactId>sendkit</artifactId>
    <version>1.0.0</version>
</dependency>

Gradle:

implementation 'dev.sendkit:sendkit:1.0.0'

The SDK source is on GitHub if you want to read through it. It's a thin wrapper around the REST API with no transitive dependencies that conflict with Spring Boot's managed versions.

Configure the API key

Add your API key to application.properties:

sendkit.api-key=${SENDKIT_API_KEY}

Or in application.yml:

sendkit:
  api-key: ${SENDKIT_API_KEY}

Pull the actual key from your Sendkit dashboard. Keys starting with sk_live_ send real email. Keys starting with sk_test_ validate everything but don't deliver, which is exactly what you want in your test environment.

Never hardcode the key in your source files. Use environment variables or Spring's externalized configuration (Vault, AWS Secrets Manager, whatever your team already uses).

Create an EmailService bean

Wrap the SDK in a Spring @Service so the rest of your application doesn't couple directly to the Sendkit client:

import dev.sendkit.Sendkit;
import dev.sendkit.SendkitException;
import dev.sendkit.SendEmailParams;
import dev.sendkit.SendEmailResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class EmailService {

    private final Sendkit client;

    public EmailService(@Value("${sendkit.api-key}") String apiKey) {
        this.client = new Sendkit(apiKey);
    }

    public String send(String from, String to, String subject, String html) {
        SendEmailParams params = SendEmailParams.builder()
            .from(from)
            .to(to)
            .subject(subject)
            .html(html)
            .build();

        SendEmailResponse response = client.emails().send(params);
        return response.getId();
    }
}

The Sendkit client is thread-safe, so a single instance shared across requests is fine. Spring creates the bean once and injects it wherever needed.

Developer writing code on a laptop screen

Send from a controller

Inject EmailService into a @RestController and call it from an endpoint:

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/emails")
public class EmailController {

    private final EmailService emailService;

    public EmailController(EmailService emailService) {
        this.emailService = emailService;
    }

    @PostMapping("/welcome")
    public ResponseEntity<String> sendWelcome(@RequestBody WelcomeRequest request) {
        String html = "<h1>Welcome, " + request.name() + "</h1>"
            + "<p>Your account is ready. Get started at "
            + "<a href=\"https://app.yoursite.com\">your dashboard</a>.</p>";

        String messageId = emailService.send(
            "[email protected]",
            request.email(),
            "Welcome to YourApp",
            html
        );

        return ResponseEntity.ok(messageId);
    }

    public record WelcomeRequest(String name, String email) {}
}

This works, but there's a problem. The HTTP request blocks until the Sendkit API responds. For a welcome email, that's unnecessary. The user doesn't need to wait for the email to be queued before seeing a success page.

Async sending with @Async

Move email sending off the request thread. First, enable async support in your application:

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

@Configuration
@EnableAsync
public class AsyncConfig {
}

Then mark the send method as @Async in your service:

import org.springframework.scheduling.annotation.Async;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class EmailService {

    private static final Logger log = LoggerFactory.getLogger(EmailService.class);
    private final Sendkit client;

    public EmailService(@Value("${sendkit.api-key}") String apiKey) {
        this.client = new Sendkit(apiKey);
    }

    @Async
    public void sendAsync(String from, String to, String subject, String html) {
        try {
            SendEmailParams params = SendEmailParams.builder()
                .from(from)
                .to(to)
                .subject(subject)
                .html(html)
                .build();

            SendEmailResponse response = client.emails().send(params);
            log.info("Email sent: {}", response.getId());
        } catch (SendkitException e) {
            log.error("Failed to send email to {}: {}", to, e.getMessage());
        }
    }
}

Now your controller returns immediately while the email sends in a background thread. The tradeoff is that the caller doesn't get the message ID back. For most transactional emails (welcome, password reset, notifications), that's perfectly fine. If you need the ID for tracking, use a CompletableFuture<String> return type instead of void.

One thing to watch: Spring's default @Async executor uses a SimpleAsyncTaskExecutor, which creates an unbounded number of threads. Configure a proper ThreadPoolTaskExecutor in production so a spike in email volume doesn't exhaust your server.

Error handling with SendkitException

The SDK throws SendkitException for API errors. Handle different status codes accordingly:

public String sendWithErrorHandling(String from, String to,
                                     String subject, String html) {
    try {
        SendEmailParams params = SendEmailParams.builder()
            .from(from)
            .to(to)
            .subject(subject)
            .html(html)
            .build();

        SendEmailResponse response = client.emails().send(params);
        return response.getId();

    } catch (SendkitException e) {
        if (e.getStatusCode() == 422) {
            log.warn("Invalid email params: {}", e.getMessage());
            throw new IllegalArgumentException("Invalid email: " + e.getMessage());
        }
        if (e.getStatusCode() == 429) {
            log.warn("Rate limited by Sendkit");
            throw new RuntimeException("Email rate limited, retry later");
        }
        if (e.getStatusCode() >= 500) {
            log.error("Sendkit server error: {}", e.getStatusCode());
            throw new RuntimeException("Email service unavailable");
        }
        throw new RuntimeException("Email send failed: " + e.getMessage());
    }
}

A 422 means bad input: invalid address format, missing required field, unverified sender domain. A 429 means you've hit the rate limit. A 5xx means something is wrong on Sendkit's side. Only retry on 429 and 5xx. Retrying a 422 will fail every time.

For production, add retry logic with exponential backoff. Spring Retry (@Retryable) works well here if you already have it in your stack.

Servers in a data center

Webhook endpoint for delivery events

Sending email is half the job. You also need to know what happened after delivery. Sendkit sends webhook events for deliveries, opens, clicks, bounces, and complaints. Set up an endpoint to receive them:

@RestController
@RequestMapping("/api/webhooks")
public class WebhookController {

    private static final Logger log = LoggerFactory.getLogger(WebhookController.class);

    @PostMapping("/sendkit")
    public ResponseEntity<Void> handleWebhook(@RequestBody Map<String, Object> payload) {
        String type = (String) payload.get("type");
        String emailId = (String) payload.get("email_id");
        String recipient = (String) payload.get("recipient");

        switch (type) {
            case "email.delivered" -> log.info("Delivered to {}", recipient);
            case "email.bounced" -> {
                log.warn("Bounce from {}: {}", recipient, payload.get("reason"));
                // Remove address from your mailing list
            }
            case "email.complained" -> {
                log.warn("Spam complaint from {}", recipient);
                // Unsubscribe immediately
            }
            case "email.opened" -> log.info("Opened by {}", recipient);
            case "email.clicked" -> log.info("Link clicked by {}", recipient);
            default -> log.debug("Unhandled event: {}", type);
        }

        return ResponseEntity.ok().build();
    }
}

Always return 200 quickly. Process heavy work (database updates, notifications) asynchronously. If your endpoint is slow or returns errors, webhook delivery will retry and eventually stop.

Bounces and complaints are the ones that matter most. Ignoring bounces tanks your sender reputation. Ignoring complaints gets you blocked. Read the email deliverability guide if you want the full picture on reputation management.

Register your webhook URL in the Sendkit dashboard or via the API docs.

SMTP alternative with Spring Mail

If you'd rather use Spring's built-in JavaMailSender instead of the SDK, point it at Sendkit's SMTP relay:

spring.mail.host=smtp.sendkit.com
spring.mail.port=587
spring.mail.username=sendkit
spring.mail.password=${SENDKIT_API_KEY}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

Then use JavaMailSender as usual:

@Service
public class SmtpEmailService {

    private final JavaMailSender mailSender;

    public SmtpEmailService(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    public void send(String from, String to, String subject, String text) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(from);
        message.setTo(to);
        message.setSubject(subject);
        message.setText(text);
        mailSender.send(message);
    }
}

Your API key doubles as the SMTP password. You still get delivery tracking, bounce handling, and analytics through the Sendkit dashboard.

The API approach is better for new projects. It's faster (no SMTP handshake), returns structured responses, and gives you a message ID immediately. But SMTP is the right call when you're migrating an existing Spring Boot app that already uses JavaMailSender everywhere and you don't want to rewrite every email call.

What to do next

The code is the easy part. Keeping your email out of spam folders requires ongoing attention.

Authenticate your domain. DKIM, SPF, and DMARC are mandatory. Without them, Gmail and Outlook will either reject your messages or bury them in junk. The DMARC/DKIM/SPF setup guide covers the DNS records.

Monitor your bounce rate. Sendkit auto-suppresses bounced addresses, but track the numbers yourself too. A sudden spike usually means a list quality problem.

Validate addresses at signup. Catching gmial.com typos and disposable addresses before they enter your database saves you from sending to dead mailboxes. Sendkit has built-in validation for this.

Check the Sendkit docs for templates, batch sending, and advanced configuration. Pricing is usage-based with no minimum commitment.

Share this article