Docs/Developer/Atlas API - Connectors
DeveloperAPI

Atlas API - Connectors

A connector is an external process that holds a persistent SignalR connection to Atlas and supplies values for binding paths.

Updated 12 Aug 2026Timebase Docs

Overview#

A connector is an external process that holds a persistent SignalR connection to Atlas and supplies values for binding paths. Atlas exposes that population two ways, and this article covers the first:

  • The REST surface. GET /api/v1/connectors tells you which connectors are registered with Atlas right now, under what identity, and how many datasources each advertises. This is the only HTTP observable Atlas publishes about connectors, and it is the first thing to check when data is not arriving.
  • The wire protocol. The SignalR hub at /hubs/connector. This article gives you only enough of it to know what the REST surface is describing. Every message shape, [Key(n)] number, union discriminator, enum ordinal and code sample lives in Connector Protocol Reference, which is the canonical owner of the wire contract. Do not implement a connector from this page.

This article is written for developers. For the base address, the bearer token story and the error format shared by every /api/v1 route, see Atlas API Overview. For a step-by-step "the connector is connected but no values appear" procedure, see Connector Diagnostics - Data Not Flowing, which owns troubleshooting.

If you are updating an older integration, read this first. The point-in-time pull method GetValue, its ConnectorValueRequest / ConnectorValueEnvelope types and all eight *ValueResponse records are not part of the current protocol. Atlas will never invoke a GetValue handler. A connector also does push data unprompted: after Atlas opens a subscription, the connector sends PushSubscriptionRows, PushSubscriptionStatus and PushSubscriptionWatermark on its own initiative. Both corrections are covered in full in Connector Protocol Reference.

GET /api/v1/connectors#

Returns a snapshot of Atlas's connector registry: one entry per connector connection that is currently registered.

GET /api/v1/connectors

The GET /api/connectors route is superseded and no longer exists; a request to it returns the single-page-application shell as 200 text/html, not a 404.

Authentication#

The group requires authentication, which attaches the bearer-only ApiAccess policy whenever Pulse:Auth:Enabled is true. Pulse:Auth:Enabled: true is the shipped default, so in a normal deployment this call requires a Pulse-issued bearer whose aud claim includes Atlas's Pulse:Auth:ClientId (Atlas by default). ApiAccess pins the JWT bearer scheme specifically, so a missing or invalid token is answered with 401, not a redirect to the sign-in page.

There are no query parameters, no pagination and no filters. The endpoint takes nothing.

Request#

curl -s https://<your-atlas-server>:4552/api/v1/connectors \
  -H "Authorization: Bearer $ATLAS_TOKEN" \
  -H "Accept: application/json"

Response#

200 application/json, a JSON array at the top level. There is no envelope and no items wrapper.

[
  {
    "connectorId": "conn.historian",
    "displayName": "Historian Connector",
    "datasourceCount": 2,
    "registeredAt": "2026-08-07T09:14:22.4180000+00:00",
    "transportId": "t-conn-1"
  }
]
Field Type Always present What it is
connectorId string yes The stable identity. The identifier the connector advertised for itself in its handshake, for example conn.historian. It survives SignalR reconnects and Atlas restarts because the connector, not Atlas, chooses it. Key your own records on this.
displayName string yes The human-readable label from the same advertisement. Shown in the Atlas user interface. Not unique and not an identifier.
datasourceCount integer yes How many DatasourceDescriptor entries the advertisement carried. A count only - the endpoint never returns the datasource ids or the binding paths. 0 means the connector handshook while advertising nothing.
registeredAt string (ISO 8601 date-time, UTC) yes Wall-clock time of the most recent Register call for this connection, that is, the last handshake, not the moment the socket opened. A re-handshake on a live connection moves this forward.
transportId string no Optional diagnostic only. The SignalR connection id for the current session. It is process-scoped and changes on every reconnect, which is exactly why it is no longer the identity. Omitted from the JSON when null - Atlas's global serializer drops nulls. Never treat it as a key.

Field names are camelCase and enums serialize as camelCase strings, because every Atlas HTTP surface shares one serializer configuration. The wire shape is V1ConnectorEntry, projected from ConnectorStatusEntry.

connectorId versus connectionId#

The older name for this surface reported a connectionId, and some client code still keys on it. That was the SignalR transport id: process-scoped, reset on every reconnect, and useless as a durable reference. Today:

  • connectorId is the identity. Use it as a dictionary key, a display key, a React key, and the correlation value in your own logs.
  • transportId is the same SignalR connection id, demoted to an optional diagnostic field. It is useful for exactly one thing: matching an entry to a ConnectionId in an Atlas log line. identifier, and asserts transportId carries the SignalR connection id.

