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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Kontent.Ai.Sync" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Kontent.Ai.Sync" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Kontent.Ai.Sync" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Kontent.Ai.Sync --version 2.0.0
                    
#r "nuget: Kontent.Ai.Sync, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Kontent.Ai.Sync@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Kontent.Ai.Sync&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Kontent.Ai.Sync&version=2.0.0
                    
Install as a Cake Tool

Kontent.ai Sync SDK for .NET

Stable Latest Downloads

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

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 to Kontent.Ai.Sync by following its Quick Start: sync has its own client, and calls return an ISyncResult rather 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.Timeout covers 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, so Task.IsCanceled and cancellation handlers behave normally.
  • Programmer errors — a null argument throws ArgumentNullException.
  • Invalid configuration — validated when the client is built or registered, as OptionsValidationException.
  • A successful response with no X-Continuation header — 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.ResponseHeaders
  • ISyncResult.RequestUrl
  • ISyncResult.SyncToken
  • ISyncResult<T>.Value — the delta payload, on the calls that return content
  • IError.Message
  • IError.RequestId
  • IError.ErrorCode / IError.SpecificCode
  • IError.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. Always nuget.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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