C# SDK

Strongly-typed .NET clients for the 4SEE Integrations API. The SDK exchanges your API key for a JWT, caches and refreshes it, follows cursor pagination, and retries transient failures so you can call the API directly.

Additional language SDKs are planned. Contact support if you need one sooner.

Install

Two packages are published. Install the REST client on its own if you only need request/response queries; add the LiveFeed package when you also want the real-time gRPC push stream. They are separate so REST-only consumers do not pull in gRPC and protobuf dependencies.

PackageUse it for
Grote.Daas.ClientREST access to fleets, trailers, devices, DeliverSafe, and telemetry
Grote.Daas.LiveFeedReal-time streaming trailer and telemetry updates over gRPC
Shell
# REST client
dotnet add package Grote.Daas.Client

# Optional: real-time gRPC live feed
dotnet add package Grote.Daas.LiveFeed
Pin the package version you install so a later release cannot change behavior under you. The package listing is the source of truth for the current version and the frameworks it targets.

Configure & authenticate

Authentication is two-step: you hold a long-lived API key, and the SDK exchanges it for a short-lived JWT against the Grote auth server, then calls the data API with that token. The token is cached and refreshed automatically, so you never handle the JWT yourself.

Set Audience to match the key you were issued. Use Customer for a fleet-owner key (the default) or Partner for an integration-partner key. The wrong audience fails authentication at the auth server.

C#Dependency injection (recommended)
using Grote.Daas.Client;

// Resolve the key up front so a missing value fails with a clear message instead of
// silently overwriting the environment-variable fallback with null.
var apiKey = builder.Configuration["DAAS_API_KEY"];
if (string.IsNullOrWhiteSpace(apiKey))
    throw new InvalidOperationException("DAAS_API_KEY is not configured.");

builder.Services.AddDaas(options =>
{
    options.ApiKey   = apiKey;
    options.Audience = DaasAudience.Customer;   // or DaasAudience.Partner
});

Then inject IDaasClient anywhere. Registration goes through IHttpClientFactory and keeps the auth handler as a singleton, so the cached JWT is shared across requests.

C#
public sealed class FleetReporter(IDaasClient daas)
{
    public Task<Fleet> GetFleetAsync(int id, CancellationToken ct)
        => daas.Fleets.GetAsync(id, ct);
}

For a console app, worker, or script, construct the client directly. It owns its HttpClient, so dispose it when you are done.

C#Standalone, without DI
using Grote.Daas.Client;

using var client = new DaasClient(new DaasClientOptions
{
    ApiKey = Environment.GetEnvironmentVariable("DAAS_API_KEY")!
});
ApiKey reads the DAAS_API_KEY environment variable when the options are created, so leaving it alone picks the key up from the environment. Assigning it always wins, including when the value you assign is empty: binding it from configuration that has no such key overwrites the fallback with null and registration then fails even though the environment variable is set. Assign it only from a source you know is populated, or leave it unset and let the environment supply it.
Keep API keys in user secrets, environment variables, or a secret store, never in source control.

Your first calls

The client exposes four resource groups: Fleets, Trailers, Devices, and DeliverSafe. Each returns strongly-typed models, and each throws on failure rather than returning an error object.

C#
using Grote.Daas.Client;
using Grote.Daas.Client.Models;

using var client = new DaasClient(new DaasClientOptions
{
    ApiKey = Environment.GetEnvironmentVariable("DAAS_API_KEY")!
});

// A single fleet by id.
Fleet fleet = await client.Fleets.GetAsync(1234);

// One page of trailers for that fleet.
OffsetResponse<Trailer> page = await client.Trailers.ListAsync(fleet: 1234, take: 50);
Console.WriteLine($"{page.Count} trailers, more available: {page.HasMore}");

// Every faulted trailer, following pagination automatically.
await foreach (Trailer trailer in client.Trailers.StreamAsync(
                   fleet: 1234, status: TrailerStatusFilter.Fault))
{
    Console.WriteLine($"{trailer.UnitId}: {trailer.Status?.Name}");
}

// Devices on a trailer, then that device's recent telemetry.
await foreach (Device device in client.Devices.StreamAsync(trailer.Id))
{
    DeviceDetails details = await client.Devices.GetDetailsAsync(device.Id);
    Console.WriteLine($"{details.Id}: {details.TroubleCodes?.Count ?? 0} trouble codes");
}

Paging

Every list and telemetry endpoint comes in two forms. A List method returns one OffsetResponse<T> page that you page through yourself, and a Stream method returns an IAsyncEnumerable<T> and walks every page for you. Prefer the Stream form unless you need control over each request.

Member of OffsetResponse<T>Meaning
DataThe items on this page, as IReadOnlyList<T>
CountNumber of items in Data
NextCursorOpaque token for the next page, or null when there are no more results
HasMoreTrue when NextCursor is not null
C#Manual paging
// Manual paging: pass the previous NextCursor back as `next`.
string? cursor = null;
do
{
    OffsetResponse<Trailer> page =
        await client.Trailers.ListAsync(fleet: 1234, next: cursor, take: 100);

    foreach (Trailer trailer in page.Data)
        Process(trailer);

    cursor = page.NextCursor;
}
while (cursor is not null);
C#Automatic paging
// Automatic paging: the SDK follows the cursor for you.
await foreach (Trailer trailer in client.Trailers.StreamAsync(fleet: 1234, take: 100))
    Process(trailer);
