Skip to main content

Edge connector protocol

The Edge connector speaks the same envelope, hello, heartbeat and job lifecycle as Axis agents and connectors (Agent and connector protocol), and adds the messages and jobs of access control. The Go types in entrosity-shared-go/proto/edge are the single source of truth; this page describes intent and behaviour.

Like the rest of the protocol, these types only grow: fields are added, never renamed or repurposed, so connectors in the field keep working.

Transport​

  • WebSocket: wss://hub.entrosity.com/edge/api/connector/v1/ws with Authorization: Bearer <connector key>. JSON text frames, one envelope per frame, at most 1 MiB; the server pings every 30 seconds.
  • The first message must be hello (capabilities: ["access_control", "edge_update"], pending_job_ids); the server answers hello.ack and then delivers the pending jobs. edge_update means the connector installs self-updates (Self-update); connectors without it are never offered one. Anything before hello closes the socket (1008).
  • The connector sends heartbeat every minute. It is online from its hello until the socket closes, or after 3 minutes without a message.
  • Jobs follow job.assign → job.ack → job.progress → job.result. A job sent without job.ack within 60 seconds is sent again; a job not finished before its expiry becomes timeout.
  • Close codes: 4003 the key was revoked (connector removed, tenant suspended); 4009 a newer connection of the same installation took over; 1008 policy violation.

HTTP endpoints (/api/connector/v1)​

Method and pathPurpose
POST /enroll{enrollment_token, hostname, domain, version} → {connector_id, connector_key, ws_url}. The token must be an Edge connector token. The same computer (tenant, host name, domain) enrolling again rotates its key and closes the old connection. Rate-limited per IP (EDGE_ENROLL_RATE_PER_MINUTE).
GET /wsThe WebSocket.
POST /heartbeatHTTP fallback of heartbeat.
GET /jobs/pending, POST /jobs/{id}/ack, …/progress, …/resultHTTP polling fallback of the job lifecycle.
POST /eventsHTTP fallback of edge.events: body an EventsChunk (gzip accepted; 4 MiB compressed, 8 MiB decoded), response the EventsAck.
POST /controllers/statusHTTP fallback of edge.controller.status.
GET /releases/{id}/msi?exp=&sig=Download of a connector release's MSI, by the signed link in an update_agent job. No connector key: the link (valid 24 hours, signed with a key derived from EDGE_MASTER_KEY) is the authorization; an expired or altered link answers 403 download_link_invalid.

All but /enroll and the release download need the connector key. Everything a connector does is confined to its tenant.

Messages​

Connector → server​

TypePayloadWhen
edge.events{first_seq, events: [Event]}: 1–500 consecutive events (events[i].seq = first_seq + i)Whenever the connector's queue has events.
edge.controller.status{controllers: [{controller_id, online, applied_version, applied_hash?, error?, doors?: [{index, open, locked}], info?: ControllerInfo}]} (up to 1,000 controllers)At least every minute, and at once after a change.

Server → connector​

TypePayloadWhen
edge.events.ack{acked_seq}, in reply (reply_to) to edge.eventsEvery event up to acked_seq is stored; the connector drops them from its queue.

Jobs​

