Skip to content

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.

Installation

terminalbash
dotnet add package hinow-ai

Keep 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.

terminalbash
export HINOW_API_KEY="hi_sua_chave_aqui"

The first call

Program.cscsharp
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.

With dependency injection

Pass your own HttpClient and the SDK will not dispose it — whoever controls the lifecycle remains the IHttpClientFactory.

Program.cscsharp
builder.Services.AddHttpClient<HinowClient>((http, _) =>
    new HinowClient(apiKey: null, baseUrl: null, timeout: null, httpClient: http));

Showing the response as it arrives

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.

Streaming.cscsharp
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.

BuscaWeb.cscsharp
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}");
TypeWhat is filled 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 remains empty

Website contacts

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.

ContatosSite.cscsharp
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.

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.

Agente.cscsharp
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.

Conhecimento.cscsharp
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.

Errors

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.

Erros.cscsharp
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}");
}
ClassWhen it happens
HinowAuthenticationException401 — missing, invalid, or revoked key
HinowPermissionException403 — the key lacks access to this resource
HinowNotFoundException404 — nonexistent model, file, or assistant
HinowInvalidRequestException400 or 422 — a field is missing or a value is out of range
HinowRateLimitException429 — limit reached or balance exhausted
HinowServerException5xx — the failure is on the API side
HinowConnectionExceptionthe request did not arrive; nothing was charged

Balance and catalog

Modelos.cscsharp
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)}");

Everything the client exposes

MemberPurpose
Chat.CompletionsChat, streaming, function calling, JSON mode
EmbeddingsVectors for semantic search
Images · Audio · VideoGeneration
ModelsCatalog and capabilities of each model
ToolsWeb search and site contacts
FilesDocument upload
VectorStoresSearchable knowledge bases
RagSemantic search in your documents
Beta.Assistants · Beta.ThreadsServer-executed agents
GetBalanceAsync()Account balance

Configuration

Cliente.cscsharp
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 5xx

Coming from version 1.x

Up 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() returned null on every deserialized response, because the value arrives as a JsonElement. It now works, and Message.Text is the short name.
  • Errors are now typed, keeping HinowException as the base class so existing catch blocks remain valid.
  • Image, video, and audio responses now return in the OpenAI format: resposta.Data[0].Url instead of resposta.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.

Was this page helpful?