RocketMonsters.Heimdall.ServiceBus.Client 1.0.7

dotnet add package RocketMonsters.Heimdall.ServiceBus.Client --version 1.0.7
                    
NuGet\Install-Package RocketMonsters.Heimdall.ServiceBus.Client -Version 1.0.7
                    
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="RocketMonsters.Heimdall.ServiceBus.Client" Version="1.0.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RocketMonsters.Heimdall.ServiceBus.Client" Version="1.0.7" />
                    
Directory.Packages.props
<PackageReference Include="RocketMonsters.Heimdall.ServiceBus.Client" />
                    
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 RocketMonsters.Heimdall.ServiceBus.Client --version 1.0.7
                    
#r "nuget: RocketMonsters.Heimdall.ServiceBus.Client, 1.0.7"
                    
#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 RocketMonsters.Heimdall.ServiceBus.Client@1.0.7
                    
#: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=RocketMonsters.Heimdall.ServiceBus.Client&version=1.0.7
                    
Install as a Cake Addin
#tool nuget:?package=RocketMonsters.Heimdall.ServiceBus.Client&version=1.0.7
                    
Install as a Cake Tool

RocketMonsters.Heimdall.ServiceBus.Client

First-party .NET client for the Heimdall ServiceBus public dispatch API. Dispatch a typed subscriber message (e.g. a print job) to a provisioned device in two lines. Devices are addressed by their subscriber key — the operator-chosen, account-unique identifier shown in the Heimdall dashboard and on the device UI.

The payload can be a pre-serialized JsonElement or any object — the object overload serializes it with reflection (member names verbatim); AOT-compiled consumers should pre-serialize and use the JsonElement constructor.

Install

dotnet add package RocketMonsters.Heimdall.ServiceBus.Client

Supported frameworks

.NET Core 3.1+, .NET 5+, and .NET Framework 4.6.2+ via the netstandard2.0 asset; a net10.0 asset is used automatically on .NET 10.

Usage

services.AddHeimdallServiceBus("token");                       // registration

var result = await client.DispatchAsync(
    new PrintLayoutMessage(4210, "front-desk", "kitchen-1", "order-ticket", payloadJson), ct); // usage

if (result.Succeeded && result.EventId is { } eventId)
{
    // Polling cadence stays application-owned; this call performs one logical lookup.
    var current = await client.GetDispatchStatusAsync(eventId, ct);
    if (current.Succeeded)
        Console.WriteLine($"{current.Snapshot!.Status} at {current.Snapshot.CompletedAt}");
}

The first argument is the account id — the numeric core.account.id of the target account (wire field accountId). It is required: every dispatch names its target explicitly. Pass a sub-client account id to dispatch into the server token's token subtree, or your own account id to dispatch to yourself. Your account id is shown in the Heimdall dashboard.

An account id outside the token's subtree returns 404 dispatch.subscriber_not_provisioned — the same uniform response as an unknown subscriber, so the surface cannot be used to enumerate accounts. A non-positive account id throws ArgumentOutOfRangeException from the constructor, before any HTTP call.

ExternalId is an optional free-text tag you set to cross-reference your own events with the Heimdall event rows they produced (wire field externalId). Set it as an init property or pass it as a trailing constructor argument — existing 5-argument call sites are unaffected either way:

new PrintLayoutMessage(4210, "front-desk", "kitchen-1", "order-ticket", payloadJson)
{
    ExternalId = "POS-ORDER-4471",
}

// or, equivalently
new PrintLayoutMessage(4210, "front-desk", "kitchen-1", "order-ticket", payloadJson, "POS-ORDER-4471")

The value is trimmed, and 20 characters after trimming is the limit. Null, empty, and whitespace-only are all the same as omitting it: the property stores null and the field is left out of the request body entirely. A longer value throws ArgumentOutOfRangeException from the property setter — including via message with { ExternalId = ... } — before any HTTP call; the server enforces the same limit independently and returns 422 dispatch.external_id_too_long. Heimdall attaches no meaning to the value, does not require it to be unique, and passes it through to the event row and on to the device inbox.

TtlSeconds is an optional dispatch time-to-live, in whole seconds, minimum 1 (wire field ttlSeconds). Set it as an init property:

new PrintLayoutMessage(4210, "front-desk", "kitchen-1", "order-ticket", payloadJson)
{
    TtlSeconds = 30,
}

Omitted (null) means the event never expires — the field is left out of the request body entirely. A value less than 1 throws ArgumentOutOfRangeException from the property setter — including via message with { TtlSeconds = ... } — before any HTTP call. Expiry is measured from the event's server-side created_at, not from when the device receives the message. An event that expires before delivery is reported back as the terminal status Expired and is not printed.

