Kontent.Ai.Sync
2.0.0
dotnet add package Kontent.Ai.Sync --version 2.0.0
NuGet\Install-Package Kontent.Ai.Sync -Version 2.0.0
<PackageReference Include="Kontent.Ai.Sync" Version="2.0.0" />
<PackageVersion Include="Kontent.Ai.Sync" Version="2.0.0" />
<PackageReference Include="Kontent.Ai.Sync" />
paket add Kontent.Ai.Sync --version 2.0.0
#r "nuget: Kontent.Ai.Sync, 2.0.0"
#:package Kontent.Ai.Sync@2.0.0
#addin nuget:?package=Kontent.Ai.Sync&version=2.0.0
#tool nuget:?package=Kontent.Ai.Sync&version=2.0.0
Kontent.ai Sync SDK for .NET
Official .NET SDK for the Kontent.ai Sync API v2.
Use this SDK to initialize sync and process delta updates for content items, content types, languages, and taxonomies.
This SDK targets Sync API v2 exclusively. Sync API v1 is deprecated and not supported.
Table of Contents
- Installation
- Upgrade Guide
- Quick Start
- What a delta page contains
- Configuration
- Standalone client (without DI)
- Named Clients
- Error Handling
- Token Persistence
- Source Tracking (for Tool Authors)
- Contributing
- License
Installation
dotnet add package Kontent.Ai.Sync
The SDK targets net10.0. See the changelog for what each release changed.
Upgrade Guide
- Coming from 1.0 — see the 1 → 2 upgrade guide. Guides are kept one per major under
docs/upgrade/. The two changes that need real work are the .NET 10 move and paging, which is now a stream you enumerate. - Coming from the sync methods that used to live in
Kontent.Ai.Delivery— those were removed in Delivery 19.0. Move toKontent.Ai.Syncby following its Quick Start: sync has its own client, and calls return anISyncResultrather than throwing.
Quick Start
1. Register the sync client
using Kontent.Ai.Sync;
services.AddSyncClient(sync => sync.Options.Configure(options =>
{
options.EnvironmentId = "your-environment-id";
options.UsePreviewApi("your-preview-api-key");
}));
AddSyncClient hands you a builder for the one client being registered. Options is its
OptionsBuilder<SyncOptions>, so everything the options pattern offers - Configure, Bind,
BindConfiguration, Validate - is available without the SDK wrapping it; the rest of this README
shows the pieces as they come up.
2. Initialize sync
public sealed class SyncService(ISyncClient syncClient)
{
public async Task<string> InitializeAsync(CancellationToken cancellationToken = default)
{
var result = await syncClient.InitializeSyncAsync(cancellationToken);
if (!result.IsSuccess)
{
throw new InvalidOperationException(result.Error?.Message ?? "Sync init failed.");
}
// Persist and reuse this token for subsequent delta calls.
return result.SyncToken;
}
}
3. Fetch delta updates
var deltaResult = await syncClient.GetDeltaAsync(syncToken, cancellationToken);
if (!deltaResult.IsSuccess)
{
Console.WriteLine($"Sync failed: {deltaResult.Error?.Message} (request {deltaResult.Error?.RequestId})");
return;
}
var delta = deltaResult.Value;
foreach (var item in delta.Items)
{
Console.WriteLine($"{item.Timestamp:u} {item.ChangeType} {item.Data.System.Codename}");
}
await SaveSyncTokenAsync(deltaResult.SyncToken);
4. Walk every page
EnumerateDeltaAsync keeps requesting until the API reports an empty response, which is how it says
you have caught up. Requests are made as you iterate, so bound the walk with Take or by breaking out
of the loop — nothing is fetched ahead of you.
var token = syncToken;
await foreach (var page in syncClient.EnumerateDeltaAsync(syncToken, cancellationToken))
{
if (!page.IsSuccess)
{
Console.WriteLine($"Sync failed: {page.Error?.Message}");
break;
}
foreach (var item in page.Value.Items)
{
Console.WriteLine($"{item.Timestamp:u} {item.ChangeType} {item.Data.System.Codename}");
}
token = page.SyncToken;
}
// An empty sequence means there was nothing new, and the token you passed in is still current.
await SaveSyncTokenAsync(token);
What a delta page contains
Each of the four collections holds SyncChange<TData> entries sharing one envelope — what changed,
when, and the metadata:
| Member | |
|---|---|
ChangeType |
Changed or Deleted |
Timestamp |
when the change occurred in the Delivery API, UTC |
Data |
the entity's metadata, on every change — a deletion names what was deleted |
The payload differs per collection, because the API's does:
| Collection | Data |
Data.System |
|---|---|---|
Items |
SyncItemData |
id, collection, name, codename, language, type, last modified, workflow, workflow step |
Types |
SyncTypeData |
id, name, codename, last modified |
Taxonomies |
SyncTaxonomyData |
id, name, codename, last modified |
Languages |
SyncLanguageData |
id, name, codename |
Workflow and WorkflowStep are absent for components. A language carries no last-modified stamp,
which is why the four payloads are separate types rather than one.
Configuration
The builder passed to AddSyncClient (and to SyncClient.Create, below) exposes:
| Member | What it is |
|---|---|
Options |
The client's OptionsBuilder<SyncOptions> - Configure, Configure<TDependency>, Bind, BindConfiguration, Validate, PostConfigure |
HttpClient |
The IHttpClientBuilder the transport is built on - every Microsoft.Extensions.Http extension applies |
TuneRetry(...) |
Adjusts the default retry options |
ConfigureResilience(...) |
Replaces the default resilience pipeline |
Services, Name |
The service collection and the client's registration key, for anything you attach to this client yourself |
One step fits an expression lambda; several go in a statement lambda. Whatever you chain runs after the SDK's own setup, so it wins.
API modes
SyncOptions.ApiMode and ApiKey decide the API; the extension methods set the two together.
// Public Production API
services.AddSyncClient(sync => sync.Options.Configure(o =>
{
o.EnvironmentId = "your-environment-id";
o.UseProductionApi();
}));
// Preview API
services.AddSyncClient(sync => sync.Options.Configure(o =>
{
o.EnvironmentId = "your-environment-id";
o.UsePreviewApi("preview-api-key");
}));
// Secure Production API - a Delivery API key with secure access enabled
services.AddSyncClient(sync => sync.Options.Configure(o =>
{
o.EnvironmentId = "your-environment-id";
o.UseProductionApi("secure-access-api-key");
}));
UseCustomEndpoint(...) points both API modes at one endpoint. Single settings are plain properties:
EnableResilience, Timeout, ProductionEndpoint, PreviewEndpoint.
Configuration binding
appsettings.json:
{
"SyncOptions": {
"EnvironmentId": "your-environment-id",
"ApiMode": "Preview",
"ApiKey": "preview-api-key",
"EnableResilience": true
}
}
Bind the section, or - in a host, where IConfiguration is in the container - name it:
services.AddSyncClient(sync => sync.Options.Bind(configuration.GetSection("SyncOptions")));
services.AddSyncClient(sync => sync.Options.BindConfiguration("MySyncSection"));
The default section name is available as SyncOptions.DefaultConfigurationSectionName, so tooling that
resolves the SDK's configuration from the same sources does not have to hard-code it.
Binding this way is change-token backed: edits to the underlying source are picked up through
IOptionsMonitor<SyncOptions> without rebuilding the container. Binding and the other steps combine
freely:
services.AddSyncClient(sync =>
{
sync.Options.BindConfiguration("SyncOptions");
sync.TuneRetry(retry => retry.MaxRetryAttempts = 5);
});
A pre-built instance works too; its values are copied onto the options the container materializes, and the object itself is not registered:
services.AddSyncClient(new SyncOptions { EnvironmentId = "your-environment-id" }.UsePreviewApi("preview-api-key"));
Tuning retries
TuneRetry receives the SDK's initialized HttpRetryStrategyOptions before the default pipeline is
assembled. Change only the settings you need; the remaining defaults, including Retry-After handling
and the 30-second per-attempt timeout, stay in place. Replacing ShouldHandle or DelayGenerator
replaces that part of the retry behavior.
Callbacks run in registration order with fresh options for each pipeline construction, in both
AddSyncClient and SyncClient.Create. They do not run when EnableResilience is false or
ConfigureResilience replaces the pipeline. Tuning retains the default pipeline's timeout behavior.
Timeouts
Two clocks bound a request, and they are not the same one:
- Per attempt — the default resilience pipeline cancels any single HTTP attempt after 30 seconds and retries it on a fresh connection. Up to four attempts, each with its own budget.
- The whole call —
SyncOptions.Timeoutcovers every attempt and the waits between them.
Timeout is unset by default, which keeps the SDK's own rule: the default pipeline bounds each attempt,
so the call runs as long as its retries need; with EnableResilience = false or a pipeline of your own,
HttpClient's 100-second default applies, because nothing else is known to bound the request.
Set it and it always wins, whatever the pipeline:
services.AddSyncClient(sync => sync.Options.Configure(o =>
{
o.EnvironmentId = "your-environment-id";
o.Timeout = TimeSpan.FromMinutes(5);
}));
Timeout.InfiniteTimeSpan removes the ceiling outright. Note that it outranks Retry-After: when the API
rate-limits you, the pipeline waits exactly as long as the server asked, but the call is still cut short if
your ceiling runs out first.
Options from other registered services
When the options depend on something else in the container — a secret store, a tenant resolver — use
Configure<TDependency> on the options builder:
services.AddSyncClient(sync => sync.Options.Configure<ISecretStore>((options, secrets) =>
{
options.EnvironmentId = secrets.EnvironmentId;
options.ApiKey = secrets.SyncApiKey;
}));
The HTTP client
HttpClient is the named IHttpClientBuilder the SDK registered, after the SDK's own handlers are on
it. A handler you add is therefore innermost: a request reaches it after the SDK's own handlers, and a
retry runs it again. A primary handler you set replaces the SDK's:
services.AddSyncClient(sync =>
{
sync.Options.Configure(o => o.EnvironmentId = "your-environment-id");
sync.HttpClient.AddHttpMessageHandler<MyAuditingHandler>();
});
Standalone client (without DI)
For console apps, Azure Functions isolated workers, scripts, or tests where a container of your own is not
available, SyncClient.Create takes the same builder and runs the same registration inside a private
container the built client owns, so the client must be disposed.
await using var client = SyncClient.Create(sync => sync.Options.Configure(o =>
{
o.EnvironmentId = "your-environment-id";
o.UsePreviewApi("preview-api-key");
}));
var result = await client.InitializeSyncAsync();
Logging, resilience and anything else the container path can do are the same calls:
await using var client = SyncClient.Create(sync =>
{
sync.Services.AddSingleton(loggerFactory);
sync.Options.Configure(o => o.EnvironmentId = "env-id");
sync.TuneRetry(retry => retry.MaxRetryAttempts = 5);
});
SyncOptions.Timeout applies here too, and matters with a pipeline of your own:
await using var client = SyncClient.Create(sync =>
{
sync.Options.Configure(o =>
{
o.EnvironmentId = "your-environment-id";
o.Timeout = TimeSpan.FromMinutes(5);
});
sync.ConfigureResilience(pipeline => pipeline.AddTimeout(TimeSpan.FromMinutes(2)));
});
Without the Timeout line, supplying your own pipeline leaves HttpClient's 100-second default in
charge, which would cut the two-minute attempt short.
The returned client is thread-safe and should be used as a singleton for the lifetime of your
application. Each Create call builds an independent client that owns the private container it was
built over, which is why it is disposable — dispose it and no further request goes out;
the pooled connections close once the HTTP client factory releases the handler. A client resolved from
your own container is owned by that container instead, so there is nothing for you to dispose there.
ISyncClientFactory, below, is the other thing with a similar name: it resolves a named client from
your container, while Create builds a standalone one.
Named Clients
services.AddSyncClient("production", sync => sync.Options.Configure(o =>
{
o.EnvironmentId = "prod-environment-id";
o.UseProductionApi();
}));
services.AddSyncClient("preview", sync => sync.Options.Configure(o =>
{
o.EnvironmentId = "preview-environment-id";
o.UsePreviewApi("preview-api-key");
}));
public sealed class MultiEnvironmentService(ISyncClientFactory factory)
{
public ISyncClient ProductionClient => factory.Get("production");
public ISyncClient PreviewClient => factory.Get("preview");
}
The name is the only difference, so named clients bind from configuration the same way:
services.AddSyncClient("production", sync => sync.Options.BindConfiguration("Sync:Production"));
services.AddSyncClient("preview", sync => sync.Options.Bind(configuration.GetSection("Sync:Preview")));
Error Handling
A failed request is a result, not an exception. ISyncResult carries the outcome — success, error,
status, the continuation token — and ISyncResult<T> adds Value for the calls that return content.
InitializeSyncAsync returns the non-generic form, because initialization produces a token rather than
content; GetDeltaAsync and EnumerateDeltaAsync return the generic one.
Four things still throw:
- Cancellation — a cancelled call throws
OperationCanceledException, soTask.IsCanceledand cancellation handlers behave normally. - Programmer errors — a
nullargument throwsArgumentNullException. - Invalid configuration — validated when the client is built or registered, as
OptionsValidationException. - A successful response with no
X-Continuationheader —InvalidOperationException. There is no result worth handing back: you could read that page and then be unable to advance.
var result = await syncClient.GetDeltaAsync(syncToken);
if (!result.IsSuccess)
{
Console.WriteLine(result.Error?.Message);
Console.WriteLine(result.Error?.RequestId);
Console.WriteLine(result.Error?.ErrorCode);
Console.WriteLine(result.StatusCode);
return;
}
Important fields:
ISyncResult.StatusCode(HttpStatusCode)ISyncResult.ResponseHeadersISyncResult.RequestUrlISyncResult.SyncTokenISyncResult<T>.Value— the delta payload, on the calls that return contentIError.MessageIError.RequestIdIError.ErrorCode/IError.SpecificCodeIError.Exception
Token Persistence
The SDK does not persist sync tokens. Store SyncToken after every successful call and pass it into
the next GetDeltaAsync or EnumerateDeltaAsync call. Every successful response carries one, so it is
never null on a successful result. A successful response that omits it throws rather than handing back a
result you could not continue from.
Where you store it during a walk is a choice. Saving once after the loop means a crash part-way through reprocesses from the previous token — some changes arrive twice, none are missed. Saving after each page resumes closer to where you stopped. Saving before processing a page is the one variant that can lose work.
Source Tracking (for Tool Authors)
Every request the SDK sends carries two analytics headers:
X-KC-SDKID— identifies this SDK. Alwaysnuget.org;Kontent.Ai.Sync;<version>. Not configurable.X-KC-SOURCE— identifies a library built on top of the SDK. Set only when a caller assembly opts in. Omitted otherwise.
End-user applications need do nothing here. This matters only if you publish a library that wraps the Sync SDK. If you do, add one of these at assembly level (AssemblyInfo.cs, or a top-level file); at request time the SDK walks the call stack, finds your assembly and reads the attribute:
// Name and version from the assembly — the usual case.
[assembly: SyncSourceTrackingHeader]
// Override the name (your package id differs from your assembly name), version still from the assembly.
[assembly: SyncSourceTrackingHeader("Acme.Kontent.Ai.AwesomeTool")]
// Pin both, independent of assembly metadata.
[assembly: SyncSourceTrackingHeader("Acme.Kontent.Ai.AwesomeTool", 1, 2, 3, "beta")]
Contributing
Contributions are welcome. Use GitHub Issues for bug reports and feature requests, and open pull requests in this repository for code contributions.
License
Distributed under the MIT License — see LICENSE.md for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.12)
- Microsoft.Extensions.Http.Resilience (>= 10.10.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.12)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.12)
- Refit (>= 15.2.0)
- Refit.HttpClientFactory (>= 15.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.0 | 143 | 9/16/2026 |
| 2.0.0-rc.3 | 69 | 9/8/2026 |
| 2.0.0-rc.2 | 78 | 8/12/2026 |
| 2.0.0-rc.1 | 81 | 8/7/2026 |
| 1.0.1 | 186 | 9/2/2026 |
| 1.0.0 | 1,347 | 4/16/2026 |
| 1.0.0-rc1 | 131 | 2/24/2026 |
| 0.0.3 | 345 | 11/13/2025 |
| 0.0.2 | 187 | 11/7/2025 |
| 0.0.1 | 410 | 11/7/2025 |