C# SDK
Install the official C# SDK and use chat, streaming, web search, agents, and semantic search in HiNow from .NET.
Updated on Aug 09, 2026
The official C# SDK for the HiNow API. Compiles to .NET 8 and .NET Standard 2.1, so it also runs in older projects and in Unity.
The API speaks the OpenAI protocol, and the SDK follows the same format. If you have already integrated with OpenAI, the shape of the calls is what you know.
dotnet add package hinow-ai<PackageReference Include="hinow-ai" Version="2.0.0" />Install-Package hinow-aiKeep the key in HINOW_API_KEY and the SDK will find it automatically. Passing the key in the constructor also works, but avoid leaving the value in the code.
export HINOW_API_KEY="hi_sua_chave_aqui"using Hinow;
// With no arguments, the client reads the key from HINOW_API_KEY.
using var client = new HinowClient();
var answer = await client.Chat.Completions.CreateAsync(new ChatCompletionRequest
{
Model = "hinow/himax",
Messages =
{
new Message("system", "You answer in English."),
new Message("user", "What is an API? Answer in one paragraph."),
},
});
// Message.Text gives you the text. Message.Content holds the raw value.
Console.WriteLine(answer.Choices[0].Message.Text);
var uso = answer.Usage!;
Console.WriteLine($"\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 applies to all: hinow/himax, hinow/hinova, hinow/higenesis.
Read the response via Message.Text
Message.Content holds the raw value, which in a deserialized response is a JsonElement. Text delivers the ready-to-use string and joins the parts when the response comes in chunks.
Pass your own HttpClient and the SDK will not dispose it — whoever controls the lifecycle remains the IHttpClientFactory.
builder.Services.AddHttpClient<HinowClient>((http, _) =>
new HinowClient(apiKey: null, baseUrl: null, timeout: null, httpClient: http));CreateStreamAsync returns an IAsyncEnumerable: you iterate with await foreach and each iteration brings a new chunk. In a chat screen, this changes the perception of speed more than switching models.
using Hinow;
using var client = new HinowClient();
var stream = client.Chat.Completions.CreateStreamAsync(new ChatCompletionRequest
{
Model = "hinow/hinova",
Messages = { new Message("user", "Write three lines about the sea at dawn.") },
});
await foreach (var chunk in stream)
{
// Note the Delta: each chunk carries the new fragment, not the whole answer.
Console.Write(chunk.Choices.Count > 0 ? chunk.Choices[0].Delta.Content : "");
}
Console.WriteLine();It is Delta, not Message
In the complete response, the text comes in Message.Text. In streaming, each chunk brings only the new snippet, in Delta.Content — putting it all together is up to you.
Responds instantly, without a job to track, at US$ 0.005 per call. The nine types are in SearchType, so the compiler won't let you misspell the name.
using Hinow;
using var client = new HinowClient();
// Answers on the spot: there is no job to follow.
var search = await client.Tools.SearchAsync(
"best beaches in northeast Brazil",
SearchType.Search,
country: "br",
lang: "pt-br");
foreach (var item in search.Results)
{
Console.WriteLine($"{item.Position}. {item.Title}");
Console.WriteLine($" {item.Url}");
}
Console.WriteLine($"\n{search.Results.Count} results · US$ {search.Cost}");| Type | What is filled 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 remains empty |
Scans one or more sites for emails, phones, and social profiles, and each finding comes with the exact page where it appeared. Since the scan takes time, this runs as a job.
using Hinow;
using var client = new HinowClient();
// The crawl takes a while, so it runs as a job. This method waits for you.
var job = await client.Tools.WebsiteContactsAndWaitAsync(
new[] { "https://www.teclia.com" },
maxDepth: 2,
maxLinksPerPage: 10);
Console.WriteLine($"status: {job.Status} · US$ {job.Cost}"
+ (job.Cached == true ? " (from cache)" : ""));
Console.WriteLine();
// Each item carries the type, the value and the exact page it was found on.
foreach (var item in job.Result!.Items)
{
if (item.Type == "social")
{
Console.WriteLine($"{item.Platform,-10}{item.Url}");
}
else
{
Console.WriteLine($"{item.Type,-10}{item.Value}");
Console.WriteLine($"{"",-10}em {item.SourceUrl}");
}
}Running the same crawl twice is not charged again
The job returns with Cached = true when the result came from the cache. If you want to control the lifecycle yourself, use WebsiteContactsAsync and track it with Tools.Jobs.RetrieveAsync(job.JobId) — note that the field is job_id, not id.
Validate what comes before using it
The scan returns what it found in the page HTML, as it is. Truncated or duplicate addresses may appear — treat the list as raw material and validate it before saving 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.
using System.Text.Json;
using Hinow;
using var client = new HinowClient();
// Your real function: a dictionary here, just for the example.
var orders = new Dictionary<string, object>
{
["A-1001"] = new { status = "delivered", delivered = "2026-08-02" },
["A-1002"] = new { status = "in transit", estimated = "2026-08-12" },
};
// 1. The assistant: model, instructions and the functions it may call.
var assistant = await client.Beta.Assistants.CreateAsync(new AssistantRequest
{
Model = "hinow/himax",
Name = "Support",
Instructions = "You answer questions about orders. Check the tool before stating any status.",
Tools = new List<Tool>
{
Tool.CreateFunction(
"get_order",
"Look up an order by its code.",
new Dictionary<string, object>
{
["type"] = "object",
["properties"] = new Dictionary<string, object>
{
["code"] = new Dictionary<string, object>
{
["type"] = "string",
["description"] = "Order code, e.g. A-1001",
},
},
["required"] = new[] { "code" },
}),
},
});
// 2. The conversation and the question.
var thread = await client.Beta.Threads.CreateAsync();
await client.Beta.Threads.Messages.CreateAsync(thread.Id, "Has order A-1002 arrived?");
// 3. Run it and wait.
var run = await client.Beta.Threads.Runs.CreateAndPollAsync(thread.Id, assistant.Id);
// 4. While the model asks for functions, run them and hand the result back.
while (run.Status == "requires_action")
{
var outputs = new List<ToolOutput>();
foreach (var call in run.RequiredAction!.SubmitToolOutputs!.ToolCalls)
{
var callArgs = JsonSerializer.Deserialize<Dictionary<string, string>>(call.Function.Arguments)!;
var code = callArgs["code"];
Console.WriteLine($"-> the model called {call.Function.Name}({code})");
var order = orders.TryGetValue(code, out var achado)
? achado
: new { error = "order not found" };
outputs.Add(new ToolOutput(call.Id, JsonSerializer.Serialize(order)));
}
await client.Beta.Threads.Runs.SubmitToolOutputsAsync(thread.Id, run.Id, outputs);
run = await client.Beta.Threads.Runs.PollAsync(thread.Id, run.Id);
}
// 5. The final answer is the last message on the thread.
var messages = await client.Beta.Threads.Messages.ListAsync(thread.Id, limit: 1, order: "desc");
Console.WriteLine($"\n{messages.Data[0].Text}");
await client.Beta.Assistants.DeleteAsync(assistant.Id);requires_action is not an error
It is the run handing control back to you to execute a function. That's why PollAsync returns in this state instead of continuing to spin: read the RequiredAction, call SubmitToolOutputsAsync, and resume monitoring.
Upload files, group them into a base, and search by meaning. Indexing is asynchronous — searching before it finishes returns zero results, without error, and that's exactly what PollAsync is for.
using Hinow;
using var client = new HinowClient();
// 1. Upload the document.
var file = await client.Files.CreateAsync("returns-policy.txt");
Console.WriteLine($"file: {file.Id} ({file.Bytes} bytes)");
// 2. Create the store and attach the file to it.
var base_ = await client.VectorStores.CreateAsync(name: "Support base");
await client.VectorStores.Files.CreateAsync(base_.Id, file.Id);
Console.WriteLine($"store: {base_.Id}");
// 3. Indexing is asynchronous. Searching before it finishes returns nothing
// at all, with no error, so PollAsync waits for it.
var indexado = await client.VectorStores.Files.PollAsync(base_.Id, file.Id);
Console.WriteLine($"indexing: {indexado.Status}");
// 4. Search by meaning, not by exact word.
var hits = await client.Rag.SearchAsync(
"How many days do I have to return an item?",
ragId: base_.Id,
topK: 3);
Console.WriteLine();
foreach (var achado in hits.Results)
{
Console.WriteLine($"{achado.Score:F2} {achado.Source}");
var excerpt = achado.Text.Replace("\n", " ");
Console.WriteLine($" {excerpt[..Math.Min(110, excerpt.Length)]}…");
}The base filter is called ragId
The API ignores vector_store_id in this endpoint: the search simply scans all documents in the account instead of the base you intended. It's the kind of detail that makes it seem like the search is returning garbage.
Each failure becomes its own class, so you can handle by type instead of comparing message strings. All inherit from HinowException, and those originating from the API carry StatusCode, ErrorType, and the response body.
using Hinow;
using var client = new HinowClient();
try
{
await client.Chat.Completions.CreateAsync(new ChatCompletionRequest
{
Model = "himax", // the hinow/ prefix is missing
Messages = { new Message("user", "Olá") },
});
}
catch (HinowAuthenticationException)
{
Console.WriteLine("Invalid or expired key. Check HINOW_API_KEY.");
}
catch (HinowNotFoundException e)
{
Console.WriteLine($"Not found: {e.Message}");
}
catch (HinowInvalidRequestException e)
{
Console.WriteLine($"Invalid request: {e.Message}");
}
catch (HinowRateLimitException)
{
Console.WriteLine("Rate limited. The SDK already retried; wait a moment.");
}
catch (HinowConnectionException e)
{
// Nothing reached the API, so nothing was charged.
Console.WriteLine($"Sem answer da API: {e.Message}");
}
catch (HinowException e)
{
// Safety net: any other error the API reported.
Console.WriteLine($"Error {e.StatusCode}: {e.Message}");
}| Class | When it happens |
|---|---|
HinowAuthenticationException | 401 — missing, invalid, or revoked key |
HinowPermissionException | 403 — the key lacks access to this resource |
HinowNotFoundException | 404 — nonexistent model, file, or assistant |
HinowInvalidRequestException | 400 or 422 — a field is missing or a value is out of range |
HinowRateLimitException | 429 — limit reached or balance exhausted |
HinowServerException | 5xx — the failure is on the API side |
HinowConnectionException | the request did not arrive; nothing was charged |
using Hinow;
using var client = new HinowClient();
// Account credit, in US dollars.
var balance = await client.GetBalanceAsync();
Console.WriteLine($"balance: US$ {balance.Amount:F2}\n");
// One specific model. The id is namespaced: hinow/himax, not himax.
var model = await client.Models.RetrieveAsync("hinow/himax");
Console.WriteLine($"{model.Name} ({model.Id})");
Console.WriteLine($"categories: {string.Join(", ", model.Category)}");| Member | Purpose |
|---|---|
Chat.Completions | Chat, streaming, function calling, JSON mode |
Embeddings | Vectors for semantic search |
Images · Audio · Video | Generation |
Models | Catalog and capabilities of each model |
Tools | Web search and site contacts |
Files | Document upload |
VectorStores | Searchable knowledge bases |
Rag | Semantic search in your documents |
Beta.Assistants · Beta.Threads | Server-executed agents |
GetBalanceAsync() | Account balance |
using var client = new HinowClient(
apiKey: Environment.GetEnvironmentVariable("HINOW_API_KEY"), // ou use a variável
baseUrl: "https://api.hinow.ai", // ou HINOW_BASE_URL
timeout: TimeSpan.FromSeconds(120),
httpClient: null,
maxRetries: 2); // repete 429 e 5xxUp to version 1.0.1, the SDK wrapped Temperature, MaxTokens, TopP, and RepetitionPenalty inside a parameters object before sending, with the numbers converted to strings. The API accepts this format and ignores it, so these options never took effect: requesting MaxTokens = 10 returned the entire response. Starting from 2.0, everything goes at the root level, as the API expects.
Three other changes that affect existing code:
Message.GetContentAsString()returnednullon every deserialized response, because the value arrives as aJsonElement. It now works, andMessage.Textis the short name.- Errors are now typed, keeping
HinowExceptionas the base class so existingcatchblocks remain valid. - Image, video, and audio responses now return in the OpenAI format:
resposta.Data[0].Urlinstead ofresposta.Data.Urls[0].
Choosing between HiMax, HiNova, and HiGenesis
What each model does well, how much it costs, and how to write the prompt for each one.

