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.setPropertyboilerplate 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")
}
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")
}
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")
}
}
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)
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
}
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")
}
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()
}
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)
}
}
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
)
}
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)
}
}
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)