The claim
A device integration should answer with a closed set of typed outcomes, never a boolean. It should read the transport's status first and the device's own status second, treat a body with no status as proof of nothing, verify every write that is meant to converge by reading the object back, present a fresh authentication challenge on every request, and put a deadline on anything that unlocks a door. None of this is theory. Most of these rules came from a measurement against a real panel, taken while Hexavera Access was being re-planned in October 2026; the deadline came from asking which way a door fails.
Context
Hexavera Access reaches access-control hardware over ISAPI, the vendor's HTTP API, which authenticates with HTTP
Digest (RFC 7616) and answers in XML or JSON. The first connector had one interface with 27 members, every write
returning a boolean. A body without a statusCode counted as success. An unknown door action was sent as "open". The
canonical models carried the vendor's storage layout, and two of the three capability flags defaulted to true and were never set.
A probe on 2026-10-05 covered three panels of a widely used vendor, two fingerprint terminals (DS-K1T502DBFWX-C and DS-K1T805MBFWX, firmware V1.9.1) and a door controller (DS-K2702WX-E1(P), firmware V1.7.2), with ledgers that recorded every exchange: write-and-restore on the two terminals, read-only on the controller. Three facts decided the redesign.
- A failed fingerprint write answered HTTP 200 with no
statusCode. The connector would have called it a success. - A write of the reader's default verification mode answered
statusCode 1and changed nothing. The vendor's documentation listsstatusCodeas "0 (OK), 1 (OK), 2 (Device Busy), 3 (Device Error), 4 (Invalid Operation), 5 (Invalid XML Format), 6 (Invalid XML Content), 7 (Reboot Required)", so 1 reads as success. The device acknowledged and did not apply. - A probe client that reused a digest nonce was answered once and refused the second time, with HTTP 401 and the message "Triggered by illegal login." That the reuse caused the refusal is an inference, since another client dialled the same panel from the same address; the standard allows it, since RFC 7616 says "an implementation might choose not to accept a previously used nonce" and describes the nonce count as the way a server detects replay: "if the same nc value is seen twice, then the request is a replay."
A boolean true could therefore mean applied, acknowledged and ignored, or failed with a 200. That is not a contract.
The outcome set
Every operation returns exactly one of these, and the set is closed. A connector that meets an answer it cannot
classify returns Indeterminate for a write and NotDelivered for a read, never success.
| Outcome | Meaning | What the site does with it |
|---|---|---|
Applied(observed) |
The device accepted the request and, for a convergent write, a read-back observed the desired state | terminal success; stores what was observed |
AlreadyInState |
The device already held the desired state | terminal success |
Refused(code, vendorDetail) |
The device understood and refused, or accepted and did not apply | terminal; the device and item pair is blocked with its reason and an alarm |
NotSupported(capability) |
The device lacks the capability | terminal; the device's capability document is refreshed |
AuthRejected(lockHint) |
Authentication failed after credentials were sent | closes the device's gate; holds its commands; alarm |
Busy(retryAfter) |
Importing, upgrading, or a search timed out on the device | retries with jitter; never abandons a revocation |
NotDelivered(transportFault) |
The request did not reach the device, or a read did not complete | safe to retry; counts towards the device's breaker |
Indeterminate(reason) |
A write may or may not have taken effect | convergent: read back, apply again if needed; momentary: Unknown |
TransportNotSecure |
The operation carries or returns a secret and the link is not verified TLS | terminal; blocked, and the reason is shown |
In C#, the set is an abstract record with one sealed record per case. The language cannot close a hierarchy for you, so the conformance suite has to: a connector that returns anything outside the set fails it.
public abstract record Outcome
{
public sealed record Applied(string? Observed) : Outcome;
public sealed record AlreadyInState : Outcome;
public sealed record Refused(RefusalCode Code, string? VendorDetail) : Outcome;
public sealed record NotSupported(Capability Capability) : Outcome;
public sealed record AuthRejected(TimeSpan? LockHint) : Outcome;
public sealed record Busy(TimeSpan? RetryAfter) : Outcome;
public sealed record NotDelivered(TransportFault Fault) : Outcome;
public sealed record Indeterminate(string Reason) : Outcome;
public sealed record TransportNotSecure : Outcome;
}
Why not exceptions for refusals? Because a refusal is an outcome every caller handles, not an error; a full capacity table or an overlapping schedule is Tuesday, not a fault.
The classification order, and the read-back rule
- A transport fault before the request is sent is
NotDelivered. A timeout or a reset after a write was sent isIndeterminate; after a read it isNotDelivered. - Then the HTTP status. The vendor documents that when a request fails "the device will return the HTTP status code and ISAPI error code".
- Then the device's status fields:
statusCode,subStatusCode,errorCode, the last one decimal on the panels and hexadecimal in the vendor's documents, so both are parsed. - A body with no
statusCodeis not success by itself. For a read it must parse into the expected document. For a write it proves nothing. - Every convergent write is verified by a read of the same object before it is
Applied(observed). A write the device acknowledged and did not apply becomesRefused(NotApplied), with an alarm, and is not retried in a loop.
Only canonical values cross the boundary: door action and mode, direction, credential kind, event kind, verification
factor, link trust mode, refusal codes. The raw vendorCode and vendorDetail travel for diagnostics, and nothing
branches on them. Capabilities are read from the device, never from documents; the vendor says of its own tables that
"the values shown in this document are only examples for reference". An unknown capability is null, never true.
A fresh challenge on every request
The connector's HTTP client keeps PreAuthenticate off. With it on, .NET sends the Authorization header "with
subsequent requests ... without waiting to be challenged by the server". .NET 10's handler implements that for Basic
only, so today the setting would not replay a Digest nonce; after fact three, that is not left to an implementation
detail on a device that locks accounts: the terminals report issupIllegalLoginLock true. Each request takes the 401 challenge and answers it. A 401 after the
handshake closes the device's gate and raises an alarm, with no retry loop, because a retry loop against a locking
device is how you lock yourself out of a building.
The cost is measured: 211 requests to one terminal and 190 to the other in the write-and-restore probe, every one two exchanges, and not one 401 after the handshake.
Deadlines, and what fails which way
Every call carries a deadline, and the host runs one lane per device with one call in flight. Momentary commands (a
door pulse) run at most once, carry a notAfter, and become Unknown when the result is lost; a lost pulse shows
Unknown, never a second opening. Convergent commands (a person's cards, a schedule) are retried until terminal,
superseded by newer generations, and read back.
Door modes split by which way they fail. A locking mode (a lockdown) converges with no deadline: it fails secure,
and applying it late is still right. An unlocking mode (held open for an emergency or for deliveries) carries a
notAfter, and a panel that could not apply it before then never applies it later: the command ends
Refused(Expired), the desired mode returns to normal, and the operator is told. The reason is the one that decided
it: a panel that returns hours after an emergency would unlock a door nobody is watching. An emergency open is also
time-boxed and ends with an explicit restore.
One more measured fact shaped a rule here. On a terminal, a remote open was followed in the same second by "Remain Closed Stopped" in the event log, with the remote close seven seconds later. The event order is a fact and the cause is an inference, so a remote open on a door in lockdown is refused unless the operator confirms the release.
Secrets travel only over verified TLS
The terminals return person PINs in clear on read, and the vendor documents that fingerprint templates can be read
back (a read not yet verified on the test panels). So PINs, duress and
door codes and templates are written only over a link whose trust mode is verified TLS (the appliance's own CA, or a
pinned leaf certificate), and no enumeration that returns PINs runs over anything else. A panel left on plain HTTP
or on unverified TLS receives cards only and TransportNotSecure for the rest. The site's choice of a weaker mode is
permitted, explicit, visible at startup and in the status surface, and it still covers cards, revocations and events.
What it cost
- Two exchanges per authenticated request, accepted for the lock it avoids.
- A read-back after every convergent write, which adds a read to every write of a person sync and makes
Appliedmean something. - A late unlock is dropped, not delayed; an operator decides again.
- A conformance suite that every connector passes over a fake device built from captured, redacted payloads.
What a team can take from this
Before you design a device contract, probe the device with a ledger and keep every exchange. Then make the outcome type say what the device can actually answer, read the transport before the body and the body before you believe anything, and read back what you wrote. Decide for each command which way it fails, and give a deadline to the ones that fail open. And treat authentication as a thing a device can take away from you: fresh challenge, no retry loop, alarm.
Principal software engineer, Belgrade
Designs and builds secure, multi-tenant .NET platforms. Questions about this entry, or a different view on it, are welcome by email.