Linkit
SDK Reference

C# / .NET (Modern)

Production-grade C# SDK for the Linkit API — .NET 10, System.Text.Json source generation, IAsyncEnumerable, zero external dependencies.

C# / .NET SDK (Modern)

Package

sa.linkit.LinkitDotNet · .NET 10+ · System.Text.Json · Zero external dependencies

Also Available: .NET Framework 4.8 (Legacy)

Need to target legacy .NET Framework? See the C# / .NET Legacy SDK — same API surface, backed by Newtonsoft.Json and Task-based async patterns.


Modern vs Legacy at a Glance

FeatureLinkitDotNet (Modern)LinkitDotNetLegacy
Target.NET 10+.NET Framework 4.8
JSONSystem.Text.Json source genNewtonsoft.Json cached settings
StreamingIAsyncEnumerableTask-based batch processing
External DepsZeroNewtonsoft.Json, polyfills
NuGet PackageLinkitDotNetLinkitDotNetLegacy
ConfigureAwaitNot neededRequired (ConfigureAwait(false))
Allocations3 per create op5 per create op
NamespaceLinkitDotNet.*LinkitDotNetLegacy.*

Installation

nuget install LinkitDotNet
dotnet add package LinkitDotNet
<PackageReference Include="LinkitDotNet" Version="0.0.4" />

Quick Start

Initialize the Client

using LinkitDotNet.Client;
using LinkitDotNet.Configuration;

await using var client = await LinkitClient.CreateClientAsync(
    config => config
        .WithBaseUrl("https://linkit.works/api/v1")
        .WithTimeout(TimeSpan.FromSeconds(30))
        .WithRetryPolicy(retry => retry.MaxRetries = 3),
    jwtToken: "your-jwt-token"
);

Create a Product

var product = await client.Products()
    .Create()
    .WithId("PROD-001")
    .WithName("Premium Widget", "ويدجت متميز")
    .WithPrice(99.99, vatPercentage: 0.15)
    .WithBarcode("1234567890123")
    .EnableQuickCommerce()
    .AsEnabled()
    .ExecuteAsync();

Manage SKU Inventory

// SKUs are auto-generated — never create manually, only update
await client.Skus()
    .UpdateStock("AUTO-SKU-ID", "BR-RIYADH-001")
    .SetQuantity(500)
    .SetAvailability(true)
    .ExecuteAsync();

Stream Large Datasets (Modern-Only Feature)

// IAsyncEnumerable — memory-efficient pagination
await client.Products()
    .Stream()
    .Where(p => p.AveragePrice > 1000)
    .WithConcurrency(10)
    .Process(async product => {
        await ProcessProductAsync(product);
    })
    .ProcessAllAsync();

Legacy Alternative

The Legacy SDK uses ProcessInBatches() with Task-based patterns instead of IAsyncEnumerable.


Configuration

await using var client = await LinkitClient.CreateClientAsync(
    config => config
        .WithBaseUrl("https://linkit.works/api/v1")
        .WithTimeout(TimeSpan.FromMinutes(2))
        .WithMaxConcurrentRequests(100)
        .WithRetryPolicy(retry => {
            retry.MaxRetries = 5;
            retry.InitialDelay = TimeSpan.FromMilliseconds(200);
            retry.BackoffMultiplier = 2.0;
            retry.MaxDelay = TimeSpan.FromSeconds(30);
        })
        .WithDefaultHeader("X-Client-Version", "1.0.0")
        .WithDefaultHeader("X-Correlation-Id", Guid.NewGuid().ToString())
        .WithRequestLogging(
            onBeforeRequest: req => logger.LogTrace("[Outbound] {Method} {Uri}", req.Method, req.RequestUri),
            onAfterResponse: res => logger.LogTrace("[Inbound] {StatusCode}", res.StatusCode))
        .OnError(error => logger.LogError(error, "Client error occurred"))
        .OnInitialized(() => logger.LogInformation("Linkit client initialized successfully")),
    jwtToken,
    errorHandler => errorHandler
        .WithCircuitBreaker(failureThreshold: 10, resetTimeout: TimeSpan.FromMinutes(2))
        .WithRetryPolicy(maxRetries: 3, retryDelay: TimeSpan.FromSeconds(1)),
    logger
);

