Configuring the Management client
September 18, 2026 · View on GitHub
How to register a client, where its options come from, and what the transport does by default.
- Client registration and lifetime
- Options and configuration binding
- Named clients
- Configuration options
- Retries and timeouts
- HTTP handlers
- Source tracking for tool authors
Client registration and lifetime
Two entry points, same builder: AddManagementClient for applications, ManagementClient.Create or the
constructor for scripts.
using Kontent.Ai.Management.Extensions;
services.AddManagementClient(management => management.Options.Configure(options =>
{
options.EnvironmentId = "<YOUR_ENVIRONMENT_ID>";
options.ApiKey = "<YOUR_API_KEY>";
}));
AddManagementClient lives in Kontent.Ai.Management.Extensions; the registration snippets below
assume that using. Inject IManagementClient wherever you need it.
The container owns a registered client — do not dispose it. A client you build yourself owns its resources, so dispose it:
await using var client = new ManagementClient(new ManagementOptions
{
EnvironmentId = "<YOUR_ENVIRONMENT_ID>",
ApiKey = "<YOUR_API_KEY>"
});
ManagementClient.Create takes the same builder as AddManagementClient, for when a script needs more
than the constructor offers:
await using var client = ManagementClient.Create(management => management.Options.Configure(options =>
{
options.EnvironmentId = "<YOUR_ENVIRONMENT_ID>";
options.ApiKey = "<YOUR_API_KEY>";
}));
The concrete ManagementClient implements IDisposable and IAsyncDisposable; the IManagementClient
interface does not, because a resolved client belongs to the container. Invalid options throw
OptionsValidationException from either entry point.
The builder exposes Options (an OptionsBuilder<ManagementOptions>), HttpClient and
SubscriptionHttpClient (the IHttpClientBuilder for each transport scope), ConfigureResilience, and
Services / Name. Whatever you chain runs after the SDK's own setup.
Note
IManagementClientFactory resolves a named client from your container. ManagementClient.Create
builds a standalone one. Similar names, different jobs.
Options and configuration binding
Bind from an IConfiguration section:
{
"ManagementOptions": {
"EnvironmentId": "<YOUR_ENVIRONMENT_ID>",
"ApiKey": "<YOUR_API_KEY>"
}
}
services.AddManagementClient(management =>
management.Options.Bind(configuration.GetSection("ManagementOptions")));
Options is a standard OptionsBuilder<ManagementOptions>, so the rest of the options pattern is
available unchanged:
Instead of Bind | Use |
|---|---|
| Section resolved from the container by name | management.Options.BindConfiguration("Management:Production") |
| A pre-built options instance | services.AddManagementClient(new ManagementOptions { … }) |
| Values from another registered service | management.Options.Configure<ISecretStore>((options, secrets) => …) |
| Validation beyond the built-in rules | management.Options.Validate(options => …) |
A pre-built instance is copied onto the options the container materializes, so mutating it afterwards has no effect once they have been read.
Named clients
Give each registration a name to run more than one environment:
services.AddManagementClient("production", management => management.Options.Configure(options =>
{
options.EnvironmentId = "<PRODUCTION_ENVIRONMENT_ID>";
options.ApiKey = "<PRODUCTION_API_KEY>";
}));
services.AddManagementClient("staging", management => management.Options.Configure(options =>
{
options.EnvironmentId = "<STAGING_ENVIRONMENT_ID>";
options.ApiKey = "<STAGING_API_KEY>";
}));
Resolve them through IManagementClientFactory:
var production = clientFactory.Get("production");
var staging = clientFactory.Get("staging");
Names are compared ordinally. A client needing different credentials — a subscription key, for instance — is a separate named registration, not a second key on an existing one.
Configuration options
| Option | Required | Default | Description |
|---|---|---|---|
EnvironmentId | For environment endpoints | — | The GUID of your Kontent.ai environment. Required for everything except subscription-scoped endpoints. |
ApiKey | Yes | — | A Management API key for environment-scoped endpoints, or a Subscription API key for subscription-scoped ones. They are different keys — see subscription-scoped operations. |
SubscriptionId | For subscription endpoints | — | The subscription GUID. Required only for subscription-scoped endpoints such as user management. |
EnableResilience | No | true | Toggles the built-in retry/backoff pipeline without uninstalling it. |
Timeout | No | 30 minutes | Ceiling on one call, covering every retry attempt and the waits between them. |
Endpoint | No | https://manage.kontent.ai | The Management API base address. Override only when targeting a non-production endpoint. |
Options are validated on use. A missing ApiKey, a malformed identifier, or neither EnvironmentId nor
SubscriptionId surfaces as OptionsValidationException — from the constructor and Create, or at host
startup when the container validates.
Each scope is built only when its identifier is present, so a client scoped to one and used for the other fails immediately, naming the missing option, rather than sending a request to a path with an empty segment.
Retries and timeouts
Every client comes with a resilience pipeline
(Microsoft.Extensions.Http.Resilience):
exponential backoff with jitter, Retry-After handling, and idempotency-aware retries.
A 429 is retried for every method, because the request was rejected rather than applied. Other
transient failures — 408, 5xx, transport errors — are retried only for idempotent methods (GET,
HEAD, OPTIONS, PUT, DELETE). A POST or PATCH that fails mid-flight is never replayed, because
the write may already have landed.
Set EnableResilience = false to make the pipeline a passthrough.
Tuning retries
Use TuneRetry to adjust the initialized default HttpRetryStrategyOptions before the SDK assembles
the pipeline:
services.AddManagementClient(management =>
{
management.Options.Configure(options => { options.EnvironmentId = "…"; options.ApiKey = "…"; });
management.TuneRetry(retry => retry.MaxRetryAttempts = 5);
});
Changing the attempt count preserves the idempotency rule, Retry-After handling, backoff and jitter.
Settings you leave alone follow the defaults of the SDK version you install. Assigning ShouldHandle
or DelayGenerator explicitly replaces that part of the retry behavior; replacing ShouldHandle
means taking responsibility for write safety.
Callbacks run in registration order with fresh options for each pipeline construction, on both the
environment and subscription transports. The same hook works in ManagementClient.Create.
It does not run when EnableResilience is false or ConfigureResilience replaces the pipeline.
Management still has no default per-attempt timeout.
Replacing the pipeline
ConfigureResilience replaces the entire pipeline on both transports, including the idempotency rule.
Use it when you need different strategies or ordering. Prefer TuneRetry for retry settings.
A bare new HttpRetryStrategyOptions() retries transient failures on every method, including POST
and PATCH. DisableFor(HttpMethod.Post, HttpMethod.Patch) prevents those replays but also prevents
retrying rate-limited writes; it is not equivalent to the SDK's rule.
Timeouts
Two clocks bound a request:
- Per attempt — no default. Asset and file uploads can legitimately run long, and cutting one off
only to retry it re-uploads the same bytes. Add one through
ConfigureResilienceif you want it. - The whole call —
ManagementOptions.Timeout, covering every attempt and the waits between them. Defaults to 30 minutes, sized against the 2 GB asset limit.
Prefer Timeout for changing the total budget; replacing the pipeline is not the way to set one. Use
Timeout.InfiniteTimeSpan to be bounded only by your CancellationToken. Timeout outranks
Retry-After: the pipeline waits exactly as long as the server asked, but the call is still cut short
if your ceiling runs out first.
Uploads have their own retry-safety rules — see supported sources and ownership.
HTTP handlers
Each transport scope has its own IHttpClientBuilder:
management.HttpClient.AddHttpMessageHandler<MyAuditingHandler>(); // environment-scoped
management.SubscriptionHttpClient.AddHttpMessageHandler<MyAuditingHandler>(); // subscription-scoped
Source tracking for tool authors
Every request carries two analytics headers:
X-KC-SDKID— identifies this SDK. Alwaysnuget.org;Kontent.Ai.Management;<version>. Not configurable.X-KC-SOURCE— identifies a library built on top of the SDK. Set only when a caller assembly opts in.
End-user applications need do nothing here. If you publish a library that wraps this SDK, add the attribute at assembly level; at request time the SDK walks the call stack, finds your assembly and reads it:
using Kontent.Ai.Management.Attributes;
[assembly: SourceTrackingHeader]
Two overloads override what it reports: SourceTrackingHeader("Acme.AwesomeTool") when your package id
differs from your assembly name, and SourceTrackingHeader("Acme.AwesomeTool", 1, 2, 3, "beta") to pin
the version too. Use one, not all three.