Skip to content

Kotlin SDK

Install the official Kotlin SDK and use chat, streaming, web search, agents and semantic search on HiNow with coroutines.

Updated on Aug 09, 2026

The official Kotlin SDK for the HiNow API. It is built on Ktor and coroutines: every call is suspend, and responses are typed data classes with kotlinx.serialization.

The API speaks the OpenAI protocol, and the SDK follows the same shape. If you have integrated with OpenAI before, the design of the calls is the one you already know.

Installation

The library is published through JitPack, so the repository has to be declared alongside the dependency. The dependency on its own does not resolve.

build.gradle.ktskotlin
repositories {
    mavenCentral()
    maven("https://jitpack.io")
}

dependencies {
    implementation("com.github.hinow-ai:sdk-kotlin:v2.0.2")
}

Keep the key in HINOW_API_KEY and build the client with no arguments. Passing apiKey also works, but avoid leaving the value in the code.

terminalbash
export HINOW_API_KEY="hi_sua_chave_aqui"

The first call

Main.ktkotlin
import ai.hinow.ChatCompletionRequest
import ai.hinow.Hinow
import ai.hinow.Message
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    // With no arguments, the client reads the key from HINOW_API_KEY.
    // use { } closes the HTTP client when the block ends.
    Hinow().use { client ->
        val answer = client.chat.completions.create(
            ChatCompletionRequest(
                model = "hinow/himax",
                messages = listOf(
                    Message.system("You answer in English."),
                    Message.user("What is an API? Answer in one paragraph."),
                ),
            )
        )

        println(answer.choices[0].message.text)

        val uso = answer.usage
        println("\ninput: ${uso?.promptTokens} · output: ${uso?.completionTokens}")
    }
}

The hinow/ prefix is part of the model name

Sending himax instead of hinow/himax returns 404 model_not_found, and the message does not make it clear that the prefix is missing. This holds for all of them: hinow/himax, hinow/hinova, hinow/higenesis.

Declare fun main(): Unit

Without the : Unit, Kotlin infers the type of the last expression in the block. If it is not Unit, the JVM does not recognize the function as an entry point and the program fails with "main method not found" — an error that does not show up at compile time.

Showing the response as it arrives

createStream hands each chunk to your lambda as soon as it arrives and returns when the model finishes. On a conversation screen this changes the perception of speed more than switching models does.

Streaming.ktkotlin
import ai.hinow.ChatCompletionRequest
import ai.hinow.Hinow
import ai.hinow.Message
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        client.chat.completions.createStream(
            ChatCompletionRequest(
                model = "hinow/hinova",
                messages = listOf(Message.user("Write three lines about the sea at dawn.")),
            )
        ) { chunk ->
            // Note the delta: each chunk carries the new fragment, not the whole answer.
            print(chunk.choices.firstOrNull()?.delta?.content ?: "")
        }

        println()
    }
}

It is delta, not message

In the full response the text comes in message.text. In streaming, each chunk carries only the new fragment, in delta.content — putting it together is up to you.

Responds right away, with no job to track, at US$ 0.005 per call. The nine types live in SearchType, and an invalid type is rejected before the request goes out.

BuscaWeb.ktkotlin
import ai.hinow.Hinow
import ai.hinow.SearchType
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        // Answers on the spot: there is no job to follow.
        val search = client.tools.search(
            query = "best beaches in northeast Brazil",
            type = SearchType.SEARCH,
            country = "br",
            lang = "pt-br",
        )

        search.results.forEach { item ->
            println("${item.position}. ${item.title}")
            println("   ${item.url}")
        }

        println("\n${search.results.size} results · US$ ${search.cost}")
    }
}
TypeWhat is populated in each result
SearchType.SEARCH, SCHOLAR, PATENTSposition, title, url, snippet
SearchType.NEWSthe above plus source, date, imageUrl
SearchType.IMAGESlink, imageUrl, thumbnailUrl, width, height
SearchType.VIDEOSchannel, duration, date, thumbnailUrl
SearchType.PLACESaddress, category, phone, website, rating, coordinates
SearchType.SHOPPINGprice, delivery, rating, source
SearchType.AUTOCOMPLETEsuggestions; results stays empty

Contacts from a website

It crawls one or more sites looking for e-mails, phone numbers and social profiles, and every finding comes with the exact page where it appeared. Because the crawl takes time, this one runs as a job.

