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.
Via Swift Package Manager. In Xcode: File → Add Package Dependencies and paste the repository URL.
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")
])File → Add Package Dependencies…
https://github.com/hinow-ai/sdk-swift.git
Dependency Rule: Up to Next Major Version — 2.0.0The 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.
export HINOW_API_KEY="hi_sua_chave_aqui"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.
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.
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.
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)")| Type | What is filled in each result |
|---|---|
.search, .scholar, .patents | position, title, url, snippet |
.news | the above plus source, date, imageURL |
.images | link, imageURL, thumbnailURL, width, height |
.videos | channel, duration, date, thumbnailURL |
.places | address, category, phone, website, rating, coordinates |
.shopping | price, delivery, rating, source |
.autocomplete | suggestions; results remains empty |
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.
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.
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.
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.
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.
HinowError is an enum, so catch covers each type of failure. statusCode returns the HTTP code when the failure came from the API.
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)")
}| Case | When it happens |
|---|---|
.authentication | 401 — missing, invalid, or revoked key |
.permission | 403 — the key does not have access to this resource |
.notFound | 404 — non-existent model, file, or assistant |
.invalidRequest | 400 or 422 — a field is missing or a value is out of range |
.rateLimit | 429 — limit reached |
.insufficientBalance | 402 — balance exhausted |
.server | 5xx — the failure is on the API side |
.connection | the request did not arrive; nothing was charged |
.timedOut | a job or run did not finish within the given time |
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)")
}| Property | Purpose |
|---|---|
chat.completions | Chat, streaming, function calling, JSON mode |
embeddings | Vectors for semantic search |
images · audio · video | Generation |
models | Catalog, pricing, and features for each model |
tools | Web search and website contacts |
files | Document upload |
vectorStores | Searchable knowledge bases |
rag | Semantic search on your documents |
beta.assistants · beta.threads | Server-side agents |
getBalance() | Account balance |
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
)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.createwrappedtemperature,maxTokens, andtopPin aparametersobject, with numbers converted to strings. The API accepts this format and ignores it, so requestingmaxTokens: 10returned the entire response. Now everything goes at the root level.HinowErrorgained a case for each failure type, instead of anapiErrorwith a dictionary.Package.swiftdeclared a test target pointing to a non-existent directory, soswift buildfailed 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.

