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.
The library is published through JitPack, so the repository has to be declared alongside the dependency. The dependency on its own does not resolve.
repositories {
mavenCentral()
maven("https://jitpack.io")
}
dependencies {
implementation("com.github.hinow-ai:sdk-kotlin:v2.0.2")
}repositories {
mavenCentral()
maven { url '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.
export HINOW_API_KEY="hi_sua_chave_aqui"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.
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.
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.
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}")
}
}| Type | What is populated in each result |
|---|---|
SearchType.SEARCH, SCHOLAR, PATENTS | position, title, url, snippet |
SearchType.NEWS | the above plus source, date, imageUrl |
SearchType.IMAGES | link, imageUrl, thumbnailUrl, width, height |
SearchType.VIDEOS | channel, duration, date, thumbnailUrl |
SearchType.PLACES | address, category, phone, website, rating, coordinates |
SearchType.SHOPPING | price, delivery, rating, source |
SearchType.AUTOCOMPLETE | suggestions; results stays empty |
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.
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.
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.
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.
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.
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.
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}")
}
}
}| Class | When it happens |
|---|---|
AuthenticationException | 401 — missing, invalid or revoked key |
PermissionException | 403 — the key has no access to that resource |
NotFoundException | 404 — model, file or assistant does not exist |
InvalidRequestException | 400 or 422 — a field is missing or a value is out of range |
RateLimitException | 429 — limit reached |
InsufficientBalanceException | 402 — balance exhausted |
ServerException | 5xx — the failure is on the API side |
ConnectionException | the request never arrived; nothing was charged |
TimeoutException | a job or run did not finish within the given time |
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}")
}
}| Property | What for |
|---|---|
chat.completions | Conversation, streaming, function calling, JSON mode |
embeddings | Vectors for semantic search |
images · audio · video | Generation |
models | Catalog, price and capabilities of each model |
tools | Web search and website contacts |
files | Document upload |
vectorStores | Searchable knowledge bases |
rag | Semantic search over your documents |
beta.assistants · beta.threads | Agents that run on the server |
getBalance() | Account balance |
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
)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.createtook loose parameters and packedtemperature,maxTokensandtopPinto aparametersobject, with the numbers converted to text. The API accepts that shape and ignores it, so those options never took effect. It now takes aChatCompletionRequestand sends everything at the root level.Messagebecame a data class with thetextproperty, in place of aMap<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.