ContatosSite.ktkotlin
import ai.hinow.Hinow
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        // The crawl takes a while, so it runs as a job. This method waits for you.
        val job = client.tools.websiteContactsAndWait(
            websites = listOf("https://www.teclia.com"),
            maxDepth = 2,
            maxLinksPerPage = 10,
        )

        val cache = if (job.cached == true) " (from cache)" else ""
        println("status: ${job.status} · US$ ${job.cost}$cache\n")

        // Each item carries the type, the value and the exact page it was found on.
        job.result?.items?.forEach { item ->
            when (item.type) {
                "email", "phone" -> {
                    println(item.type.padEnd(9) + item.value)
                    println("".padEnd(9) + "em ${item.sourceUrl}")
                }
                else -> println((item.platform ?: "").padEnd(9) + (item.url ?: ""))
            }
        }
    }
}

Repeating an identical run is not charged again

The job comes back with cached true when the result came from the cache. If you would rather drive the loop yourself, use websiteContacts and track it with tools.jobs.retrieve(job.jobId) — note that the field is job_id, not id.

Validate whatever comes back before using it

The crawl returns what it found in the page HTML, without judging it. Truncated or duplicated addresses do show up — treat the list as raw material and validate it before writing to your database.

Agents

Assistants, threads and runs in the same shape as the OpenAI API. You declare the functions and the model decides when to call them: the run stops, you execute and return the result.

Agente.ktkotlin
import ai.hinow.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.*

// Your real function: a when here, just for the example.
fun getOrder(code: String): String = when (code) {
    "A-1001" -> """{"status":"delivered","delivered":"2026-08-02"}"""
    "A-1002" -> """{"status":"in transit","estimated":"2026-08-12"}"""
    else -> """{"error":"order not found"}"""
}

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        // The argument schema, in JSON Schema.
        val schema = buildJsonObject {
            put("type", "object")
            putJsonObject("properties") {
                putJsonObject("code") {
                    put("type", "string")
                    put("description", "Order code, e.g. A-1001")
                }
            }
            putJsonArray("required") { add("code") }
        }

        // 1. The assistant: model, instructions and the functions it may call.
        val assistant = client.beta.assistants.create(
            AssistantRequest(
                model = "hinow/himax",
                name = "Support",
                instructions = "You answer questions about orders. " +
                    "Check the tool before stating any status.",
                tools = listOf(
                    Tool.function(
                        "get_order",
                        "Look up an order by its code.",
                        schema,
                    )
                ),
            )
        )

        // 2. The conversation and the question.
        val thread = client.beta.threads.create()
        client.beta.threads.messages.create(thread.id, "Has order A-1002 arrived?")

        // 3. Run it and wait.
        var run = client.beta.threads.runs.createAndPoll(thread.id, assistant.id)

        // 4. While the model asks for functions, run them and hand the result back.
        while (run.status == "requires_action") {
            val outputs = run.requiredAction?.submitToolOutputs?.toolCalls.orEmpty().map { call ->
                val args = Json.parseToJsonElement(call.function.arguments).jsonObject
                val code = args["code"]?.jsonPrimitive?.content ?: ""
                println("-> the model called ${call.function.name}($code)")

                ToolOutput(call.id, getOrder(code))
            }

            client.beta.threads.runs.submitToolOutputs(thread.id, run.id, outputs)
            run = client.beta.threads.runs.poll(thread.id, run.id)
        }

        // 5. The final answer is the last message on the thread.
        val messages = client.beta.threads.messages.list(thread.id, limit = 1, order = "desc")
        println("\n${messages.data[0].text}")

        client.beta.assistants.delete(assistant.id)
    }
}

requires_action is not an error

It is the run handing control back to you so you can execute a function. That is why poll returns in this state instead of spinning: read requiredAction, call submitToolOutputs and resume tracking.

Upload files, group them into a knowledge base and search by meaning. Indexing is asynchronous — searching before it finishes returns zero results, with no error, and that is what poll is for.