Services Reference

ServiceMethodsDescription
client.Products()Create, Query, Update, Delete, StreamCentral product catalog
client.Skus()Query, Update, UpdateStockStock Keeping Units per branch (update-only)
client.Branches()Create, Query, Update, Delete, FindNearbyPhysical locations
client.Customers()Create, Query, Search, Lookup, Stream, GroupsCustomer profiles
client.Orders()Query, UpdateStatusOrder management
client.Offers()Create, Query, Update, UpdateStatus, Delete, BulkUpsertPromotions & discounts
client.Brands()Create, Query, Update, DeleteBrand management
client.Categories()Create, Query, Update, DeleteProduct categories
client.Generics()Create, Query, Update, DeleteGeneric entities
client.Health()CheckAsync, PingAsyncSystem health monitoring
client.Integrations()GetByIdAsync, ExecuteAsync, EnableAsync, DisableAsyncPlatform connections

Products

Create

var product = await client.Products()
    .Create()
    .WithId("PROD-001")
    .WithName("Premium Widget", "ويدجت متميز")
    .WithPrice(99.99, vatPercentage: 0.15)
    .WithBarcode("1234567890123")
    .WithAttributes(attr => attr
        .IsFastMoving()
        .Buffer(25)
        .SitemapPriority(0.9))
    .EnableQuickCommerce()
    .AsEnabled()
    .ExecuteAsync();

Query with Filtering

var products = await client.Products()
    .Query()
    .SearchFor("laptop")
    .EnabledOnly()
    .FastMovingOnly()
    .QuickCommerceOnly()
    .Page(1)
    .Take(50)
    .OrderBy("-created")
    .WithMetadata("query_type", "inventory_check")
    .ExecuteAsync();

Stream (IAsyncEnumerable)

await client.Products()
    .Stream()
    .Where(p => p.AveragePrice > 1000)
    .WithConcurrency(10)
    .Process(async product => {
        await ProcessProductAsync(product);
    })
    .ProcessAllAsync();

SKU Operations (Update Only)

Important

SKUs are auto-generated when you create products and branches. Never create SKUs manually — only update existing ones.

// Update stock levels
await client.Skus()
    .UpdateStock("EXISTING-SKU-ID", "BRANCH-ID")
    .SetQuantity(150)
    .SetAvailability(true)
    .WithMetadata("reason", "restocking")
    .ExecuteAsync();

// Update SKU with marketplace IDs
await client.Skus()
    .Update("EXISTING-SKU-ID", "BRANCH-ID")
    .WithPrice(899.99)
    .WithStock(quantity: 200, maxQuantity: 1000)
    .WithReorderThreshold(50)
    .WithMarketplaceIds(ids => ids
        .Amazon("AMZ-SKU-123", "B08N5WRWNW")
        .Noon("N123456789", "Z87654321")
        .Salla("SALLA-987")
        .Zid("ZID-654"))
    .ExecuteAsync();

Branches

// Create
var branch = await client.Branches()
    .Create()
    .WithId("STORE-001")
    .WithName("Downtown Store", "متجر وسط المدينة")
    .AtLocation(25.2048, 55.2708)
    .WithStatus(BranchStatus.Published)
    .AsActive()
    .WithWorkingHours(hours => hours
        .Monday("09:00", "21:00")
        .Friday("14:00", "22:00")
        .Saturday("10:00", "20:00")
        .ClosedOn(DayOfWeek.Sunday))
    .ExecuteAsync();

// Find nearby
var nearbyBranches = await client.Branches()
    .FindNearby(lat: 25.2048, lon: 55.2708)
    .WithinRadius(10)
    .ActiveOnly()
    .WithStatus(BranchStatus.Published)
    .ExecuteAsync();

