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.
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.
| Package | Use it for |
|---|---|
Grote.Daas.Client | REST access to fleets, trailers, devices, DeliverSafe, and telemetry |
Grote.Daas.LiveFeed | Real-time streaming trailer and telemetry updates over gRPC |
# REST client
dotnet add package Grote.Daas.Client
# Optional: real-time gRPC live feed
dotnet add package Grote.Daas.LiveFeedAuthentication 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.
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.
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.
using Grote.Daas.Client;
using var client = new DaasClient(new DaasClientOptions
{
ApiKey = Environment.GetEnvironmentVariable("DAAS_API_KEY")!
});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.
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");
}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 |
|---|---|
Data | The items on this page, as IReadOnlyList<T> |
Count | Number of items in Data |
NextCursor | Opaque token for the next page, or null when there are no more results |
HasMore | True when NextCursor is not null |
// 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);// Automatic paging: the SDK follows the cursor for you.
await foreach (Trailer trailer in client.Trailers.StreamAsync(fleet: 1234, take: 100))
Process(trailer);Every method takes an optional trailing CancellationToken. Defaults shown are the SDK defaults; page-size maximums are enforced by the API.
| client.Fleets | Returns |
|---|---|
ListAsync(next?, take = 50) | OffsetResponse<Fleet> |
StreamAsync(take = 100) | IAsyncEnumerable<Fleet> |
GetAsync(int fleetId) | Fleet |
| client.Trailers | Returns |
|---|---|
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.Devices | Returns |
|---|---|
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.DeliverSafe | Returns |
|---|---|
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> |
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.
| Exception | Raised on |
|---|---|
DaasAuthorizationException | Data API rejected the call with 401/403 |
DaasNotFoundException | 404 Not Found |
DaasValidationException | 400, 422; exposes per-field Errors |
DaasRateLimitException | 429 Too Many Requests |
DaasServerException | 500 Internal Server Error |
DaasAuthenticationException | The API key to JWT exchange failed at the auth server |
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}");
}| DaasClientOptions | Default |
|---|---|
ApiKey | DAAS_API_KEY environment variable |
BaseUrl | https://4see.groteintegrations.com/ |
AuthUrl | https://auth.groteintegrations.com/ |
Audience | Customer (or Partner) |
Timeout | 30 seconds |
Retry | Enabled: 3 retries, 500ms base delay, 10s cap |
PrimaryHandlerFactory | None; 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.
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.
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))));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.
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.
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.
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| DaasLiveFeedOptions | Default |
|---|---|
ApiKey | DAAS_API_KEY environment variable |
BaseUrl | https://4see.groteintegrations.com/ |
AuthUrl | https://auth.groteintegrations.com/ |
Audience | Customer (or Partner) |
DispatchMode | Sequential (or Parallel for concurrent handlers) |
InitialReconnectDelay | 2 seconds |
ReconnectBackoffFactor | 2.0 |
MaxReconnectDelay | 60 seconds |
AuthTimeout | 30 seconds |
KeepAlivePingDelay / KeepAlivePingTimeout | 25s / 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.
using var client = new DaasLiveFeedClient(new DaasLiveFeedOptions { ApiKey = apiKey });
await foreach (LiveUpdate update in client.SubscribeAsync(ct))
{
// Your logic. Heartbeats are already filtered out.
}Questions about the SDK, onboarding, or requesting an API key?