Conhecimento.ktkotlin
import ai.hinow.Hinow
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        // 1. Upload the document.
        val file = client.files.create("returns-policy.txt")
        println("file: ${file.id} (${file.bytes} bytes)")

        // 2. Create the store and attach the file to it.
        val base = client.vectorStores.create("Support base")
        client.vectorStores.files.create(base.id, file.id)
        println("store: ${base.id}")

        // 3. Indexing is asynchronous. Searching before it finishes returns nothing
        //    at all, with no error, so poll waits for it.
        val state = client.vectorStores.files.poll(base.id, file.id)
        println("indexing: ${state.status}\n")

        // 4. Search by meaning, not by exact word.
        val hits = client.rag.search(
            query = "How many days do I have to return an item?",
            ragId = base.id,
            topK = 3,
        )

        hits.results.forEach { achado ->
            val excerpt = achado.text.replace("\n", " ").take(110)
            println("%.2f  %s".format(achado.score, achado.source))
            println("      $excerpt…")
        }
    }
}

The knowledge base filter is called ragId

The API ignores vector_store_id on this endpoint: the search simply sweeps every document in the account instead of the knowledge base you meant. It is the kind of detail that makes the search look like it is returning garbage.

Errors

Each failure becomes a class of its own, so you can handle it by type instead of comparing message strings. They all inherit from HinowException, and the ones that came from the API carry statusCode, errorType and the response body.

Erros.ktkotlin
import ai.hinow.*
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        try {
            client.chat.completions.create(
                ChatCompletionRequest(
                    model = "himax", // the hinow/ prefix is missing
                    messages = listOf(Message.user("Olá")),
                )
            )
        } catch (e: AuthenticationException) {
            println("Invalid or expired key. Check HINOW_API_KEY.")
        } catch (e: NotFoundException) {
            println("Not found: ${e.message}")
        } catch (e: InvalidRequestException) {
            println("Invalid request: ${e.message}")
        } catch (e: RateLimitException) {
            println("Rate limited. The SDK already retried; wait a moment.")
        } catch (e: ConnectionException) {
            // Nothing reached the API, so nothing was charged.
            println("Sem answer da API: ${e.message}")
        } catch (e: HinowException) {
            // Safety net: any other error the API reported.
            println("Error ${e.statusCode}: ${e.message}")
        }
    }
}
ClassWhen it happens
AuthenticationException401 — missing, invalid or revoked key
PermissionException403 — the key has no access to that resource
NotFoundException404 — model, file or assistant does not exist
InvalidRequestException400 or 422 — a field is missing or a value is out of range
RateLimitException429 — limit reached
InsufficientBalanceException402 — balance exhausted
ServerException5xx — the failure is on the API side
ConnectionExceptionthe request never arrived; nothing was charged
TimeoutExceptiona job or run did not finish within the given time

Balance and catalog

Modelos.ktkotlin
import ai.hinow.Hinow
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    Hinow().use { client ->
        // Account credit, in US dollars.
        val balance = client.getBalance()
        println("balance: US$ %.2f\n".format(balance.balance))

        // One specific model. The id is namespaced: hinow/himax, not himax.
        val model = client.models.retrieve("hinow/himax")
        println("${model.name} (${model.id})")
        println("categories: ${model.category.joinToString(", ")}")
        println("price per million tokens: input US$ ${model.cost?.input} · output US$ ${model.cost?.output}")
    }
}

Everything the client exposes

PropertyWhat for
chat.completionsConversation, streaming, function calling, JSON mode
embeddingsVectors for semantic search
images · audio · videoGeneration
modelsCatalog, price and capabilities of each model
toolsWeb search and website contacts
filesDocument upload
vectorStoresSearchable knowledge bases
ragSemantic search over your documents
beta.assistants · beta.threadsAgents that run on the server
getBalance()Account balance

Configuration

Cliente.ktkotlin
val client = Hinow(
    apiKey = System.getenv("HINOW_API_KEY"),  // ou deixe em branco e use a variável
    baseUrl = "https://api.hinow.ai",         // ou HINOW_BASE_URL
    timeout = 120_000,                        // milissegundos
    maxRetries = 2,                           // repete 429 e 5xx
)

Coming from version 1.x

1.0.0 returned a raw JsonObject on every call and did not check the HTTP status. A 401 or a 404 came back as if they had succeeded: the error JSON was delivered as though it were the response. That has been fixed — responses are typed data classes and anything outside 2xx raises the corresponding exception.

Two other changes:

  • chat.completions.create took loose parameters and packed temperature, maxTokens and topP into a parameters object, with the numbers converted to text. The API accepts that shape and ignores it, so those options never took effect. It now takes a ChatCompletionRequest and sends everything at the root level.
  • Message became a data class with the text property, in place of a Map<String, Any>.

Choosing between HiMax, HiNova and HiGenesis

What each model does well, how much it costs and how to write the prompt for each one.

Was this page helpful?