Skip to content

Swift SDK

Install the official Swift SDK and use chat, streaming, web search, agents, and semantic search on HiNow with async/await.

Updated on Aug 09, 2026

The official Swift SDK for the HiNow API. Uses async/await and Codable throughout, with no external dependencies, and runs on both iOS and macOS as well as Linux.

The API speaks the OpenAI protocol, and the SDK follows the same format. If you've already integrated with OpenAI, the shape of the calls will be familiar to you.

Installation

Via Swift Package Manager. In Xcode: File → Add Package Dependencies and paste the repository URL.

Package.swiftswift
dependencies: [
    .package(url: "https://github.com/hinow-ai/sdk-swift.git", from: "2.0.0")
]

// e no alvo:
.target(name: "MeuApp", dependencies: [
    .product(name: "HinowAI", package: "sdk-swift")
])

The repository is sdk-swift, the module is HinowAI

The package is named after the repository, but what you import in your code is import HinowAI.

Store the key in HINOW_API_KEY and build the client without arguments. Passing apiKey: also works, but avoid leaving the value in your code — and in an iOS app, never embed the key in the binary: call the API from your server.

terminalbash
export HINOW_API_KEY="hi_sua_chave_aqui"

The first call

main.swiftswift
import Foundation
import HinowAI

// With no arguments, the client reads the key from HINOW_API_KEY.
let client = try Hinow()

let answer = try await client.chat.completions.create(
    ChatCompletionRequest(
        model: "hinow/himax",
        messages: [
            .system("You answer in English."),
            .user("What is an API? Answer in one paragraph."),
        ]
    )
)

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