JobPayloadResultTimeout / expiryConnector lane
edge.config.apply{controller: ControllerRef, version, hash, snapshot: ConfigSnapshot}{version, hash, cards}10 min / 7 days, priority 10config (one at a time)
edge.controller.remove{controller_id}{}1 min / 30 daysconfig
edge.door.open{controller: ControllerRef, door_index, pulse_ms, requested_by?}{}30 s / 30 s, priority 100door (4 in parallel)
edge.discover{driver, transport?, addresses?: ["host:port", …]} (up to 256; empty for the simulator lists every simulated controller){controllers: [ControllerInfo]}2 min / 5 mindiscover (one at a time)
edge.controller.test{controller: ControllerRef}ControllerInfo1 min / 5 mindiscover
update_agentUpdateJob (component: edge-connector, see Self-update)UpdateResult30 min / 24 hupdate
  • ControllerRef is {controller_id, driver, target}; target (edge.Target) is {transport: "tcp"|"rs485", address, unit_id?, pin?}: address is host:port, a serial device (COM3) or a Modbus gateway host:port; unit_id is the RS-485 bus address 1–31, absent on TCP.
  • pin (uint32, 1–4294967294; omitted when unset) is the controller's PIN (TrackBase002: every command frame carries it). It is not part of the target's key (transport, address, unit id), so a new PIN is the same controller. Edge stores it encrypted and adds it when it delivers a job: job payloads stored in the database carry no PIN. The connector keeps it in controllers.json and never sends it back: ControllerInfo in discovery and test results, and status reports, have it cleared.
  • ControllerInfo is {driver, target, model, serial, firmware, door_mode?}.
  • edge.config.apply makes the controller hold exactly the snapshot. A new job for a controller cancels its older open apply job. A job older than the configuration the connector already applied (delivered late) succeeds with the newer version. The connector stores the last applied snapshot and restores it at start to controllers that lost it. A new PIN, address, transport or unit id is sent as an apply of the unchanged configuration (same version and hash): the connector only updates the target, with no full rewrite.
  • edge.controller.remove stops polling the controller and forgets its configuration. It is sent when a controller is removed or disabled; a disabled controller that is enabled again gets an edge.config.apply (forced), which registers it with the connector again.
  • edge.door.open releases the lock for pulse_ms (the door's unlock time; 3 s when absent). The controller reports door_opened_remote with requested_by in the detail. Its short expiry means a door is never opened long after the request.

Self-update (update_agent)​

The same job type and payload as for Axis agents and connectors (Protocol), with component: edge-connector:

{"release_id": "…", "component": "edge-connector",
"target": {"version": "1.4.0", "url": "https://…/api/connector/v1/releases/…/msi?exp=…&sig=…",
"sha256": "…", "size_bytes": 9437184, "signature": "…"},
"rollback": {"version": "1.3.2", "url": "…", "sha256": "…", "size_bytes": 9412608, "signature": "…"}}
FieldMeaning
release_idEdge's id of the offered release.
targetThe MSI to install: version, url (a signed download link valid 24 hours, added when the job is delivered), sha256, size_bytes, and signature, the Ed25519 signature (base64) of "rmm-release-v1\n<component>\n<version>\n<sha256>\n<size>\n" with EDGE_RELEASE_SIGNING_KEY.
rollbackThe same for the version the connector runs, when Edge still has it: the watchdog reinstalls it if the new version does not start.

JobResult.Result is an UpdateResult: {scheduled, from_version, to_version, rollback} (scheduled: the installer runs in a minute from the scheduled task; rollback: a previous MSI is kept for the watchdog). The job succeeds once the installation is scheduled; the new version shows in the connector's next hello.

Edge offers an update when a connector says hello and every 5 minutes (releases.rollout): the newest release of the connector's update channel (stable: stable releases; beta: beta and stable) that is newer than the connector's version, only to connectors that announce edge_update, not while an update_agent job of the connector is open, and the same version not again within an hour. What the connector does: Edge connector → Self-update.

Job error codes​

job.result.error_code of Edge jobs:

CodeMeaning
capacity_exceededThe snapshot has more cards than the controller holds, or the controller's card memory is full.
controller_unreachableThe controller did not answer.
config_rejectedThe controller, or the connector's check of the snapshot, refused the configuration.
unsupportedThe driver cannot reach the controller over this connection (trackbase002 over RS-485/serial).
unknown_driverThis connector build has no driver of that name.
invalid_payloadThe payload could not be decoded or is invalid.
exec_failedThe connector restarted before the job finished; for update_agent, the update folder or scheduled task could not be created.
update_unsignedupdate_agent: the build has no release public key and refuses updates.
signature_invalidupdate_agent: a release signature did not verify.
download_failed, hash_mismatchupdate_agent: the MSI could not be downloaded, or does not match the release's SHA-256 and size.

Configuration snapshot​

{
"timezone": "Europe/Sofia",
"door_mode": "two_unidirectional",
"doors": [{"index": 1, "lock_relay": 1, "open_pulse_ms": 3000, "held_open_seconds": 30}],
"readers": [{"channel": "wiegand1", "door_index": 1, "direction": "in"}],
"schedules": [{"slot": 1, "name": "Office", "intervals": [{"day": 0, "start": 480, "end": 1080}]}],
"holidays": ["2026-12-25"],
"cards": [{"kind": "wiegand26", "facility_code": 12, "card_number": 3456,
"card_id": "…", "grants": [{"door_index": 1, "schedule_slot": 1}]}]
}
FieldMeaning
timezoneIANA time zone the schedules and holidays are expressed in: the site's, else the tenant's default, else UTC.
door_modeone_bidirectional (one door, entry and exit reader) or two_unidirectional (two doors, one reader each).
doorsDoor index 1–2, lock_relay 1–4, open_pulse_ms, held_open_seconds (0: never raise door_held_open).
readersEnabled readers only: channel (wiegand1, wiegand2, ibutton1, ibutton2), the door it serves, direction (in, out).
schedulesThe schedules the controller's doors use, numbered slot 1–255. intervals are [start, end) in minutes since midnight; day 0 = Monday … 6 = Sunday, 7 = holiday windows.
holidaysDates (YYYY-MM-DD, in timezone) from the day before up to 90 days ahead, on which the holiday windows apply instead of the weekday's.
cardsEvery card that may pass at least one of the doors (at most 2,000): the credential (wiegand26 with facility_code 0–255 and card_number 0–65535, or ibutton with ibutton_id, 2–16 hex digits), Edge's card_id, and its grants (door and schedule slot).

hash is the SHA-256 (hex) of the snapshot's JSON; every list is ordered deterministically, so the same configuration always has the same hash. The connector reports the hash the controller holds in applied_hash; a different hash on an online controller that is not syncing makes the server send the configuration again.

The reference decision (ConfigSnapshot.Allows, implemented by the drivers and the simulator): an unknown credential → unknown_card; no grant for the door → access_denied/no_access; a grant whose schedule has an open window now (holiday windows on a holiday) → access_granted; otherwise access_denied with holiday on a holiday, else outside_schedule.

Events​

{"seq": 1042, "controller_id": "…", "occurred_at": "2026-09-28T06:00:12Z",
"type": "access_granted", "door_index": 1, "reader_channel": "wiegand1",
"direction": "in", "credential": {"kind": "wiegand26", "facility_code": 12, "card_number": 3456}}
FieldMeaning
seqAssigned by the connector's queue: strictly increasing per connector installation.
controller_id, occurred_at, typeRequired. Types: access_granted, access_denied, unknown_card, door_opened_remote, door_forced, door_held_open, door_closed, controller_online, controller_offline, config_applied, tamper.
door_index1–2; absent for controller events.
reader_channel, directionThe reader, for badge events.
credentialThe card read.
reasonFor access_denied: no_access, outside_schedule, holiday.
detailFree text, at most 512 characters (who opened a door remotely, driver messages).

Exactly once: the server keeps, per connector, the highest sequence number it stored. A chunk at or below it is a duplicate and only acknowledged; the part of a chunk above it is stored and the cursor moved, in one transaction. The connector keeps events on disk until they are acknowledged, so a lost acknowledgement, a restart or an outage only causes a resend. Over the WebSocket, a chunk without acknowledgement within 15 seconds is sent again over HTTP.

On ingest the server resolves the controller, door, reader, card and cardholder of each event (history keeps them even after deletions) and streams the new entries to the web app. occurred_at more than 24 hours in the future or more than a year in the past is replaced by the time of arrival, with a note in the detail.

controller_online and controller_offline are recorded by the server from edge.controller.status transitions.

Limits​

LimitValue
Cards per controller2,000
Schedules per controller (slots)255
Doors per controller, relays2, 4
RS-485 bus addresses1–31
Events per edge.events chunk500 (the connector also keeps a chunk under 256 KiB)
Controllers per status report1,000
WebSocket message1 MiB
Addresses per discovery256