Ktor

Ktor

Install

Add the dependency to the project configuration:

Maven

1
2
3
4
5
<dependency>
    <groupId>com.botbye</groupId>
    <artifactId>kotlin-module</artifactId>
    <version>3.0.1</version>
</dependency>

or

Gradle

1
implementation("com.botbye:kotlin-module:3.0.1")

Configuration

Create BotbyeConfig with your server-key (available inside your Project) and build the client with Botbye.withExtractor(...). Bind a Ktor extractor once: the SDK pulls ip/token/headers/method/uri out of each ApplicationRequest, so your handlers pass only the raw request to the evaluate* methods. The type parameter is your framework request type.

1
2
3
4
5
6
7
8
9
10
11
val config = BotbyeConfig(serverKey = "00000000-0000-0000-0000-000000000000") // Use your project server-key

val botbye: Botbye<ApplicationRequest> = Botbye.withExtractor(config) { request ->
    BotbyeRequestInfo(
        ip = request.local.remoteAddress,
        token = request.queryParameters["botbye_token"] ?: "", // wherever you pass it: query param, header, body, etc.
        headers = Headers(request.headers.toMap()), // multi-value headers; the SDK owns the flattening
        requestMethod = request.local.method.value,
        requestUri = request.local.uri,
    )
}

Usage

Add a Ktor plugin to evaluate all incoming requests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.botbye.protection.Botbye
import com.botbye.protection.model.BotbyeRequestInfo
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*

fun Application.configureBotbye(botbye: Botbye<ApplicationRequest>) {
    intercept(ApplicationCallPipeline.Plugins) {
        val response = botbye.evaluateValidation(call.request)

        if (response.isBlocked) {
            call.respond(HttpStatusCode.Forbidden, "Access denied")
            finish()
            return@intercept
        }
    }
}

Build the client (with its extractor) and install the plugin in your application module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun main() {
    embeddedServer(Netty, port = 8080) {
        val config = BotbyeConfig(
            serverKey = "00000000-0000-0000-0000-000000000000" // Use your project server-key
        )
        val botbye = Botbye.withExtractor(config) { request ->
            BotbyeRequestInfo(
                ip = request.local.remoteAddress,
                token = request.queryParameters["botbye_token"] ?: "",
                headers = Headers(request.headers.toMap()),
                requestMethod = request.local.method.value,
                requestUri = request.local.uri,
            )
        }

        configureBotbye(botbye)

        routing {
            post("/api/demo") {
                call.respondText("hello world!")
            }
        }
    }.start(wait = true)
}

There are three event types — validate, risk, and full — each suited for a different layer of your application.

validate — edge-level bot check

Use at the edge — API gateway, route handler, middleware — when you just want to know: was this request made by a bot? No user or domain context needed.

1
2
3
4
5
6
7
8
9
10
11
fun Application.configureBotbye(botbye: Botbye<ApplicationRequest>) {
    intercept(ApplicationCallPipeline.Plugins) {
        val response = botbye.evaluateValidation(call.request)

        if (response.isBlocked) {
            call.respond(HttpStatusCode.Forbidden, "Access denied")
            finish()
            return@intercept
        }
    }
}

risk — domain-level risk scoring

Use inside routes/services that already know the user: auth, payments, account management, etc. The purpose shifts from "is this a bot?" to "is something suspicious happening for this user?" — credential stuffing, account takeover, account sharing, logins from a new geo. Pass call.request so the extractor can attach request context to the risk event:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fun Route.loginRoute(botbye: Botbye<ApplicationRequest>) {
    post("/auth/login") {
        val body = call.receive<LoginRequest>()

        val user = findUser(body.email)
        val loginSucceeded = user != null && checkPassword(user, body.password)

        val response = botbye.evaluateRiskScoring(
            call.request,
            user = BotbyeUserInfo(
                accountId = user?.id ?: "unknown",
                email = body.email,
            ),
            eventType = "login",
            eventStatus = if (loginSucceeded) BotbyeEventStatus.SUCCESSFUL else BotbyeEventStatus.FAILED,
        )

        if (response.isBlocked) {
            call.respond(HttpStatusCode.Forbidden, "Access denied")
            return@post
        }

        call.respondText("Login successful")
    }
}

Linking validate and risk events

When the same request is evaluated at two layers — for example, once at the edge (validate) and then again in a route/service (risk) — BotBye can link both events and display them as a single event in the dashboard.

Step 1 — edge layer (plugin interceptor or route handler): run validate and capture the result:

1
2
3
4
// e.g. in configureBotbye() interceptor
val edgeResponse = botbye.evaluateValidation(call.request)
val edgeBotbyeResult = edgeResponse.botbyeResult
// Pass edgeBotbyeResult downstream — a call attribute, function argument, shared context, etc.