Status codes#

Code When
200 Always, on any authenticated call.
401 Pulse:Auth:Enabled=true and the request carried no bearer, or an invalid one.
403 A token that authenticates but does not satisfy ApiAccess, typically a wrong or missing aud claim.

The handler is a pure read over an in-memory registry and has no failure path of its own. If it ever did fail it would surface as RFC-9457 application/problem+json like every other /api/v1 route - see Atlas API Overview.

How to read the result#

Four behaviours of this list are load-bearing and none of them is obvious from the shape:

  • Presence means online. Absence means offline. There is no grace period. The registry entry is created by ConnectorHub.Handshake and removed by ConnectorHub.OnDisconnectedAsync. A connector that is dropping and reconnecting flickers in and out of this list.
  • Connecting is not enough. The entry appears only after a successful handshake. A connector that has negotiated the socket but not yet invoked Handshake is not here.
  • The list is per connection, not per connector. The registry is keyed by SignalR connection id (Snapshot()). Nothing rejects two live connections that advertise the same connectorId, so a duplicated connectorId in this response means you are running two instances, or a stale one has not dropped yet. That matters, because two connections advertising the same binding path resolve last-writer-wins with only a log warning.
  • Order is not specified. The snapshot is taken over a ConcurrentDictionary. Sort client-side if you need stable output.

Finding out which binding paths a connector serves#

No Atlas endpoint lists the binding paths a connector has advertised. datasourceCount is the only thing projected out. If you need to know whether a specific path is being served, get the path list from the connector's own interface and compare it against your instance-map bindings. Connector Diagnostics - Data Not Flowing covers that comparison.

Nor is there a create, update or delete. Connectors register themselves over the hub; there is no way to add one through the API.

GET /app/connectors (the user-interface twin)#

GET /app/connectors returns the identical shape from the identical query. It exists because the Atlas single-page application authenticates with a session cookie rather than a bearer: the route is gated by the cookie-based UiAccess policy when authentication is on, and it is marked .ExcludeFromDescription() so it never appears in the OpenAPI documents. The whole /app plane is private glue for the Atlas user interface, not a public integration surface. It is documented here only so you recognise it in a browser network trace. Build against /api/v1/connectors. A bearer token does not clear UiAccess, so pointing an integration at /app/connectors fails for a reason that looks like a token problem and is not.

The connector hub in outline#

Everything below is orientation. The contract itself is in Connector Protocol Reference.

Property Value
Route /hubs/connector
Ports 4551 HTTP, 4552 HTTPS
Protocol SignalR with MessagePack. A JSON SignalR client cannot interoperate.
Serializer options ConnectorMessagePackOptions.Options - mandatory, and must be bound on both ends
Authentication RequireAtlasHubAuth, which attaches the HubAccess policy when Pulse:Auth:Enabled=true

Authentication. HubAccess accepts either credential: the browser session cookie or a Pulse bearer. It pins both schemes and requires an authenticated user, with no role or admin requirement. Pinning the bearer scheme is what makes an unauthenticated negotiate answer 401 instead of redirecting a WebSocket handshake to a sign-in page. Pulse:Auth:Enabled: true is the shipped default, so a machine connector must supply a Pulse client-credentials token through SignalR's HttpConnectionOptions.AccessTokenProvider, and its Pulse client registration must list Atlas's Pulse:Auth:ClientId as an audience. With Pulse:Auth:Enabled=false the hub is open, because RequireAtlasHubAuth attaches nothing at all.

MessagePack options are not optional. A bare .AddMessagePackProtocol() connects successfully and then fails on every frame. Atlas binds ConnectorMessagePackOptions.Options, which adds LZ4 block-array compression and a native binary Guid resolver. A client without the compression cannot decode anything Atlas sends, and a client without the resolver corrupts every Guid on the wire, which is every subscription id. Bind the same Options instance on your side. The full explanation and the compilable connect sample are in Connector Protocol Reference.

