PDFPipe

Guide

HTML to PDF in Kotlin

Generating PDFs on the JVM typically means pulling in heavyweight libraries like Flying Saucer, OpenPDF, or iText, none of which support modern CSS or JavaScript. The alternative is spinning up a headless browser process from your Kotlin service, which adds deployment complexity and consumes significant memory. PDFPipe is a single HTTP call that returns a high-quality PDF rendered by a real browser engine, with nothing extra to install in your service.

OkHttp (no framework)

If you are using plain Kotlin or a lightweight stack, OkHttp is the simplest client:

kotlin
// build.gradle.kts
// implementation("com.squareup.okhttp3:okhttp:4.12.0")
// implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0")

import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File

val JSON = "application/json; charset=utf-8".toMediaType()
val client = OkHttpClient()
val mapper = jacksonObjectMapper()

fun generatePdf(html: String, outputFile: File) {
    val body = mapper.writeValueAsString(
        mapOf("html" to html, "options" to mapOf("format" to "A4"))
    ).toRequestBody(JSON)

    val request = Request.Builder()
        .url("https://api.pdfpipe.xyz/v1/pdf")
        .addHeader("Authorization", "Bearer ${System.getenv("PDFPIPE_API_KEY")}")
        .post(body)
        .build()

    client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) error("PDF generation failed: ${response.code}")
        outputFile.writeBytes(response.body!!.bytes())
    }
}

Spring Boot with WebClient

In a Spring Boot service, WebClient gives you a non-blocking reactive call. The PDF bytes stream back as a ByteArray that you return directly as the response body:

kotlin
// Spring Boot controller with WebClient (reactive)
// implementation("org.springframework.boot:spring-boot-starter-webflux")

import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono

@RestController
class PdfController(private val webClient: WebClient) {

    @PostMapping("/invoice/{orderId}.pdf")
    fun invoice(@PathVariable orderId: String): Mono<ResponseEntity<ByteArray>> {
        val html = """
            <!DOCTYPE html>
            <html>
            <body style="font-family: system-ui; padding: 40px">
              <h1>Invoice #$orderId</h1>
              <p>Thank you for your order.</p>
            </body>
            </html>
        """.trimIndent()

        return webClient.post()
            .uri("https://api.pdfpipe.xyz/v1/pdf")
            .header("Authorization", "Bearer ${System.getenv("PDFPIPE_API_KEY")}")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(mapOf("html" to html, "options" to mapOf("format" to "A4")))
            .retrieve()
            .bodyToMono(ByteArray::class.java)
            .map { bytes ->
                ResponseEntity.ok()
                    .header(
                        HttpHeaders.CONTENT_DISPOSITION,
                        "attachment; filename="invoice-$orderId.pdf""
                    )
                    .contentType(MediaType.APPLICATION_PDF)
                    .body(bytes)
            }
    }
}

Ktor with coroutines

If you prefer coroutines with Ktor, the call is a clean suspend function:

kotlin
// Coroutine-friendly version with Ktor client
// implementation("io.ktor:ktor-client-core:${ktor_version}")
// implementation("io.ktor:ktor-client-okhttp:${ktor_version}")
// implementation("io.ktor:ktor-client-content-negotiation:${ktor_version}")
// implementation("io.ktor:ktor-serialization-kotlinx-json:${ktor_version}")

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable

@Serializable
data class PdfRequest(
    val html: String,
    val options: Map<String, String> = mapOf("format" to "A4"),
)

val httpClient = HttpClient(OkHttp) {
    install(ContentNegotiation) { json() }
}

suspend fun generatePdf(html: String): ByteArray {
    val response = httpClient.post("https://api.pdfpipe.xyz/v1/pdf") {
        header("Authorization", "Bearer ${System.getenv("PDFPIPE_API_KEY")}")
        contentType(ContentType.Application.Json)
        setBody(PdfRequest(html = html))
    }
    if (!response.status.isSuccess()) error("PDF generation failed: ${response.status}")
    return response.body()
}

Getting an API key

The Hobby plan gives you 500 free documents a month with no credit card. Paid plans start at $19 and include email support, a longer archive window, and higher monthly limits. Set the key as an environment variable (PDFPIPE_API_KEY) and deploy normally.

Full API reference on the docs page. Try it now in the live playground with no signup.

Get an API key →