DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

Screenshot API for Kotlin: Screenshots and PDFs Without a JVM Browser

Screenshot API for Kotlin: Screenshots and PDFs Without a JVM Browser

Generating a PDF from HTML or capturing a screenshot in a Kotlin project leads most developers down a painful path: Selenium, WebDriver, or a full headless Chrome setup on the JVM. These work, but they're heavy — Selenium alone pulls in dozens of transitive dependencies, WebDriver binaries need version-pinning, and running headless Chrome in a Kotlin backend or Android project is either awkward or impossible.

The cleaner approach: call a screenshot API. You send an HTTP request, get back binary image or PDF data. No browser process, no JVM–Chromium glue code, no container bloat.

This guide covers PageBolt — a hosted capture API with 100 free requests per month, no credit card required — using OkHttp (Android-friendly) and Ktor client (backend Kotlin).

Why Not Selenium or JVM Headless Chrome?

  • Selenium setup: WebDriverManager, ChromeDriver version mismatches, and System.setProperty boilerplate before you write a single business logic line.
  • JVM memory: A headless Chrome process spawned from a Ktor server adds 200–500 MB baseline memory on top of the JVM heap.
  • Android: You cannot run Selenium or Puppeteer on Android. If you want to generate a PDF or screenshot from an Android app, a REST API is the only practical option.
  • CI fragility: Chrome binary versions must match ChromeDriver versions. Dependency updates frequently break CI pipelines.

A screenshot API removes all of this. Your Kotlin code is a single HTTP call.

OkHttp: Screenshot and PDF

OkHttp is the standard HTTP client in Android and is widely used in backend Kotlin. Add it to your Gradle build:

// build.gradle.kts
dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
Enter fullscreen mode Exit fullscreen mode

Screenshot

import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.File

val client = OkHttpClient()
val JSON = "application/json".toMediaType()

fun screenshot(url: String, outputPath: String) {
    val body = JSONObject()
        .put("url", url)
        .put("fullPage", true)
        .put("blockBanners", true)
        .toString()
        .toRequestBody(JSON)

    val request = Request.Builder()
        .url("https://pagebolt.dev/api/v1/screenshot")
        .addHeader("x-api-key", System.getenv("PAGEBOLT_API_KEY"))
        .post(body)
        .build()

    client.newCall(request).execute().use { response ->
        check(response.isSuccessful) { "Screenshot failed: ${response.code}" }
        File(outputPath).writeBytes(response.body!!.bytes())
        println("Saved to $outputPath")
    }
}

fun main() {
    screenshot("https://example.com", "output.png")
}
Enter fullscreen mode Exit fullscreen mode

PDF Generation

fun generatePDF(url: String, outputPath: String) {
    val body = JSONObject()
        .put("url", url)
        .put("pdfOptions", JSONObject().put("format", "A4").put("printBackground", true))
        .toString()
        .toRequestBody(JSON)

    val request = Request.Builder()
        .url("https://pagebolt.dev/api/v1/pdf")
        .addHeader("x-api-key", System.getenv("PAGEBOLT_API_KEY"))
        .post(body)
        .build()

    client.newCall(request).execute().use { response ->
        check(response.isSuccessful) { "PDF generation failed: ${response.code}" }
        File(outputPath).writeBytes(response.body!!.bytes())
        println("PDF saved to $outputPath")
    }
}
Enter fullscreen mode Exit fullscreen mode

Pass raw HTML instead of a URL to render a template directly:

val body = JSONObject()
    .put("html", "<html><body><h1>Invoice #${invoiceId}</h1></body></html>")
    .put("pdfOptions", JSONObject().put("format", "A4"))
    .toString()
    .toRequestBody(JSON)
Enter fullscreen mode Exit fullscreen mode

This is the cleanest invoice PDF pattern for Kotlin backends — build the HTML string from your template engine, POST it, stream the PDF response to the client.

Kotlin Coroutines with OkHttp

For Android or coroutine-based backends, wrap the blocking OkHttp call with withContext(Dispatchers.IO):

import kotlinx.coroutines.*
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject

suspend fun screenshotAsync(url: String): ByteArray = withContext(Dispatchers.IO) {
    val body = JSONObject()
        .put("url", url)
        .put("fullPage", true)
        .toString()
        .toRequestBody("application/json".toMediaType())

    val request = Request.Builder()
        .url("https://pagebolt.dev/api/v1/screenshot")
        .addHeader("x-api-key", System.getenv("PAGEBOLT_API_KEY"))
        .post(body)
        .build()

    OkHttpClient().newCall(request).execute().use { response ->
        check(response.isSuccessful) { "Request failed: ${response.code}" }
        response.body!!.bytes()
    }
}

// In an Android ViewModel or coroutine scope:
viewModelScope.launch {
    val imageBytes = screenshotAsync("https://example.com")
    // Display or save imageBytes
}
Enter fullscreen mode Exit fullscreen mode

No callback hell, no thread management — just a suspending function that returns bytes.

Ktor Client: Backend Kotlin

For Ktor-based backends, use the Ktor HTTP client with coroutine support:

// build.gradle.kts
dependencies {
    implementation("io.ktor:ktor-client-core:2.3.9")
    implementation("io.ktor:ktor-client-cio:2.3.9")
    implementation("io.ktor:ktor-client-content-negotiation:2.3.9")
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.9")
}
Enter fullscreen mode Exit fullscreen mode
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*

val httpClient = HttpClient(CIO)

suspend fun screenshot(url: String): ByteArray {
    val response: HttpResponse = httpClient.post("https://pagebolt.dev/api/v1/screenshot") {
        header("x-api-key", System.getenv("PAGEBOLT_API_KEY"))
        contentType(ContentType.Application.Json)
        setBody("""{"url": "$url", "fullPage": true, "blockBanners": true}""")
    }
    check(response.status.isSuccess()) { "Screenshot failed: ${response.status}" }
    return response.readBytes()
}
Enter fullscreen mode Exit fullscreen mode

Ktor Route: PDF Download

Wire it into a Ktor route for an invoice download endpoint:

import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

routing {
    get("/invoice/{id}/pdf") {
        val id = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest)

        val response: HttpResponse = httpClient.post("https://pagebolt.dev/api/v1/pdf") {
            header("x-api-key", System.getenv("PAGEBOLT_API_KEY"))
            contentType(ContentType.Application.Json)
            setBody("""
                {
                  "url": "https://yourapp.com/invoice/$id?print=true",
                  "pdfOptions": { "format": "A4", "printBackground": true }
                }
            """.trimIndent())
        }

        val bytes = response.readBytes()
        call.response.header(
            HttpHeaders.ContentDisposition,
            "attachment; filename=\"invoice-$id.pdf\""
        )
        call.respondBytes(bytes, ContentType.Application.Pdf)
    }
}
Enter fullscreen mode Exit fullscreen mode

Use Case: Screenshot Proof for Compliance Workflows

If your Kotlin backend automates browser-based tasks — form submissions, portal logins, data entry — capturing a screenshot after each action provides an audit trail. Courts and compliance teams accept screenshots as evidence when they're timestamped and tied to a specific action:

data class ActionEvidence(
    val actionId: String,
    val url: String,
    val timestamp: Long,
    val screenshotBytes: ByteArray
)

suspend fun captureEvidence(actionId: String, url: String): ActionEvidence {
    val bytes = screenshot(url)
    return ActionEvidence(
        actionId = actionId,
        url = url,
        timestamp = System.currentTimeMillis(),
        screenshotBytes = bytes
    )
}
Enter fullscreen mode Exit fullscreen mode

Store the bytes in S3, a blob column, or your audit log. No headless browser process required.

Android: Save a Screenshot to the Gallery

On Android, use the coroutine-wrapped OkHttp call and save to external storage:

// In a ViewModel or repository
suspend fun savePageScreenshot(url: String, context: Context) {
    val bytes = screenshotAsync(url)
    val filename = "screenshot_${System.currentTimeMillis()}.png"
    context.openFileOutput(filename, Context.MODE_PRIVATE).use {
        it.write(bytes)
    }
}
Enter fullscreen mode Exit fullscreen mode

No Chrome WebView required. Works on any Android API level that supports OkHttp.

Free Tier

PageBolt's free tier includes 100 requests per month — no credit card required. That's enough to build and test your integration end-to-end. Paid plans start at $29/mo for 5,000 requests.

Get started: pagebolt.dev

Top comments (0)