.NET SDK
Consystence.Sdk is the official .NET client library for the Consystence API. It targets .NET 10, wraps the HTTP surface behind a set of strongly-typed clients, and is built for dependency injection (DI) first. The same library backs the csy CLI, which supplies its own credential and token-refresh behaviour through the SDK’s extension points.
The library is deliberately thin: it owns HTTP composition (a named HttpClient, auth and tenant message handlers, retry policy, JSON), and it mirrors the server’s wire contracts as SDK-local records so it never has to reference the server’s internal contract assemblies.
Installation
Section titled “Installation”The SDK is not yet published to a public feed — during the beta, reference the Consystence.Sdk project from the monorepo (backend/Sdk/Consystence.Sdk). Once published it will install as:
dotnet add package Consystence.SdkIt depends only on the Microsoft.Extensions.* DI/HTTP/Options packages and Polly (via Microsoft.Extensions.Http.Polly) for transient-fault handling — there is no heavyweight transitive footprint.
Registering the client
Section titled “Registering the client”Add the SDK to an IServiceCollection with AddConsystenceClient (in the Consystence.Sdk.Extensions namespace). Two overloads are provided.
Configure options inline:
using Consystence.Sdk.Extensions;
builder.Services.AddConsystenceClient(options =>{ // The server for the current context: an org's cell-direct host for /api routes, // or the account host for account commands. options.BaseUrl = "https://acme.consystence.com"; options.TenantCode = "acme";});Or bind from configuration. With no argument it binds the Consystence section, validates data annotations, and validates on start:
builder.Services.AddConsystenceClient(); // binds "Consystence"{ "Consystence": { "BaseUrl": "https://acme.consystence.com", "TenantCode": "acme" }}Either overload registers everything needed: the named ConsystenceApi HttpClient, the message handlers, the default credential provider, every typed client as a singleton, and the IConsystenceClient aggregate. Resolve and use it:
var client = serviceProvider.GetRequiredService<IConsystenceClient>();
var sites = await client.Sites.ListAsync(cancellationToken);Configuration options
Section titled “Configuration options”ConsystenceClientOptions (section name Consystence):
| Property | Default | Purpose |
|---|---|---|
BaseUrl | https://api.consystence.com | Base address of the named ConsystenceApi client (all typed clients). |
TenantCode | required | Sent on every API request as X-Tenant-Code. |
MaxRetryAttempts | 3 | Polly transient-error retry count (idempotency-gated — see the HTTP pipeline). |
InitialRetryDelay | 500 ms | First retry delay; backoff doubles each attempt. |
HttpTimeout | 30 s | Per-request timeout. |
RefreshTokenOverride | null | Host-supplied bearer-refresh callback — the only refresh mechanism. On expiry/401 the SDK invokes it with the current refresh token so the host can renew against the token’s issuer (e.g. the CIAM token_endpoint, or a site server’s POST /auth/site/refresh). When null, the SDK never refreshes. |
The aggregate client
Section titled “The aggregate client”IConsystenceClient exposes the typed clients as properties. Each is also independently injectable.
| Property | Interface | Route family | Purpose |
|---|---|---|---|
Setup | ISetupClient | GET /api/setup/status | Server setup status (mode, version, activation state). |
Admin | IAdminClient | /api/admin/* | Org/tenant administration, tenant YAML import/export and restore (RestoreTenantAsync — served on site binaries, unlike import/export), and org re-bootstrap (ReBootstrapTenantAsync). |
Sites | ISiteClient | /api/sites | Site CRUD, plus site-wide operational reads: active alarms (ReadAlarmsAsync) and the command-audit ledger (ReadCommandsAsync). |
Controllers | IControllerClient | /api/sites/{siteId}/controllers | Controller create/list/get/update/assign/decommission. |
Instances | IInstanceClient | /api/sites/{siteId}/instances | Device-instance create/list/get/update/delete, plus live reads: current tag values (ReadTagsAsync), a historian trend window (ReadTrendAsync), and the instance’s command audit (ReadCommandsAsync). |
Electrical | IElectricalClient | /api/sites/{siteId}/electrical | Power-distribution nodes, edges, and topology. |
Account | IAccountClient | /api/me, /api/tenants, /api/orgs/* (account host) | Account control-plane self-service (account-of-record, tenants, org/member/licence management). |
Config | IConfigClient | /api/admin/config, /api/admin/orgs/{slug}/config | Platform/org config store: list/set/unset, audit trail, reload (admin-gated). |
Ai | IAiClient | /api/ai | AI plane (org-admin-gated): provider health (HealthAsync) and test completions (CompleteAsync). |
TrendImports | ITrendImportClient | /api/historian/import-batches | Bulk historian import: SubmitAsync uploads a trend CSV + tag-mapping CSV (a duplicate-file_hash 409 resolves to the existing batch rather than failing); GetStatusAsync polls a batch. |
Authentication and credentials
Section titled “Authentication and credentials”The SDK separates which credential to send from how the HTTP pipeline sends it. This is the canonical surface for SDK auth — see the authentication model for the platform-wide picture.
The credential model
Section titled “The credential model”ClientCredentialcarries aClientAuthMechanism—None,Bearer, orApiKey— and, for the API-key case, the key secret.IClientCredentialProvider.GetCredential()returns the active credential per request.- The default
TokenManagerCredentialProviderreportsBearerwhenever the in-memoryTokenManagerholds a token, otherwiseNone.
The two mechanisms map directly onto the platform’s MultiAuth schemes:
- Bearer — a CIAM access token sent as
Authorization: Bearer …, backed byTokenManager, with automatic refresh. - ApiKey — a tenant API key (
csk_prefix) sent asX-Api-Key. Mutually exclusive withAuthorization, and never refreshed — the key is the credential.
To use a non-interactive API key, register your own IClientCredentialProvider after AddConsystenceClient (last registration wins). The SDK ships no static-key provider — the host defines its own wrapper (the CLI does the same with its internal CliCredentialProvider):
builder.Services.AddConsystenceClient(/* … */);
builder.Services.AddSingleton<IClientCredentialProvider>( _ => new StaticApiKeyProvider( ClientCredential.ForApiKey("csk_…")));
sealed record StaticApiKeyProvider(ClientCredential Credential) : IClientCredentialProvider{ public ClientCredential GetCredential() => Credential;}API keys are minted via the admin API; see API keys.
Token lifecycle
Section titled “Token lifecycle”The SDK has no login surface and no server-mediated refresh — the platform authenticates with OIDC against the CIAM identity provider (Entra External ID), and token acquisition belongs to the host. A host application (such as the CLI) drives the OIDC flow itself, places the resulting token in TokenManager (which computes ExpiresAt), and sets RefreshTokenOverride so refresh goes to the token’s actual issuer — the IdP token_endpoint for a CIAM token, or the site server’s POST /auth/site/refresh for a site-PIN session. RefreshTokenOverride is the only refresh mechanism: when it is null, the SDK never refreshes. See the authentication model.
The HTTP pipeline
Section titled “The HTTP pipeline”All typed clients use the named ConsystenceApi client, whose pipeline is:
flowchart LR A[Typed client<br/>e.g. Sites.ListAsync] --> B[TenantHeaderHandler<br/>adds X-Tenant-Code] B --> C[AuthenticatedHttpHandler<br/>X-Api-Key XOR Bearer] C --> D[Polly retry<br/>transient HTTP errors] D --> E[Consystence server]
TenantHeaderHandleraddsX-Tenant-Code: {TenantCode}to every request that does not already carry it.AuthenticatedHttpHandlerasks the credential provider for the active credential. On the API-key path it clears anyAuthorizationheader, setsX-Api-Key, and does no refresh or 401 retry. On the Bearer path it attachesAuthorization, and — if the token is expired or a401comes back and a refresh token exists — refreshes once (guarded by a semaphore) and resends the request.- Polly wraps the named client with
HandleTransientHttpErrorand exponentialWaitAndRetryAsyncbackoff governed byMaxRetryAttemptsandInitialRetryDelay. - Both the Polly retry and the post-
401resend are idempotency-gated:GET/HEAD/OPTIONS/TRACE,PUT, andDELETErequests may always be re-sent; aPOSTorPATCHis re-sent only when it declares an idempotency contract — a stableIdempotency-Keyheader the endpoint honours, or an endpoint whose duplicate-submission409resolves to the existing resource. UndeclaredPOST/PATCHrequests are never retried, so a transient failure cannot duplicate a side effect.
Working with the typed clients
Section titled “Working with the typed clients”var site = await client.Sites.CreateAsync( new SiteCreateRequest { Code = "CHPP", Name = "Coal Handling Prep Plant" }, cancellationToken);
var all = await client.Sites.ListAsync(cancellationToken);var one = await client.Sites.GetAsync("CHPP", cancellationToken);The /api/sites endpoints return the bare SiteInfo record directly (no {site} envelope). See the org and site model for what a site is.
Controllers
Section titled “Controllers”Controllers (the term Consystence uses, not “PLC”) are hand-created per site. The server mints each controller’s canonical ctl_… id — the request carries no id of your own. ControllerConnectionConfig defaults Port to 44818 (EtherNet/IP), and ControllerPlatform spans the supported families (ControlLogix, CompactLogix, Micro800, MicroLogix, PLCnext, Other, plus Vplc for a cloud-hosted virtual PLC).
var controller = await client.Controllers.CreateAsync(site.Id, new ControllerCreateRequest { Name = "Pump House Controller", Platform = ControllerPlatform.PLCnext, AssetNumber = "PP-01", Connection = new ControllerConnectionConfig { Protocol = "ethernet-ip", IpAddress = "10.0.4.10", }, }, cancellationToken);
// GET returns the device plus its live connection health.var details = await client.Controllers.GetAsync(site.Id, controller.Id, cancellationToken);// details.Device.AssetNumber, details.Health.StatusLicences
Section titled “Licences”Licence issuance and revocation live in the account console — the cell API no longer mints or revokes licences, and the SDK has no dedicated licence client. Licences are read-only through two surviving paths:
// Cell-direct: org details carry the licences inline.var org = await client.Admin.GetOrgAsync("acme", cancellationToken);// Account host: the org's licence pools and allocations.var licences = await client.Account.GetOrgLicencesAsync(orgId, cancellationToken);// licences.Pools, licences.Allocations, licences.AssignmentsSee licensing for the tier and seat-pool model.
Account
Section titled “Account”The account control-plane client targets the account host (the CLI scopes these to an account context with an account-audienced bearer).
var me = await client.Account.GetMeAsync(cancellationToken); // null if no account-of-record yetvar tenants = await client.Account.ListTenantsAsync(cancellationToken);GetMeAsync returns null on a 404 (authenticated, but no account row) rather than throwing — distinct from a 401, which throws.
IAccountClient also carries the org/member self-service family (ListOrgsAsync, CreateOrgAsync, GetOrgAsync, GetOrgLicencesAsync, RequestOrgTierAsync, ProvisionUnderOrgAsync, plus invite and member management — mutations are Owner-only server-side). It also exposes an internal operator surface, not intended for integrators.
JSON serialisation
Section titled “JSON serialisation”The SDK includes a source-generated JsonSerializerContext, ConsystenceSdkJsonContext, configured with JsonSerializerDefaults.Web and UseStringEnumConverter = true, so enums serialise as strings bidirectionally. It registers the account, licence, setup, site, and admin contract types for AOT-clean, reflection-free (de)serialisation.
The account client, the admin client’s org/member/domain endpoints (its tenant endpoints predate the context), and the static platform-status client read through the generated JsonTypeInfo (for example ConsystenceSdkJsonContext.Default.AccountMe); the remaining clients currently use a plain Web-defaults JsonSerializerOptions.
Defensive deserialisation
Section titled “Defensive deserialisation”The SDK includes guards (ReadJsonOrThrowAsync and EnsureNotHtmlAsync) that detect a non-JSON / HTML response — for example an SPA shell from a path that matched no route, or an endpoint the server’s baked mode does not serve — and surface a clean HttpRequestException naming the request URI, instead of a bare JsonException or a write that silently appears to succeed.
See also
Section titled “See also”- API overview and endpoint reference
- Authentication model and API keys
- The
csyCLI — the SDK’s primary host