The exchange has three phases, not two.

  1. Handshake. The connector invokes Handshake on the hub with a ConnectorAdvertisement and Atlas returns a HandshakeResponse carrying an AtlasSessionId. This is not fire-and-forget. The session id is minted once per Atlas process; a value different from the one you saw last time means Atlas restarted and every subscription id you hold is now unreachable, so you must drop your local subscription state. Ignoring the return value costs you restart detection and produces data loss. A re-handshake on the same connection is a full replacement of that connection's routes, and is the supported way to publish a changed path set.
  2. Backfill. Atlas invokes your registered GetRange handler and awaits a ConnectorRangeResponse. This is the only pull method that still exists.
  3. Live subscriptions. Atlas invokes your registered Subscribe handler and expects one ack per request. From then on the connector pushes unprompted with PushSubscriptionRows, PushSubscriptionStatus and PushSubscriptionWatermark until Atlas sends Unsubscribe. This is the only path by which live values reach Atlas. A connector that implements only GetRange can serve history and will never show a current value.

The complete method vocabulary is seven names:

Method Direction Your side
Handshake connector to Atlas you invoke it, and you read the response
GetRange Atlas to connector you register a handler
Subscribe Atlas to connector you register a handler, one ack per request, in order
Unsubscribe Atlas to connector you register a handler, fire and forget, no ack
PushSubscriptionRows connector to Atlas you send it
PushSubscriptionStatus connector to Atlas you send it
PushSubscriptionWatermark connector to Atlas you send it

There is no eighth. If your client registers a GetValue handler, delete it.

What happens when a connector disconnects#

Atlas calls _registry.Unregister(Context.ConnectionId) from OnDisconnectedAsync. Three things follow, and one thing that people expect does not happen.

What happens:

  1. The connector's entry leaves GET /api/v1/connectors immediately.
  2. Its binding-path reverse-map rows and compiled template matchers are swept, so those paths no longer resolve to any route.
  3. Every live subscription fanout for that connection is dropped locally. No upstream Unsubscribe is issued, because the wire is already gone.

What does not happen:

Affected properties do not flip to Bad quality. The structure-of-arrays cells retain their last written value, and the snapshot phase is replayed on re-handshake. The registry sweep writes no quality value anywhere. A property whose connector died an hour ago still reads its last good value, with its last good quality, and looks entirely healthy.

This is the single most misleading part of a connector outage, so plan your monitoring around it. The reliable staleness signals are:

  • Absence from GET /api/v1/connectors. This is the authoritative "is it online" check, and it is why the endpoint exists.
  • A frozen watermark. A live connector attests progress with PushSubscriptionWatermark even when nothing changed. A watermark that stops advancing is a stalled source.
  • A stale Timestamp field on a TVQ-bound property, compared against wall clock.

Do not poll a property's value and wait for it to change: it will not.

Quality does bind#

Connectors do supply quality, and it binds like any other value. If you have been told otherwise, the details below are what actually happens.

  • The property is atlas-common:Quality, not isa95-common:Quality, with rdfs:range of xsd:integer. Its comment records the encoding: the raw OPC quality code, whose status is the top two bits, where 0 is Bad, 64 is Uncertain and 192 is Good.
  • It is field order 2 of the shipped atlas-common:TVQ compound.
  • Seven connector plugin families emit it today as IntColumn("Quality", ...): Timebase Historian, OPC UA, CESMII i3X, Aveva Historian, Aveva PI System, Canary Labs Historian and InfoHub.

Emit Quality on every rowset, including empty ones. A cell that has not been written reads as 0, which is the code for Bad, so a TVQ rowset that omits the Quality column presents as a Bad reading rather than as an absent one. Column naming rules and the skip behaviour behind this are in Connector Protocol Reference.

Troubleshooting#

Troubleshooting is owned by Connector Diagnostics - Data Not Flowing, which walks the failure in order: registration, authentication, handshake, compile and binding path, subscription, and finally the property itself. Two entry points from this article:

Symptom Where to start
A connector you expect is missing from GET /api/v1/connectors Step 1 and step 2 of the diagnostics article. The most common cause is that Atlas ships Pulse:Auth:Enabled=true and the Timebase Connector ships Pulse:Auth:Enabled=false, so the connector attaches no credential and Atlas rejects the negotiate with a 401 that neither log names as an authentication failure.
The connector is listed, with the right datasourceCount, but values are stale or absent Step 4 onward. Registration proves the socket and the handshake; it proves nothing about whether the binding paths match your instance map.
The same connectorId appears twice You have two live connections. Binding paths are last-writer-wins with only a log warning, so the two instances are stealing each other's paths. Stop one.
GET /api/v1/connectors returns HTML instead of JSON Your client followed a redirect to a sign-in page. Send a bearer, and disable redirect-following on API calls. See Atlas API Overview.
esc
↑↓ navigate · ↵ open · ⌘K anywhere