NextCursor is an opaque token. Its underlying meaning differs by endpoint (a record id for lists, a timestamp for telemetry), so treat it as a string and pass it back unmodified. When paging a filtered list, resend the same filters on every request.

Method reference

Every method takes an optional trailing CancellationToken. Defaults shown are the SDK defaults; page-size maximums are enforced by the API.

client.FleetsReturns
ListAsync(next?, take = 50)OffsetResponse<Fleet>
StreamAsync(take = 100)IAsyncEnumerable<Fleet>
GetAsync(int fleetId)Fleet
client.TrailersReturns
ListAsync(fleet?, next?, take = 50, activeOnly = true, status = Any, vin?, unitId?)OffsetResponse<Trailer>
StreamAsync(fleet?, activeOnly = true, status = Any, vin?, unitId?, take = 100)IAsyncEnumerable<Trailer>
GetAsync(string trailerId)Trailer
ListGpsAsync(trailerId, next?, take = 25, speedThreshold?)OffsetResponse<TelemetryDetails>
StreamGpsAsync(trailerId, speedThreshold?, take = 50)IAsyncEnumerable<TelemetryDetails>
client.DevicesReturns
ListAsync(trailerId, next?, take = 30, activeOnly = true)OffsetResponse<Device>
StreamAsync(trailerId, activeOnly = true, take = 60)IAsyncEnumerable<Device>
GetDetailsAsync(string deviceId)DeviceDetails, with latest readings and trouble codes
ListTelemetryAsync(deviceId, next?, take = 25)OffsetResponse<DeviceTelemetry>
StreamTelemetryAsync(deviceId, take = 50)IAsyncEnumerable<DeviceTelemetry>
client.DeliverSafeReturns
ListAsync(fleet?, next?, take = 50, activeOnly = true, vin?, unitId?)OffsetResponse<DeliverSafe>
StreamAsync(fleet?, activeOnly = true, vin?, unitId?, take = 100)IAsyncEnumerable<DeliverSafe>
GetAsync(string id)DeliverSafe
ListTelemetryAsync(id, next?, take = 25, speedThreshold?)OffsetResponse<DeliverSafeTelemetry>
StreamTelemetryAsync(id, speedThreshold?, take = 50)IAsyncEnumerable<DeliverSafeTelemetry>
Trailer status filtering uses the TrailerStatusFilter enum. TrailerStatusFilter.Any applies no filter; Fault and Warning map to the API's supported status values.

Error handling

Failures throw a DaasException subclass carrying the HTTP status code and, when the API returned one, the parsed ProblemDetails. Catch the specific type you care about, or DaasException to handle everything.

ExceptionRaised on
DaasAuthorizationExceptionData API rejected the call with 401/403
DaasNotFoundException404 Not Found
DaasValidationException400, 422; exposes per-field Errors
DaasRateLimitException429 Too Many Requests
DaasServerException500 Internal Server Error
DaasAuthenticationExceptionThe API key to JWT exchange failed at the auth server
C#
using Grote.Daas.Client;

try
{
    Trailer trailer = await client.Trailers.GetAsync(trailerId, ct);
}
catch (DaasNotFoundException)
{
    // Unknown trailer, or one outside your fleet. See the note below.
}
catch (DaasValidationException ex)
{
    foreach (var (field, messages) in ex.Errors)
        Console.WriteLine($"{field}: {string.Join(", ", messages)}");
}
catch (DaasRateLimitException)
{
    // The SDK already retried with backoff; treat this as a hard throttle.
}
catch (DaasException ex)
{
    Console.WriteLine($"4SEE call failed ({ex.StatusCode}): {ex.Message}");
}
A get-by-id returns 404 both when the resource does not exist and when it belongs to a fleet you cannot access. The two are intentionally indistinguishable so resource existence is not leaked across fleets.

Options & resilience

DaasClientOptionsDefault
ApiKeyDAAS_API_KEY environment variable
BaseUrlhttps://4see.groteintegrations.com/
AuthUrlhttps://auth.groteintegrations.com/
AudienceCustomer (or Partner)
Timeout30 seconds
RetryEnabled: 3 retries, 500ms base delay, 10s cap
PrimaryHandlerFactoryNone; supply one for a proxy, custom handler, or tests

The client retries transient failures (429, 502, 503, 504, and network errors) on idempotent GETs using bounded exponential backoff with jitter. It is on by default and tunable.

C#
builder.Services.AddDaas(o =>
{
    o.ApiKey           = apiKey;
    o.Retry.MaxRetries = 3;                              // default
    o.Retry.BaseDelay  = TimeSpan.FromMilliseconds(500);
    o.Retry.MaxDelay   = TimeSpan.FromSeconds(10);
    // o.Retry.Enabled = false;                           // to own resilience yourself
});