Breaking change

AccountId used to be an optional init property that defaulted to the token's own account. It is now the required first constructor argument, so every existing call site must add it:

// before
new PrintLayoutMessage("front-desk", "kitchen-1", "order-ticket", payloadJson)

// after — 4210 is your own account id if you are not targeting a sub-client
new PrintLayoutMessage(4210, "front-desk", "kitchen-1", "order-ticket", payloadJson)

The break is a compile error at every call site, never a silent change of target. The wire now always carries accountId; the server has accepted that field since the token-subtree dispatch release and treats an explicit home-account id exactly as it treated the omitted field.

AddHeimdallServiceBus targets https://heimdall.rocket-monsters.com by default; use the Action<HeimdallServiceBusOptions> overload to override BaseUrl, MaxRetries, or AttemptTimeout for staging/local or tuned resilience:

services.AddHeimdallServiceBus(o =>
{
    o.Token = "token";
    o.BaseUrl = "https://staging.example.test";
    o.MaxRetries = 3;                          // default — additional attempts after the first
    o.AttemptTimeout = TimeSpan.FromSeconds(15); // default — per-attempt time budget
});

Retry and never-throws contract

DispatchAsync does not throw for any HTTP-level outcome, transport failure, or attempt timeout — every one of those, including an exhausted retry, is a DispatchResult. Check DispatchResult.Succeeded and DispatchResult.Error. Cancellation of the caller's own CancellationToken (mid-attempt or mid-backoff) always propagates.

A 2xx response whose body cannot be read as the accepted payload — a captive-portal HTML page returned with a 200/202, a truncated body, or an exotic, disabled or unregistered response charset — does not throw and does not report success. It returns DispatchError(<status>, "invalid_response", null), because the eventId is the sole message identity and a success carrying a null eventId would look like an accepted dispatch the caller can never poll for.

A transient failure — a connection-level exception, a per-attempt timeout, or an HTTP 408/429/5xx response — is retried up to MaxRetries additional times with exponential backoff and full jitter (base 0.5s, doubling, 8s cap). A Retry-After header on 429/503 is honored verbatim (capped at 8s, no jitter) in preference to the computed backoff. Every other 4xx response (401, 403, 404, 413, 422, etc. — everything except 408/429) is returned immediately, unretried.

Once retries are exhausted: a transient HTTP response returns its ordinary HTTP-shaped result; a transport exception or attempt-timeout returns DispatchResult.Error with Status = 0 and Message "transport_error" or "timeout".

At-least-once, not exactly-once: retrying a request whose response was lost in transit (the server received and processed it, but the acknowledgement never arrived) may create a duplicate message. This is an accepted tradeoff for restaurant-network reliability — there is no idempotency key.

Dispatch outcomes

GetDispatchStatusAsync(eventId) returns one compact Current Snapshot and uses the same transport/timeout/HTTP retry policy as DispatchAsync. It does not hide a polling loop or total poll timeout. A Completed status means the thermal printer explicitly confirmed physical completion (or a non-thermal File/CUPS sink completed). SentUnconfirmed means the dispatch is resolved and the queue may continue, but printer completion was disabled or its response was irretrievably lost. WaitingForPrinter is non-terminal and includes the latest bounded printer diagnostic when available.

Cadence is caller-owned — this client does not implement a poll loop or backoff schedule for status lookups — but it is not unbounded: the lookup endpoint sits behind its own per-IP rate-limit policy, separate from the Heimdall public API's general 30/min window, sized for a polling workload (120 requests/minute per IP at the time of writing; the server is the source of truth for the current value). Exceeding it returns HTTP 429 with the standard ErrorResponse(429, "rate_limited") envelope and a Retry-After header — GetDispatchStatusAsync already treats a 429 as transient and honors Retry-After on its own retry path (see "Retry and never-throws contract" above), so a caller polling faster than the limit will see individual lookups retried/delayed rather than fail outright, up to MaxRetries. Callers polling multiple concurrent dispatches from the same outbound IP should budget their overall cadence against this shared per-IP ceiling, not just per-dispatch.

A malformed or non-JSON 2xx response (e.g. a proxy or captive-portal page) never throws; it returns DispatchStatusResult.Succeeded = false with DispatchError(200, "invalid_response", null).

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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
1.0.7 119 8/13/2026
1.0.6 105 8/7/2026
1.0.5 117 7/27/2026
1.0.4 110 7/27/2026
1.0.3 116 7/20/2026
1.0.2 114 7/17/2026
1.0.1 116 7/16/2026
1.0.0 143 7/16/2026