SearchStack.PublicApi
6.0.0
dotnet add package SearchStack.PublicApi --version 6.0.0
NuGet\Install-Package SearchStack.PublicApi -Version 6.0.0
<PackageReference Include="SearchStack.PublicApi" Version="6.0.0" />
<PackageVersion Include="SearchStack.PublicApi" Version="6.0.0" />
<PackageReference Include="SearchStack.PublicApi" />
paket add SearchStack.PublicApi --version 6.0.0
#r "nuget: SearchStack.PublicApi, 6.0.0"
#:package SearchStack.PublicApi@6.0.0
#addin nuget:?package=SearchStack.PublicApi&version=6.0.0
#tool nuget:?package=SearchStack.PublicApi&version=6.0.0
SearchStack.PublicApi
The official .NET client for the Search Stack public API
(api.searchstack.dev) — search, ingest, and manage your indexes from any .NET app.
- Typed and
HttpClient-based: aSearchStackClientwith a service per area; types match the wire format (snake_case) one-to-one. - Covers the full public surface: search and suggest, list/group management and versioning, ingestion, evals and judges, analytics, account details.
- The C# counterpart of
@searchstack/public-api, mirroring its surface closely.
Targets & dependencies
- Multi-targets
netstandard2.0(.NET Framework 4.6.1+, Mono, Xamarin, Unity, .NET Core 2.0+) andnet8.0. - Only dependency is
System.Text.Json, pulled in only onnetstandard2.0— in-box onnet8.0, so zero transitive dependencies there.
Install
dotnet add package SearchStack.PublicApi
Quickstart
using SearchStack.PublicApi;
var client = new SearchStackClient("sk_live_...");
// Search version 1 of the "products" list in the "acme" account.
var result = await client.Search.SearchListAsync("acme", "products", 1,
new SearchOptions { Query = "running shoes", Size = 10 });
if (result.IsSuccess)
{
foreach (var hit in result.ToSuccess().Results)
Console.WriteLine($"{hit.Name}: {hit.Text("colour")} — {hit.Number("price")}");
}
else
{
// Methods never throw — errors come back as a typed Problem (see below).
Console.Error.WriteLine($"{result.ToProblem().Status}: {result.ToProblem().Detail}");
}
Reading a result's fields
hit.Fields is a Dictionary<string, object?> of JsonElement values, because a field can hold text,
a number, a date, or several of any of those. Read through the readers, not the dictionary:
hit.Text("colour") // "red" — one value, as itself
hit.Text("genre") // "Comedy, Drama" — several, joined
hit.Values("genre") // ["Comedy", "Drama"]
hit.Number("price") // 19.99
- A facet is always an array in a response —
["red"],[2006],[]when the record carries no value — whatever its cardinality. Searchable fields and resources stay scalar. The readers cover both. - A field that is absent, null or empty reads as
null(or an empty list) rather than throwing, and a name in the wrong case still finds its field. - The same readers work on
VerifiedOffer.Fields, whereNumbermatters most — that one is the price you are about to charge.
Authentication
Configure one credential on construction; if both are supplied, the access token wins.
using SearchStack.PublicApi;
// API key -> sent as the `X-API-Key` header
var client = new SearchStackClient("sk_live_...");
// or, with full options:
var client = new SearchStackClient(new SearchStackClientOptions
{
AccessToken = "eyJ...", // sent as Authorization: Bearer <token>
BaseUrl = "https://api.searchstack.dev/", // optional, defaults to production
});
Swap credentials at runtime (e.g. after refreshing a token):
client.SetAccessToken(newToken);
client.SetApiKey(newKey);
Bring your own HttpClient
By default the client creates and owns an HttpClient, disposed with the client. Pass your own — to
control handlers, pooling or timeouts, or to use IHttpClientFactory — and it will not be disposed:
var client = new SearchStackClient(new SearchStackClientOptions
{
ApiKey = "sk_live_...",
HttpClient = httpClientFactory.CreateClient("searchstack"),
});
Result handling
No method throws on an HTTP error. Each resolves to a Response<T> (or Response for no-body calls) —
either a success carrying the typed payload or a Problem (RFC 7807).
- Network and JSON errors surface as a
ProblemwithStatus500. - Cancellation still throws
OperationCanceledException. result.Dataandresult.Problemare exposed directly for pattern-style checks.
var result = await client.Search.SearchListAsync("acme", "products", 1,
new SearchOptions { Query = "boots" });
if (result.IsSuccess)
{
SearchResponse page = result.ToSuccess();
Console.WriteLine($"{page.TotalCount}, {page.Results.Count}");
}
else
{
Problem problem = result.ToProblem();
Console.Error.WriteLine($"{problem.Status}: {problem.Detail}");
}
Services
| Property | Service | Highlights |
|---|---|---|
client.Accounts |
Account | GetAsync, GetNameAsync |
client.Lists |
List | CreateAsync, GetAsync, EditAsync, CloneAsync, RestoreVersionAsync, SetSynonymsAsync, DeleteAsync |
client.Groups |
Group | CreateAsync, GetAsync, GetVersionsAsync, GetVersionMembersAsync, UpdateAsync, CloneAsync, BumpVersionAsync, RestoreVersionAsync, AddListAsync, RemoveListAsync, SetModelAsync, RemoveModelAsync, SetRerankerAsync, RemoveRerankerAsync, TransferAsync, DeleteAsync |
client.Facets |
Facet | CreateAsync, RenameAsync, RemoveAsync |
client.Resources |
Resource | CreateAsync, RenameAsync, RemoveAsync |
client.Searchables |
Searchable field | CreateAsync, RenameAsync, RemoveAsync |
client.Contributors |
Contributor | AddAsync, RemoveAsync |
client.Coordinates |
Coordinates | AddAsync, RemoveAsync |
client.UserGeneratedSuggestions |
User Generated Suggestions | EnableAsync, EditAsync, DisableAsync |
client.MediaStores |
Media store | DeleteMediaAsync |
client.SearchResults |
Search result writes | CreateAsync, BulkInsertAsync, BulkInsertWithFieldsAsync, EditAsync, EditVectorAsync, RemoveAsync, DeleteByFilterAsync, SoftDeleteByFilterAsync |
client.Search |
Search / query | SearchListAsync, SearchGroupAsync, RelatedAsync, SearchListByImageAsync, SearchGroupByImageAsync, SearchListByImageBase64Async, SearchGroupByImageBase64Async, ExtractDocumentTextAsync, RecordListClickAsync, RecordGroupClickAsync |
client.Suggest |
Suggest | ListAsync, GroupAsync |
client.Ask |
Ask (grounded, cited answers) | AskListAsync, AskGroupAsync, AskGroupLatestAsync |
client.Analytics |
Analytics | GetListAsync, GetGroupAsync |
client.Evals |
Eval sets | ListAsync, GetAsync, GetRunsAsync, GetRunStatusAsync, CreateAsync, BootstrapAsync, RunAsync, DeleteAsync |
client.Evals |
Eval cases | ListCasesAsync, GetCaseAsync, CreateCaseAsync, EditCaseAsync, DeleteCaseAsync |
client.Judges |
Judge graders | ListAsync, GetAsync, GetRunStatusAsync, CreateAsync, EditAsync, DeleteAsync |
client.Judges |
Judge instructions | ListInstructionsAsync, GetInstructionAsync, CreateInstructionAsync, EditInstructionAsync, DeleteInstructionAsync, ListInstructionVersionsAsync, GetInstructionVersionAsync, RestoreInstructionVersionAsync, RunAsync, TryAsync |
client.QueryRules |
Query rules (query optimisation) | TemplatesAsync, ListAsync, GetAsync, CreateAsync, TestAsync, ReorderAsync, EditAsync, SetStateAsync, SetStanceAsync, InferAsync, GetMetricsAsync, GetFieldProfilesAsync, InferFromSearchesAsync, DeleteAsync |
client.Discovery |
Discovery (anonymous) | InfoAsync, ExamplesAsync, CatalogAsync |
Search and suggest methods use the API's
POSTvariants so options are passed as a typed object rather than a hand-built query string.
Examples
Text search a list version:
var res = await client.Search.SearchListAsync("acme", "products", 3, new SearchOptions
{
Query = "running shoes",
Size = 20,
Filter = "brand eq 'Acme'",
MinimumTextScore = 0.6,
});
Related results and a click-through:
var related = await client.Search.RelatedAsync("acme", "products", "sku-123",
new RelatedOptions { Size = 5 });
if (related.IsSuccess)
{
var page = related.ToSuccess();
if (!string.IsNullOrEmpty(page.QueryId))
{
await client.Search.RecordListClickAsync("acme", "products", new RecordClickRequest
{
QueryId = page.QueryId!,
ResultId = page.Results[0].Name,
});
}
}
Bulk insert. Each row carries its name under name — bulk insert reads that key and no other, so a
row named title or search_result_name is dropped:
await client.SearchResults.BulkInsertWithFieldsAsync("acme", "products", new[]
{
new Dictionary<string, object?> { ["name"] = "Trail Shoe", ["sku"] = "sku-1", ["price"] = 99 },
new Dictionary<string, object?> { ["name"] = "Road Shoe", ["sku"] = "sku-2", ["price"] = 79 },
});
A group and its membership versions:
await client.Groups.AddListAsync("acme", "catalog",
new AddListToGroupRequest { ListName = "products", Version = 4 });
await client.Groups.BumpVersionAsync("acme", "catalog"); // pin members to their latest
var versions = await client.Groups.GetVersionsAsync("acme", "catalog");
// search a specific, frozen membership version:
await client.Search.SearchGroupAsync("acme", "catalog",
versions.ToSuccess().CurrentVersion, new SearchOptions { Query = "boots" });
// ...or follow the current members, whatever they are, on search, suggest and facet values:
await client.Search.SearchGroupLatestAsync("acme", "catalog", new SearchOptions { Query = "boots" });
await client.Suggest.GroupLatestAsync("acme", "catalog", "boo");
await client.Facets.GroupValuesLatestAsync("acme", "catalog", "colour");
A judge instruction (the prompt a judge grades against), then an eval set:
var verdict = await client.Judges.TryAsync("acme", "relevance-check", new TryJudgeRequest
{
Context = new JudgeContextDto { Text = "waterproof hiking boots" },
Candidate = new JudgeCandidateDto { Id = "sku-1", Text = "Trail Runner — mesh, not waterproof" },
});
if (verdict.IsSuccess) Console.WriteLine(verdict.ToSuccess().Passed);
var run = await client.Evals.RunAsync("acme", "relevance"); // async (202)
if (run.IsSuccess)
{
var status = await client.Evals.GetRunStatusAsync("acme", "relevance", run.ToSuccess().RunId);
}
List analytics for a date range:
var report = await client.Analytics.GetListAsync("acme", "products", "2026-05-01", "2026-05-31");
Development
dotnet build
dotnet pack -c Release
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Text.Json (>= 8.0.5)
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.