Send Transactional Email with Kotlin + MailAnvil — No HTTP Library Needed
Kotlin runs the same JVM as Java, which means you already have a production-grade HTTP client in the JDK — no Ktor, no OkHttp, no Retrofit. MailAnvil is a plain HTTPS API, so sending a transactional email is one HttpRequest away.
This tutorial builds a Kotlin function that sends a welcome email when a user signs up, using only java.net.http.HttpClient.
Why no HTTP library?
HttpClient shipped in the JDK 11+ and handles HTTP/2, TLS, redirects, and timeouts out of the box. For a single POST /v1/send call, pulling in Ktor or OkHttp is dependency weight you don't need. The fewer moving parts, the fewer things to break in production email code.
Prerequisites
- JDK 17+ (Kotlin compiles to JVM bytecode; any recent JDK works)
- A MailAnvil API key (starts with
re_) — get one at mailanvil.com
Step 1 — The send function
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.security.MessageDigest
object MailAnvil {
private val client: HttpClient = HttpClient.newHttpClient()
private const val API_KEY = "re_your_api_key"
private const val ENDPOINT = "https://api.mailanvil.com/v1/send"
fun sendWelcomeEmail(to: String, name: String): Int {
val body = """
{
"from": "MailAnvil <[email protected]>",
"to": ["$to"],
"subject": "Welcome to MailAnvil, $name",
"html": "<h1>Welcome, $name!</h1><p>Your account is ready to send.</p>"
}
""".trimIndent()
val request = HttpRequest.newBuilder()
.uri(URI.create(ENDPOINT))
.header("Authorization", "Bearer $API_KEY")
.header("Content-Type", "application/json")
.header("Idempotency-Key", idempotencyKey(to))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build()
return client.send(request, HttpResponse.BodyHandlers.ofString()).statusCode()
}
}
Step 2 — Deterministic idempotency key
The Idempotency-Key header is what keeps your email from double-sending on a retry. It must be deterministic — same recipient + same intent = same key. Never use a timestamp here, or every retry looks like a new email.
fun idempotencyKey(to: String): String =
MessageDigest.getInstance("SHA-256")
.digest("welcome:$to".toByteArray())
.joinToString("") { "%02x".format(it) }
MailAnvil remembers the key for a window and returns the original result instead of sending a duplicate.
Step 3 — Sending it
fun main() {
val status = MailAnvil.sendWelcomeEmail("[email protected]", "Budi")
check(status == 202) { "MailAnvil returned HTTP $status" }
println("Welcome email accepted")
}
202 Accepted means MailAnvil queued the email. The actual delivery happens asynchronously — that's the correct contract for transactional email, because it lets your API respond instantly.
What about async?
HttpClient.send() blocks the calling thread. On the JVM, run it in a coroutine with Dispatchers.IO, or use sendAsync():
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenAccept { resp -> println("Accepted: ${resp.statusCode()}") }
Why MailAnvil
- IDR pricing + QRIS/GoPay — bill in Rupiah, not USD markup
- Bahasa docs + support — no translation layer for your team
- Cloudflare Workers native — the same API you'd call from Kotlin also runs at the edge
- MCP-native — the API is discoverable by AI agents, not just your JVM app
Next steps
Swap the hardcoded to/name for real signup data, add retry with exponential backoff, and wire the 202 check into your logging. The full API reference is at docs.mailanvil.com.
No Ktor, no OkHttp, no SMTP server — just the JDK you already have.