To bring your own resilience pipeline, the DI overload takes a configureHttp hook exposing the IHttpClientBuilder. Handlers you add there run closest to the network, which puts them inside the SDK's built-in auth and retry handlers.

Turn the built-in retry off when you add your own. Because your handler sits inside it, leaving both enabled nests one retry loop within the other and the attempt counts multiply: 3 built-in retries around 5 of your own is up to 24 requests for a single call. Set Retry.Enabled = false and own the behavior completely, or leave the built-in retry alone and do not add a second one.
C#Bring your own Polly pipeline
builder.Services.AddDaas(
    o =>
    {
        o.ApiKey        = apiKey;
        o.Retry.Enabled = false;   // required: your pipeline replaces the built-in retry
    },
    http => http.AddResilienceHandler("daas", b => b
        .AddRetry(new HttpRetryStrategyOptions { MaxRetryAttempts = 5 })
        .AddTimeout(TimeSpan.FromSeconds(10))));
Retry responsibility is all or nothing. If you disable the built-in retry, your pipeline is responsible for backoff on 429 and 5xx responses, which the API returns without a Retry-After hint, so choose your own delays.

Real-time LiveFeed

The Grote.Daas.LiveFeed package subscribes to the gRPC live feed and fans updates out to your handlers. Authentication, heartbeat filtering, and reconnect are handled for you, so there is no connection code or stream loop in your application. It uses the same API key and audience model as the REST client.

C#Register the feed and its handlers
using Grote.Daas.LiveFeed;

builder.Services
    .AddDaasLiveFeed(options =>
    {
        options.ApiKey   = apiKey;   // resolved as shown under Configure & authenticate
        options.Audience = DaasAudience.Customer;
    })
    .AddHandler<TrailerFaultAlerter>()
    .AddHandler<TelemetryArchiver>()
    .OnUpdate((update, services, ct) =>
    {
        Console.WriteLine($"{update.Trailer.UnitId} @ {update.Ts}");
        return Task.CompletedTask;
    });

A handler is a plain class, constructor-injected like any other service. Each update is dispatched to every registered handler inside a fresh DI scope, so handlers can depend on scoped services such as an EF Core DbContext. If one handler throws, the failure is logged, the other handlers still run, and the stream keeps going.

C#A handler
public sealed class TrailerFaultAlerter : ILiveFeedHandler
{
    private readonly ILogger<TrailerFaultAlerter> _log;

    public TrailerFaultAlerter(ILogger<TrailerFaultAlerter> log) => _log = log;

    public Task HandleAsync(LiveUpdate update, CancellationToken ct)
    {
        if (update.Trailer.Status?.IsFault == true)
            _log.LogWarning("Trailer {Unit} faulted", update.Trailer.UnitId);

        return Task.CompletedTask;
    }
}

Reconnect itself is not optional, but the backoff is tunable, and you can observe the connection lifecycle to flip a health flag or reconcile through the REST client.

C#Lifecycle and backoff
builder.Services
    .AddDaasLiveFeed(o =>
    {
        o.ApiKey                 = apiKey;
        o.InitialReconnectDelay  = TimeSpan.FromSeconds(2);
        o.ReconnectBackoffFactor = 2.0;
        o.MaxReconnectDelay      = TimeSpan.FromSeconds(60);
    })
    .OnConnected((ctx, sp, ct) => Task.CompletedTask)
    .OnDisconnected((ctx, sp, ct) => Task.CompletedTask)   // ctx: Error, Attempt, WillRetry, NextDelay
    .OnReconnecting((ctx, sp, ct) => Task.CompletedTask);  // ctx: Attempt, Delay, LastError
DaasLiveFeedOptionsDefault
ApiKeyDAAS_API_KEY environment variable
BaseUrlhttps://4see.groteintegrations.com/
AuthUrlhttps://auth.groteintegrations.com/
AudienceCustomer (or Partner)
DispatchModeSequential (or Parallel for concurrent handlers)
InitialReconnectDelay2 seconds
ReconnectBackoffFactor2.0
MaxReconnectDelay60 seconds
AuthTimeout30 seconds
KeepAlivePingDelay / KeepAlivePingTimeout25s / 20s

If you would rather own the loop, use the low-level client directly. Heartbeats are still filtered, but there is no automatic reconnect at this level.

C#Low-level escape hatch
using var client = new DaasLiveFeedClient(new DaasLiveFeedOptions { ApiKey = apiKey });

await foreach (LiveUpdate update in client.SubscribeAsync(ct))
{
    // Your logic. Heartbeats are already filtered out.
}
Both packages declare a DaasAudience enum in their own namespace. If you register the REST client and the live feed in the same file, qualify them (Grote.Daas.Client.DaasAudience and Grote.Daas.LiveFeed.DaasAudience) or alias one.

Best practices

Need help?

Questions about the SDK, onboarding, or requesting an API key?