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
1. Declare the constraint
A standard Jakarta Bean Validation annotation.
// 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 — 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.
# 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
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.
| Result | What it means | What to do |
|---|---|---|
| valid | The mail server confirmed the mailbox exists. | Accept. |
| risky | The 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. |
| invalid | Bad 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 503 | The 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 402 | The 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.