1. Home
  2. API docs
  3. Spring Boot

Validate email addresses in Spring Boot

In Spring Boot, create a custom @DeliverableEmail constraint whose ConstraintValidator calls the SimpleVerifier API with RestClient and returns false only when the status is invalid. Put it next to @NotBlank and @Email on your request DTO, inject the key with @Value, and return true when the call fails.

By SimpleVerifier Team

Before you start

  • Create a key on the API keys page. Keys start with sv_live_ and are shown once.
  • Map the environment variable in application.properties and Spring injects it into the validator's constructor.
  • API access is included in the Pro plan ($29.99/month) and the Email + Phone plan ($49.99/month). See pricing.

1. Declare the constraint

A standard Jakarta Bean Validation annotation.

DeliverableEmail.java
// DeliverableEmail.java
package com.example.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.RECORD_COMPONENT})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DeliverableEmailValidator.class)
public @interface DeliverableEmail {
    String message() default "This email address cannot receive mail. Please check it.";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

2. Write the validator

Spring Boot creates validators through the application context, so constructor injection with @Value works. RestClient needs Spring Boot 3.2 or later. The {email} URI variable is encoded for you, including + in plus addresses.

DeliverableEmailValidator.java
// DeliverableEmailValidator.java — Spring Boot 3.2+ (RestClient)
package com.example.validation;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.time.Duration;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;

public class DeliverableEmailValidator implements ConstraintValidator<DeliverableEmail, String> {
    private static final Logger log = LoggerFactory.getLogger(DeliverableEmailValidator.class);
    private static final Pattern EMAIL_SHAPE = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");

    private final RestClient client;

    public DeliverableEmailValidator(@Value("${simpleverifier.key}") String apiKey) {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(Duration.ofSeconds(5));
        factory.setReadTimeout(Duration.ofSeconds(30));
        this.client = RestClient.builder()
                .baseUrl("https://simpleverifier.com/api/v1")
                .defaultHeader("Authorization", "Bearer " + apiKey)
                .requestFactory(factory)
                .build();
    }

    @Override
    public boolean isValid(String email, ConstraintValidatorContext context) {
        if (email == null || !EMAIL_SHAPE.matcher(email).matches()) {
            return true; // leave empty or malformed input to @NotBlank and @Email
        }
        try {
            VerifyResult result = client.get()
                    .uri("/verify?email={email}", email)
                    .retrieve()
                    .body(VerifyResult.class);
            return result == null || !"invalid".equals(result.status());
        } catch (RestClientException e) {
            log.warn("SimpleVerifier unavailable, accepting without a mailbox check: {}", e.getMessage());
            return true; // timeout, 429, 503: fail open
        }
    }

    record VerifyResult(
            String email,
            String status,
            String reason,
            @JsonProperty("mailbox_checked") boolean mailboxChecked) {}
}

3. Set the key

Spring resolves ${SIMPLEVERIFIER_KEY} from the environment at startup.

src/main/resources/application.properties
# application.properties
simpleverifier.key=${SIMPLEVERIFIER_KEY}

4. Annotate the request

Add spring-boot-starter-validation if your project doesn't have it yet; @Valid won't run without it.

SignupController.java
// SignupController.java
package com.example.signup;

import com.example.validation.DeliverableEmail;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SignupController {

    public record SignupRequest(@NotBlank @Email @DeliverableEmail String email) {}

    @PostMapping("/signup")
    public ResponseEntity<Void> signup(@Valid @RequestBody SignupRequest request) {
        // Save the signup here. An undeliverable address never reaches this line:
        // validation fails first and Spring returns 400 Bad Request.
        return ResponseEntity.status(201).build();
    }
}

What the API returns

GET or POST https://simpleverifier.com/api/v1/verify with email runs the syntax, domain, disposable-domain and live mailbox checks in one call and returns:

{
  "email": "jane@company.com",
  "status": "valid",
  "reason": "Mailbox exists",
  "mailbox_checked": true
}

Every snippet above follows the same rule: only a successful response with status set to invalid blocks a person.

ResultWhat it meansWhat to do
validThe mail server confirmed the mailbox exists.Accept.
riskyThe domain is real but the mailbox couldn't be confirmed: catch-all domains, role addresses like info@, providers that hide mailbox status.Accept on signup forms. Store the status and keep these out of cold campaigns.
invalidBad format, no mail server, a disposable domain, or the mailbox doesn't exist.Reject and ask the person to check the address.
timeout, 429 or 503The check didn't finish: a slow mail server, too many calls this minute, or a brief outage.Accept (fail open) and log it. Re-check later if it matters.
401 or 402The key is wrong or revoked, or the subscription isn't active.Accept, and alert yourself: every check is being skipped until it's fixed.

Errors return JSON with an error message. Each key can make 120 calls a minute; for thousands of addresses, upload a CSV in the dashboard instead.

Phone numbers and spreadsheets

The same key works with /api/v1/phone?phone=…&country=… to validate a phone number and normalise it to E.164, with live carrier lookups on the Email + Phone plan. The request and response are in the API reference. To check a column of addresses without code, use the Google Sheets add-on with the same key.

Common questions

Does @Email already check whether the address exists?
No. Jakarta's @Email only checks the format, and it accepts addresses without a top-level domain. @DeliverableEmail adds the domain, disposable-domain and mailbox checks.
Can I use WebClient instead of RestClient?
Bean Validation is synchronous, so a reactive call would have to block anyway. RestClient (or RestTemplate on older Spring versions) is the straightforward choice inside a ConstraintValidator.
How much does the SimpleVerifier API cost?
API access is included in the paid plans: Pro at $29.99/month and Email + Phone at $49.99/month. There are no per-call credits. Each key can make 120 calls a minute.
Should I reject risky addresses?
Not on a signup or contact form. Risky means the mailbox couldn't be confirmed, which includes every address at a catch-all company domain. Accept it, store the status, and leave risky addresses out of cold outreach.
What if the API is slow or down?
Let the address through. A mailbox check can take several seconds when a mail server is slow, so cap the wait at about 30 seconds (less on a busy form) and treat a timeout, a 429 or a 503 as unknown rather than invalid.

Ready to clean your email and phone lists?

Start verifying emails and phone numbers today and boost your deliverability.

Get Started

Cancel anytime • No setup required