com.nuntly:nuntly-java-spring, which registers a Nuntly client bean. The starter requires Spring Boot 3.4 or later. The samples use the SDK’s exceptions directly and catch them with try/catch.
Prerequisites
- A Nuntly account. The free plan includes 3,000 emails per month (100 per day) and needs no credit card. See account setup.
- A verified sending domain. See sending domains.
- An API key with the Sending only permission, exported as an environment variable. See API keys. Keep it out of
application.propertiesand out of Git. - Java 21 or later. The SDK uses records, sealed interfaces, and pattern matching for
switch.
Install and configure the Spring Boot starter
1
Create a Spring Boot project
Generate a project with Spring Web and Java 21 from Spring Initializr, or from the command line:
2
Add the starter
The starter pulls the core SDK,
com.nuntly:nuntly-java, as a transitive dependency. Check Maven Central for the latest version.3
Set the API key
Add one line to
src/main/resources/application.properties. Spring resolves the placeholder from the environment at startup.src/main/resources/application.properties
nuntly prefix. All of them except the API key are optional:
If your app already sends with
JavaMailSender and you want to keep it, point spring-boot-starter-mail at the Nuntly SMTP server instead. The samples use the API through the SDK, which returns the email ID, sends an idempotency key, and raises one exception type per HTTP status.Send your first email
Put the send in a@Service so your signup code can call it. Create src/main/java/com/example/demo/WelcomeEmailService.java:
WelcomeEmailService.java
Nuntly bean through the constructor. Use a from address on your verified domain. Sending both html and text gives mail clients that block HTML a readable fallback.
To try it before wiring it into your signup flow, expose it in src/main/java/com/example/demo/WelcomeController.java:
WelcomeController.java
{"id":"em_01k...","status":"queued"}. The API answers 202 Accepted: queued means Nuntly accepted the email, and delivery events follow. Keep the em_ ID: every later event about this email carries it.
Observe what happened
Email timeline in the dashboard
Open Emails in the dashboard, open the email you sent, and click Email Timeline to see each event with its timestamp. The Events tab of the observability dashboard shows the same events for your whole organization. A webhook can subscribe to these events:
You enable open and click tracking per domain. See enable open and click tracking.
Receive webhooks in a Spring Boot controller
1
Create the webhook
In the dashboard, go to Webhooks and click Create Webhook. Enter your public HTTPS URL ending in
/webhooks/nuntly and select at least email.delivered, email.bounced, and email.complained. See webhooks for every option.2
Store the signing secret
Copy the signing secret, which starts with
whsec_, and export it next to your API key:3
Add the controller
Create
src/main/java/com/example/demo/NuntlyWebhookController.java:NuntlyWebhookController.java
- The body stays a
String. The signature covers the exact bytes Nuntly sent. Parsing the body into an object first and serializing it again can change those bytes and break verification. Webhook.verifyruns the full check. It parses thewebhook-signatureheader (t=<unix seconds>,v0=<hex>) and computes an HMAC-SHA256 of<timestamp>.<body>with your secret. It compares the result in constant time and rejects timestamps older than 5 minutes. When the header carries two signatures, for example during a secret rotation, either one passes. On failure it throwsWebhookVerificationError, which the controller turns into a401.- The result is a sealed type.
WebhookEventhas one record per event type, so aswitchgives you typed access toe.data(). Event types added after your SDK version arrive asUnknownWebhookEventand fall intodefault.
Email em_... delivered appears in your logs once the recipient’s server accepts it.
In production:
- Return
200first and move slow work, such as database updates, off the request thread. - Deduplicate on
event.id()(it starts withevt_): Nuntly retries failed deliveries, so an event can arrive more than once. - On
email.bouncedoremail.complained, stop sending to that address in your own data. - If deliveries keep failing, Nuntly disables the webhook and emails your organization owner. Once your endpoint is fixed, replay missed events from the webhooks observability tab.
Production notes
Idempotency keys
When you pass no key,emails().send() generates a UUID v4 and reuses it across its own automatic retries, so a timeout followed by a retry never sends the email twice. Each call gets a new key, so a job that runs twice or a form submitted twice makes two calls and sends two emails.
Derive the key from the business event to cover those cases:
409 idempotency_key_payload_mismatch, raised as ConflictError. See idempotency keys for key strategies.
Retries
By default, the client makes up to 3 attempts per request (the first one plus 2 retries) with a 60-second timeout each. It retries network errors and HTTP408, 409, 429, 500, 502, 503, and 504, and waits for the Retry-After header on a 429. A send called from a web request blocks that request thread. Set a shorter timeout for that case:
src/main/resources/application.properties
Error handling
Every API error extendsApiError, with one subclass per status: BadRequestError, AuthenticationError, PermissionDeniedError, NotFoundError, ConflictError, UnprocessableEntityError, RateLimitError, and InternalServerError. Network failures that outlast the retries throw ConnectionError. Both extend NuntlyError, a RuntimeException.
Keep the signup successful when the welcome email fails. Catch the error, log identifiers, and continue:
e.requestId() reads the X-Request-Id header of the response. Include it when you contact support.
Separate environments
Create one API key per environment with the Sending only permission, and restrict the production key to your production domain. Give each environment its own webhook endpoint, which also gives it its own signing secret. Because both values come from environment variables, the sameapplication.properties works in every environment.
What not to log
Recipient addresses are personal data. Log identifiers instead: theem_ email ID, the evt_ event ID, and the request ID. The controllers above never print e.data().to(), the subject, or the raw payload. When you need the recipient of an email, look it up in the dashboard by its ID.
The SDK debug mode follows the same rule: nuntly.debug=true prints the method, URL, status, and request ID, never request or response bodies.
Next steps
Handle webhook events
Signature verification details and webhook best practices
Sending events
Payload reference for every sending event
Idempotency keys
Key formats, strategies, and error codes
SDK reference
Install and configure the Nuntly SDKs
Other frameworks
Send email from Express
Send from an Express route and verify webhooks on the raw body
Send email from NestJS
Inject the client as a provider and send from a service
Send email from Next.js
Send from a Server Action and receive webhooks in a Route Handler
