← Back to all posts
2026-09-01 · MailAnvil Team

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

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

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.