Customers

// Create
var customer = await client.Customers()
    .Create()
    .WithName("Ahmad", "Hamdi")
    .WithEmail("ahmad@example.com")
    .WithPhone("+966500000000")
    .AsType("vip")
    .WithStatus("active")
    .ExecuteAsync();

// Lookup
var found = await client.Customers()
    .Lookup()
    .ByEmail("ahmad@example.com")
    .ExecuteAsync();

// Address management
var address = await client.Customers()
    .ForCustomer(customer.Id)
    .CreateAddress()
    .WithLabel("Home")
    .WithAddress("123 Main Street", "Apt 4B")
    .InLocation("Riyadh", "Riyadh")
    .InCountry("SA")
    .AsDefault()
    .ExecuteAsync();

Batch Operations

var batchResult = await client.Batch()
    .WithContinueOnError(true)
    .WithTimeout(TimeSpan.FromMinutes(5))
    .CreateProduct(p => p
        .WithId("BATCH-001")
        .WithName("Batch Product 1", "منتج دفعة 1")
        .WithPrice(99.99))
    .CreateBranch(b => b
        .WithId("BATCH-STORE")
        .WithName("Batch Store", "متجر دفعة")
        .AtLocation(25.2, 55.3))
    .ExecuteAsync();

Console.WriteLine($"Success: {batchResult.SuccessfulOperations}/{batchResult.TotalOperations}");

Error Handling

using LinkitDotNet.Exceptions;

try
{
    var sku = await client.Skus().Query().ForProduct("nonexistent").ExecuteAsync();
}
catch (LinkitNotFoundException ex)
{
    Console.WriteLine($"Not found: {ex.ResourceId}");
}
catch (LinkitRateLimitException ex)
{
    Console.WriteLine($"Rate limited — retry after {ex.RetryAfterMs}ms");
}
catch (LinkitValidationException ex)
{
    foreach (var error in ex.Details)
        Console.WriteLine($"  {error.Key}: {error.Value}");
}
catch (LinkitApiException ex)
{
    Console.WriteLine($"API error {ex.StatusCode}: {ex.Message}");
}

Exception Hierarchy

ExceptionStatusDescription
LinkitExceptionBase for all SDK errors
LinkitInitializationExceptionInvalid configuration
LinkitApiExceptionAnyAPI error responses
LinkitAuthenticationException401Invalid credentials
LinkitAuthorizationException403Insufficient permissions
LinkitNotFoundException404Resource not found
LinkitValidationException400Request validation failed
LinkitConflictException409State conflict
LinkitRateLimitException429Rate limit exceeded
LinkitServerException5xxServer-side error
LinkitSerializationExceptionJSON parse failure

Resilience

Zero Configuration Required

The resilience pipeline is active by default. Transient errors (5xx, 429, timeouts) are automatically retried with exponential backoff + jitter. The circuit breaker trips after 5 consecutive failures and recovers after 60 seconds.

Override defaults via configuration:

config => config
    .WithRetryPolicy(retry => {
        retry.MaxRetries = 5;
        retry.InitialDelay = TimeSpan.FromMilliseconds(200);
        retry.BackoffMultiplier = 2.0;
    })
    .WithMaxConcurrentRequests(20)

Performance

OperationAllocationsMean TimeMemory
Create Product342.3 ms4.2 KB
List Products (100)578.6 ms11.8 KB
Update SKU Stock221.7 ms1.9 KB
Stream Products (1000)12398.4 ms17.2 KB
Batch Operation (10)8156.2 ms8.6 KB

Benchmarks run on .NET 10.0, Intel i7-12700K, 32GB RAM

Why Modern is Faster

  • Source-generated JSON: Zero-reflection System.Text.Json serialization eliminates startup overhead
  • IAsyncEnumerable: Streams pages on-demand rather than buffering entire datasets
  • No external deps: Avoids Newtonsoft.Json's heavier allocation patterns
  • await using: Native async disposal without ConfigureAwait ceremony