Step 2 — domain route/service (auth, payment, account management): pass it as botbyeResult in the risk call:

1
2
3
4
5
6
7
8
// e.g. in loginRoute()
val riskResponse = botbye.evaluateRiskScoring(
    call.request,
    user = BotbyeUserInfo(accountId = userId, email = email),
    eventType = "login",
    eventStatus = if (loginSucceeded) BotbyeEventStatus.SUCCESSFUL else BotbyeEventStatus.FAILED,
    botbyeResult = edgeBotbyeResult,
)

botbyeResult is null when absent — in that case, omit it (or pass null) and the events will be recorded independently.

full — edge check and domain scoring in one call

Use when you have all context at once: raw request, token, user, and event. A login endpoint is a typical example — it receives the HTTP request and immediately knows the user and outcome.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fun Route.loginRoute(botbye: Botbye<ApplicationRequest>) {
    post("/auth/login") {
        val body = call.receive<LoginRequest>()

        val user = findUser(body.email)
        val loginSucceeded = user != null && checkPassword(user, body.password)

        val response = botbye.evaluateFull(
            call.request,
            user = BotbyeUserInfo(
                accountId = user?.id ?: "unknown",
                email = body.email,
            ),
            eventType = "login",
            eventStatus = if (loginSucceeded) BotbyeEventStatus.SUCCESSFUL else BotbyeEventStatus.FAILED,
        )

        if (response.isBlocked) {
            call.respond(HttpStatusCode.Forbidden, "Access denied")
            return@post
        }

        call.respondText("Login successful")
    }
}

Settings

BotbyeConfig contains next configurable parameters:

Setting Description Required Default Value
botbyeEndpoint Host of the API Server no https://verify.botbye.com
serverKey Your BotBye server-key yes -
contentType Content type for API requests no application/json
readTimeout Read timeout for HTTP client no Duration.ofSeconds(2)
writeTimeout Write timeout for HTTP client no Duration.ofSeconds(2)
connectionTimeout Connection timeout for HTTP client no Duration.ofSeconds(2)
callTimeout Total call timeout no Duration.ofSeconds(5)
maxIdleConnections Max idle connections in the pool no 250
keepAliveDuration Keep-alive duration no Duration.ofSeconds(300)
maxRequestsPerHost Max requests per host no 1500
maxRequests Max requests total no 1500

Custom HTTP transport

The BotbyeConfig settings above tune the built-in OkHttp client. If you need a different HTTP stack entirely — your framework's own client, a shared pool, an outbound proxy — the SDK talks to BotBye only through the BotbyeHttpClient interface (OkHttp is just the default, OkHttpBotbyeClient). Implement the interface and pass it to the client; a transport you supply is caller-owned, so the SDK never closes it.

1
2
3
4
5
6
7
8
9
10
11
import com.botbye.common.http.BotbyeHttpClient
import com.botbye.common.http.BotbyeHttpRequest
import com.botbye.common.http.BotbyeHttpResponse

class MyHttpClient : BotbyeHttpClient {
    override val type = "my-client"
    override suspend fun call(request: BotbyeHttpRequest): BotbyeHttpResponse { /* ... */ }
}

// Pass it to either client (or the withExtractor factory):
val botbye = Botbye(config, client = MyHttpClient())

Examples of BotBye API responses

Blocked (bot detected):

1
2
3
4
5
6
7
{
  "request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
  "decision": "BLOCK",
  "risk_score": 0.95,
  "scores": { "bot": 0.95 },
  "signals": ["AutomationTool"]
}

Allowed:

1
2
3
4
5
6
7
{
  "request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
  "decision": "ALLOW",
  "risk_score": 0.05,
  "scores": { "bot": 0.05, "ato": 0.02 },
  "signals": []
}

Challenge:

1
2
3
4
5
6
7
8
{
  "request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
  "decision": "CHALLENGE",
  "risk_score": 0.65,
  "scores": { "bot": 0.65 },
  "signals": ["SuspiciousFingerprint"],
  "challenge": { "type": "CAPTCHA" }
}

Invalid server-key — not a known project (fail-closed):

1
2
3
4
5
6
7
8
{
  "request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
  "decision": "BLOCK",
  "risk_score": 1.0,
  "scores": {},
  "signals": ["evaluation_error"],
  "error": { "message": "Invalid server key" }
}

Evaluation error — request could not be processed (fail-closed):

1
2
3
4
5
6
7
8
{
  "request_id": "f77b2abd-c5d7-44f0-be4f-174b04876583",
  "decision": "BLOCK",
  "risk_score": 1.0,
  "scores": {},
  "signals": ["evaluation_error"],
  "error": { "message": "Invalid request body: missing or invalid field 'user.account_id'" }
}

SDK never received a decision (fail-open):

1
2
3
4
{
  "decision": "ALLOW",
  "error": { "message": "connection error" }
}