if let uso = answer.usage {
    print("\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 doesn't make it clear that the prefix is missing. This applies to all: hinow/himax, hinow/hinova, hinow/higenesis.

Showing the response as it arrives

createStream delivers each chunk to your closure as soon as it arrives and returns when the model finishes. In a chat screen, this changes the perception of speed more than switching models does.

Streaming.swiftswift
import Foundation
import HinowAI

let client = try Hinow()

try await client.chat.completions.createStream(
    ChatCompletionRequest(
        model: "hinow/hinova",
        messages: [.user("Write three lines about the sea at dawn.")]
    )
) { chunk in
    // Note the delta: each chunk carries the new fragment, not the whole answer.
    print(chunk.choices.first?.delta.content ?? "", terminator: "")
}

print()

It's delta, not message

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

Responds instantly, without a job to track, at $0.005 per call. The nine types are cases of SearchType, so the compiler won't let you misspell the name.

BuscaWeb.swiftswift
import Foundation
import HinowAI

let client = try Hinow()

// Answers on the spot: there is no job to follow.
let search = try await client.tools.search(
    query: "best beaches in northeast Brazil",
    type: .search,
    country: "br",
    lang: "pt-br"
)

for item in search.results ?? [] {
    print("\(item.position ?? 0). \(item.title ?? "")")
    print("   \(item.url ?? "")")
}

print("\n\((search.results ?? []).count) results · US$ \(search.cost ?? 0)")
TypeWhat is filled in each result
.search, .scholar, .patentsposition, title, url, snippet
.newsthe above plus source, date, imageURL
.imageslink, imageURL, thumbnailURL, width, height
.videoschannel, duration, date, thumbnailURL
.placesaddress, category, phone, website, rating, coordinates
.shoppingprice, delivery, rating, source
.autocompletesuggestions; results remains empty

Website contacts

Scans one or more sites for emails, phone numbers, and social profiles, and each finding comes with the exact page where it appeared. Since the scan takes time, this runs as a job.

ContatosSite.swiftswift
import Foundation
import HinowAI

let client = try Hinow()

// The crawl takes a while, so it runs as a job. This method waits for you.
let job = try await client.tools.websiteContactsAndWait(
    websites: ["https://www.teclia.com"],
    maxDepth: 2,
    maxLinksPerPage: 10
)

let cache = job.cached == true ? " (from cache)" : ""
print("status: \(job.status) · US$ \(job.cost ?? 0)\(cache)\n")

// Each item carries the type, the value and the exact page it was found on.
for item in job.result?.items ?? [] {
    switch item.type {
    case "email", "phone":
        print("\(item.type.padding(toLength: 9, withPad: " ", startingAt: 0))\(item.value)")
        print("\(String(repeating: " ", count: 9))em \(item.sourceURL ?? "")")
    default:
        print("\((item.platform ?? "").padding(toLength: 9, withPad: " ", startingAt: 0))\(item.url ?? "")")
    }
}

Running the same crawl twice is not charged again

The job returns with cached set to true when the result came from the cache. If you want to control the lifecycle yourself, use websiteContacts and track it with tools.jobs.retrieve(job.jobID) — note that the field is job_id, not id.

Validate what comes back before using it

The scan returns what it found in the page HTML, as it is. Truncated or repeated addresses appear — treat the list as raw material and validate it before saving it to your database.

Agents

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

The function schema is arbitrary JSON, so it doesn't fit into a fixed struct — for this, there is JSONValue, which accepts Swift literals and serializes them in the format the API expects.

Agente.swiftswift
import Foundation
import HinowAI

// Your real function: a switch here, just for the example.
func getOrder(_ code: String) -> String {
    switch code {
    case "A-1001": return #"{"status":"delivered","delivered":"2026-08-02"}"#
    case "A-1002": return #"{"status":"in transit","estimated":"2026-08-12"}"#
    default: return #"{"error":"order not found"}"#
    }
}

let client = try Hinow()

// The argument schema, in JSON Schema. JSONValue takes Swift literals.
let schema: JSONValue = [
    "type": "object",
    "properties": [
        "code": ["type": "string", "description": "Order code, e.g. A-1001"]
    ],
    "required": ["code"],
]

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

// 2. The conversation and the question.
let thread = try await client.beta.threads.create()
try await client.beta.threads.messages.create(
    threadID: thread.id, content: "Has order A-1002 arrived?")

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

// 4. While the model asks for functions, run them and hand the result back.
while run.status == "requires_action" {
    var outputs: [ToolOutput] = []

    for call in run.requiredAction?.submitToolOutputs?.toolCalls ?? [] {
        let args = try JSONDecoder().decode(
            JSONValue.self, from: Data(call.function.arguments.utf8))
        let code = args["code"]?.stringValue ?? ""
        print("-> the model called \(call.function.name)(\(code))")

        outputs.append(ToolOutput(toolCallID: call.id, output: getOrder(code)))
    }

    try await client.beta.threads.runs.submitToolOutputs(
        threadID: thread.id, runID: run.id, outputs: outputs)
    run = try await client.beta.threads.runs.poll(threadID: thread.id, runID: run.id)
}

// 5. The final answer is the last message on the thread.
let messages = try await client.beta.threads.messages.list(
    threadID: thread.id, limit: 1, order: "desc")
print("\n\(messages.data[0].text)")

try await client.beta.assistants.delete(assistant.id)

requires_action is not an error

It is the run handing control back to you to execute a function. That's why poll returns in this state instead of continuing to spin: read the requiredAction, call submitToolOutputs, and resume monitoring.

Upload files, group them into a vector store, and search by meaning. Indexing is asynchronous — searching before it finishes returns zero results, without error, and that's what poll is for.

Conhecimento.swiftswift
import Foundation
import HinowAI

let client = try Hinow()

// 1. Upload the document.
let file = try await client.files.create(path: "returns-policy.txt")
print("file: \(file.id) (\(file.bytes ?? 0) bytes)")

// 2. Create the store and attach the file to it.
let base = try await client.vectorStores.create(name: "Support base")
_ = try await client.vectorStores.files.create(vectorStoreID: base.id, fileID: file.id)
print("store: \(base.id)")

// 3. Indexing is asynchronous. Searching before it finishes returns nothing
//    at all, with no error, so poll waits for it.
let state = try await client.vectorStores.files.poll(
    vectorStoreID: base.id, fileID: file.id)
print("indexing: \(state.status)\n")

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

for achado in hits.results {
    let excerpt = achado.text.replacingOccurrences(of: "\n", with: " ").prefix(110)
    print(String(format: "%.2f  %@", achado.score, achado.source ?? ""))
    print("      \(excerpt)…")
}

The filter for the vector store is called ragID

The API ignores vector_store_id in this endpoint: the search simply scans all documents in the account instead of the store you intended. It's the kind of detail that makes it seem like the search is returning garbage.

Errors

HinowError is an enum, so catch covers each type of failure. statusCode returns the HTTP code when the failure came from the API.

Erros.swiftswift
import Foundation
import HinowAI

let client = try Hinow()

do {
    let answer = try await client.chat.completions.create(
        ChatCompletionRequest(
            model: "himax", // the hinow/ prefix is missing
            messages: [.user("Olá")]
        )
    )
    print(answer.choices[0].message.text)
} catch HinowError.authentication {
    print("Invalid or expired key. Check HINOW_API_KEY.")
} catch HinowError.notFound(_, let message) {
    print("Not found: \(message)")
} catch HinowError.invalidRequest(_, let message) {
    print("Invalid request: \(message)")
} catch HinowError.rateLimit {
    print("Rate limited. The SDK already retried; wait a moment.")
} catch HinowError.connection(let message) {
    // Nothing reached the API, so nothing was charged.
    print("Sem answer da API: \(message)")
} catch let error as HinowError {
    // Safety net: any other error the API reported.
    print("Error \(error.statusCode ?? 0): \(error.localizedDescription)")
}
CaseWhen it happens
.authentication401 — missing, invalid, or revoked key
.permission403 — the key does not have access to this resource
.notFound404 — non-existent model, file, or assistant
.invalidRequest400 or 422 — a field is missing or a value is out of range
.rateLimit429 — limit reached
.insufficientBalance402 — balance exhausted
.server5xx — the failure is on the API side
.connectionthe request did not arrive; nothing was charged
.timedOuta job or run did not finish within the given time

Balance and catalog

Modelos.swiftswift
import Foundation
import HinowAI

let client = try Hinow()

// Account credit, in US dollars.
let balance = try await client.getBalance()
print(String(format: "balance: US$ %.2f\n", balance.balance))

// One specific model. The id is namespaced: hinow/himax, not himax.
let model = try await client.models.retrieve("hinow/himax")
print("\(model.name ?? "") (\(model.id))")
print("categories: \((model.category ?? []).joined(separator: ", "))")

if let cost = model.cost {
    print("price per million tokens: input US$ \(cost.input ?? 0) · output US$ \(cost.output ?? 0)")
}

Everything the client exposes

PropertyPurpose
chat.completionsChat, streaming, function calling, JSON mode
embeddingsVectors for semantic search
images · audio · videoGeneration
modelsCatalog, pricing, and features for each model
toolsWeb search and website contacts
filesDocument upload
vectorStoresSearchable knowledge bases
ragSemantic search on your documents
beta.assistants · beta.threadsServer-side agents
getBalance()Account balance

Configuration

Cliente.swiftswift
let client = try Hinow(
    apiKey: ProcessInfo.processInfo.environment["HINOW_API_KEY"],  // ou use a variável
    baseURL: "https://api.hinow.ai",  // ou HINOW_BASE_URL
    timeout: 120,                     // segundos
    maxRetries: 2                     // repete 429 e 5xx
)

Coming from version 1.x

1.0.0 returned [String: Any] on every call, so reading a response involved a sequence of casts and unwrapping. Now responses are Codable structs.

Three other changes:

  • chat.completions.create wrapped temperature, maxTokens, and topP in a parameters object, with numbers converted to strings. The API accepts this format and ignores it, so requesting maxTokens: 10 returned the entire response. Now everything goes at the root level.
  • HinowError gained a case for each failure type, instead of an apiError with a dictionary.
  • Package.swift declared a test target pointing to a non-existent directory, so swift build failed before compiling anything.

Choosing between HiMax, HiNova, and HiGenesis

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

Was this page helpful?